diff --git a/.coveragerc b/.coveragerc index 76d4872011..9f2fdc8071 100644 --- a/.coveragerc +++ b/.coveragerc @@ -73,8 +73,6 @@ omit = homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/avea/light.py homeassistant/components/avion/light.py - homeassistant/components/avri/const.py - homeassistant/components/avri/sensor.py homeassistant/components/azure_devops/__init__.py homeassistant/components/azure_devops/const.py homeassistant/components/azure_devops/sensor.py @@ -102,7 +100,12 @@ omit = homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py homeassistant/components/bmp280/sensor.py - homeassistant/components/bmw_connected_drive/* + homeassistant/components/bmw_connected_drive/__init__.py + homeassistant/components/bmw_connected_drive/binary_sensor.py + homeassistant/components/bmw_connected_drive/device_tracker.py + homeassistant/components/bmw_connected_drive/lock.py + homeassistant/components/bmw_connected_drive/notify.py + homeassistant/components/bmw_connected_drive/sensor.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py @@ -386,7 +389,6 @@ omit = homeassistant/components/hvv_departures/sensor.py homeassistant/components/hvv_departures/__init__.py homeassistant/components/hydrawise/* - homeassistant/components/hyperion/light.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py @@ -568,6 +570,8 @@ omit = homeassistant/components/n26/* homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py + homeassistant/components/neato/__init__.py + homeassistant/components/neato/api.py homeassistant/components/neato/camera.py homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py @@ -578,14 +582,9 @@ omit = homeassistant/components/nest/api.py homeassistant/components/nest/binary_sensor.py homeassistant/components/nest/camera.py - homeassistant/components/nest/camera_legacy.py - homeassistant/components/nest/camera_sdm.py homeassistant/components/nest/climate.py - homeassistant/components/nest/climate_legacy.py - homeassistant/components/nest/climate_sdm.py - homeassistant/components/nest/local_auth.py + homeassistant/components/nest/legacy/* homeassistant/components/nest/sensor.py - homeassistant/components/nest/sensor_legacy.py homeassistant/components/netatmo/__init__.py homeassistant/components/netatmo/api.py homeassistant/components/netatmo/camera.py @@ -834,8 +833,9 @@ omit = homeassistant/components/solaredge_local/sensor.py homeassistant/components/solarlog/* homeassistant/components/solax/sensor.py - homeassistant/components/soma/cover.py homeassistant/components/soma/__init__.py + homeassistant/components/soma/cover.py + homeassistant/components/soma/sensor.py homeassistant/components/somfy/* homeassistant/components/somfy_mylink/* homeassistant/components/sonos/* @@ -1015,7 +1015,6 @@ omit = homeassistant/components/watson_tts/tts.py homeassistant/components/waze_travel_time/sensor.py homeassistant/components/webostv/* - homeassistant/components/wemo/* homeassistant/components/whois/sensor.py homeassistant/components/wiffi/* homeassistant/components/wink/* diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 56b181aa02..a33cd59f22 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore base Python virtual environment @@ -73,7 +73,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -118,7 +118,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -163,7 +163,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -230,7 +230,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -278,7 +278,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -326,7 +326,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -371,7 +371,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -419,7 +419,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -475,7 +475,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -555,7 +555,7 @@ jobs: - name: Check out code from GitHub uses: actions/checkout@v2 - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v2.1.4 + uses: actions/setup-python@v2.2.1 id: python with: python-version: ${{ env.DEFAULT_PYTHON }} @@ -785,4 +785,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.0.15 + uses: codecov/codecov-action@v1.1.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a69f0b444..c96a990433 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - pydocstyle==5.1.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit - rev: 1.6.2 + rev: 1.7.0 hooks: - id: bandit args: diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 218bb1132a..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -dist: focal -addons: - apt: - packages: - - ffmpeg - - libudev-dev - - libavformat-dev - - libavcodec-dev - - libavdevice-dev - - libavutil-dev - - libswscale-dev - - libswresample-dev - - libavfilter-dev - -python: - - "3.7.1" - - "3.8" - -env: - - TOX_ARGS="-- --test-group-count 4 --test-group 1" - - TOX_ARGS="-- --test-group-count 4 --test-group 2" - - TOX_ARGS="-- --test-group-count 4 --test-group 3" - - TOX_ARGS="-- --test-group-count 4 --test-group 4" - -jobs: - fast_finish: true - include: - - python: "3.7.1" - env: TOXENV=lint - - python: "3.7.1" - # PYLINT_ARGS=--jobs=0 disabled for now: https://github.com/PyCQA/pylint/issues/3584 - env: TOXENV=pylint TRAVIS_WAIT=30 - - python: "3.7.1" - env: TOXENV=typing - -cache: - pip: true - directories: - - $HOME/.cache/pre-commit -install: pip install -U tox tox-travis -language: python -script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox -vv --develop ${TOX_ARGS-} diff --git a/.vscode/launch.json b/.vscode/launch.json index 6976e26ebb..3d967b25c1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,7 +9,41 @@ "type": "python", "request": "launch", "module": "homeassistant", - "args": ["--debug", "-c", "config"] + "args": [ + "--debug", + "-c", + "config" + ] + }, + { + // Debug by attaching to local Home Asistant server using Remote Python Debugger. + // See https://www.home-assistant.io/integrations/debugpy/ + "name": "Home Assistant: Attach Local", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ], + }, + { + // Debug by attaching to remote Home Asistant server using Remote Python Debugger. + // See https://www.home-assistant.io/integrations/debugpy/ + "name": "Home Assistant: Attach Remote", + "type": "python", + "request": "attach", + "port": 5678, + "host": "homeassistant.local", + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "/usr/src/homeassistant" + } + ], } ] -} +} \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index 27614c3d49..a660d93012 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,7 +54,6 @@ homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland -homeassistant/components/avri/* @timvancann homeassistant/components/awair/* @ahayworth @danielsjf homeassistant/components/aws/* @awarecan homeassistant/components/axis/* @Kane610 @@ -323,6 +322,7 @@ homeassistant/components/onewire/* @garbled1 @epenet homeassistant/components/onvif/* @hunterjm homeassistant/components/openerz/* @misialq homeassistant/components/opengarage/* @danielhiversen +homeassistant/components/openhome/* @bazwilliams homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff @freekode @nzapponi @@ -355,6 +355,7 @@ homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff homeassistant/components/pvpc_hourly_pricing/* @azogue +homeassistant/components/qbittorrent/* @geoffreylagaisse homeassistant/components/qld_bushfire/* @exxamalte homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan @@ -453,6 +454,7 @@ homeassistant/components/tado/* @michaelarnauts @bdraco homeassistant/components/tag/* @balloob @dmulcahey homeassistant/components/tahoma/* @philklei homeassistant/components/tankerkoenig/* @guillempages +homeassistant/components/tapsaff/* @bazwilliams homeassistant/components/tasmota/* @emontnemery homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike @@ -491,6 +493,7 @@ homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @Cereal2nd @brefra homeassistant/components/velux/* @Julius2342 homeassistant/components/vera/* @vangorra +homeassistant/components/verisure/* @frenck homeassistant/components/versasense/* @flamm3blemuff1n homeassistant/components/version/* @fabaff @ludeeus homeassistant/components/vesync/* @markperdue @webdjoe @thegardenmonkey diff --git a/README.rst b/README.rst index 0de30d43c6..cf8323d2e8 100644 --- a/README.rst +++ b/README.rst @@ -5,7 +5,7 @@ Open source home automation that puts local control and privacy first. Powered b Check out `home-assistant.io `__ for `a demo `__, `installation instructions `__, -`tutorials `__ and `documentation `__. +`tutorials `__ and `documentation `__. |screenshot-states| @@ -14,8 +14,8 @@ Featured integrations |screenshot-components| -The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture `__ and the `section on creating your own -components `__. +The system is built using a modular approach so support for other devices or actions can be implemented easily. See also the `section on architecture `__ and the `section on creating your own +components `__. If you run into issues while using Home Assistant or during development of a component, check the `Home Assistant help section `__ of our website for further help and information. diff --git a/build.json b/build.json index 49cee1ff28..a7ce097ae8 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2020.11.2", - "armhf": "homeassistant/armhf-homeassistant-base:2020.11.2", - "armv7": "homeassistant/armv7-homeassistant-base:2020.11.2", - "amd64": "homeassistant/amd64-homeassistant-base:2020.11.2", - "i386": "homeassistant/i386-homeassistant-base:2020.11.2" + "aarch64": "homeassistant/aarch64-homeassistant-base:2021.01.0", + "armhf": "homeassistant/armhf-homeassistant-base:2021.01.0", + "armv7": "homeassistant/armv7-homeassistant-base:2021.01.0", + "amd64": "homeassistant/amd64-homeassistant-base:2021.01.0", + "i386": "homeassistant/i386-homeassistant-base:2021.01.0" }, "labels": { "io.hass.type": "core" diff --git a/docs/source/conf.py b/docs/source/conf.py index 242a90088b..ab09df87ae 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -207,7 +207,6 @@ html_theme_options = { "github_repo": PROJECT_GITHUB_REPOSITORY, "github_type": "star", "github_banner": True, - "travis_button": True, "touch_icon": "logo-apple.png", # 'fixed_sidebar': True, # Re-enable when we have more content } diff --git a/homeassistant/components/abode/translations/ca.json b/homeassistant/components/abode/translations/ca.json index 47bfd031c8..1d758bc439 100644 --- a/homeassistant/components/abode/translations/ca.json +++ b/homeassistant/components/abode/translations/ca.json @@ -1,13 +1,28 @@ { "config": { "abort": { + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "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" + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_mfa_code": "Codi MFA inv\u00e0lid" }, "step": { + "mfa": { + "data": { + "mfa_code": "Codi MFA (6 d\u00edgits)" + }, + "title": "Introdueix el codi MFA per a Abode" + }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix la informaci\u00f3 d'inici de sessi\u00f3 d'Abode." + }, "user": { "data": { "password": "Contrasenya", diff --git a/homeassistant/components/abode/translations/de.json b/homeassistant/components/abode/translations/de.json index b0f17918cd..43d6ba21ca 100644 --- a/homeassistant/components/abode/translations/de.json +++ b/homeassistant/components/abode/translations/de.json @@ -1,9 +1,28 @@ { "config": { "abort": { + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt." }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_mfa_code": "Ung\u00fcltiger MFA-Code" + }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA-Code (6-stellig)" + }, + "title": "Gib deinen MFA-Code f\u00fcr Abode ein" + }, + "reauth_confirm": { + "data": { + "password": "Passwort", + "username": "E-Mail" + }, + "title": "Gib deine Abode-Anmeldeinformationen ein" + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/abode/translations/hu.json b/homeassistant/components/abode/translations/hu.json index 77ce53abef..5df508d0f3 100644 --- a/homeassistant/components/abode/translations/hu.json +++ b/homeassistant/components/abode/translations/hu.json @@ -4,9 +4,15 @@ "single_instance_allowed": "Csak egyetlen Abode konfigur\u00e1ci\u00f3 enged\u00e9lyezett." }, "error": { - "cannot_connect": "Sikertelen csatlakoz\u00e1s" + "cannot_connect": "Sikertelen csatlakoz\u00e1s", + "invalid_mfa_code": "\u00c9rv\u00e9nytelen MFA k\u00f3d" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA k\u00f3d (6 jegy\u0171)" + } + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/abode/translations/nl.json b/homeassistant/components/abode/translations/nl.json index 9ef9c74aa1..9177b1deb7 100644 --- a/homeassistant/components/abode/translations/nl.json +++ b/homeassistant/components/abode/translations/nl.json @@ -5,9 +5,18 @@ }, "error": { "cannot_connect": "Kan geen verbinding maken", - "invalid_auth": "Ongeldige authenticatie" + "invalid_auth": "Ongeldige authenticatie", + "invalid_mfa_code": "Ongeldige MFA-code" }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA-code (6-cijfers)" + } + }, + "reauth_confirm": { + "title": "Vul uw Abode-inloggegevens in" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/abode/translations/no.json b/homeassistant/components/abode/translations/no.json index c215ec7dae..27706c3d79 100644 --- a/homeassistant/components/abode/translations/no.json +++ b/homeassistant/components/abode/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "Reautentisering var vellykket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/abode/translations/pt.json b/homeassistant/components/abode/translations/pt.json index 0df67a9418..95a5174122 100644 --- a/homeassistant/components/abode/translations/pt.json +++ b/homeassistant/components/abode/translations/pt.json @@ -1,6 +1,20 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { + "reauth_confirm": { + "data": { + "password": "Palavra-passe", + "username": "Email" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/abode/translations/sl.json b/homeassistant/components/abode/translations/sl.json index aa54e582af..3f6a142e28 100644 --- a/homeassistant/components/abode/translations/sl.json +++ b/homeassistant/components/abode/translations/sl.json @@ -1,9 +1,26 @@ { "config": { "abort": { + "reauth_successful": "Ponovno overjanje je uspelo", "single_instance_allowed": "Dovoljena je samo ena konfiguracija Abode." }, + "error": { + "invalid_mfa_code": "Napa\u010dna MFA koda" + }, "step": { + "mfa": { + "data": { + "mfa_code": "MFA koda (6 \u0161tevilk)" + }, + "title": "Vnesite MFA kodo za Abode" + }, + "reauth_confirm": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Vnesite podatke za prijavo v Abode" + }, "user": { "data": { "password": "Geslo", diff --git a/homeassistant/components/abode/translations/th.json b/homeassistant/components/abode/translations/th.json new file mode 100644 index 0000000000..2b9eefdbb6 --- /dev/null +++ b/homeassistant/components/abode/translations/th.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "mfa": { + "title": "\u0e1b\u0e49\u0e2d\u0e19\u0e23\u0e2b\u0e31\u0e2a MFA \u0e08\u0e32\u0e01 Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/translations/zh-Hant.json b/homeassistant/components/abode/translations/zh-Hant.json index d3e1db007f..6725df4445 100644 --- a/homeassistant/components/abode/translations/zh-Hant.json +++ b/homeassistant/components/abode/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index fa9ed6b467..cbccc3a462 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -183,6 +183,20 @@ FORECAST_SENSOR_TYPES = { ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, }, + "WindDay": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Day", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, + "WindNight": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind Night", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, } OPTIONAL_SENSORS = ( @@ -284,6 +298,13 @@ SENSOR_TYPES = { ATTR_UNIT_METRIC: TEMP_CELSIUS, ATTR_UNIT_IMPERIAL: TEMP_FAHRENHEIT, }, + "Wind": { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:weather-windy", + ATTR_LABEL: "Wind", + ATTR_UNIT_METRIC: SPEED_KILOMETERS_PER_HOUR, + ATTR_UNIT_IMPERIAL: SPEED_MILES_PER_HOUR, + }, "WindGust": { ATTR_DEVICE_CLASS: None, ATTR_ICON: "mdi:weather-windy", diff --git a/homeassistant/components/accuweather/sensor.py b/homeassistant/components/accuweather/sensor.py index 4f61322b2c..90058e254d 100644 --- a/homeassistant/components/accuweather/sensor.py +++ b/homeassistant/components/accuweather/sensor.py @@ -96,7 +96,7 @@ class AccuWeatherSensor(CoordinatorEntity): return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ self.kind ]["Value"] - if self.kind in ["WindGustDay", "WindGustNight"]: + if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: return self.coordinator.data[ATTR_FORECAST][self.forecast_day][ self.kind ]["Speed"]["Value"] @@ -115,7 +115,7 @@ class AccuWeatherSensor(CoordinatorEntity): return self.coordinator.data["PrecipitationSummary"][self.kind][ self._unit_system ]["Value"] - if self.kind == "WindGust": + if self.kind in ["Wind", "WindGust"]: return self.coordinator.data[self.kind]["Speed"][self._unit_system]["Value"] return self.coordinator.data[self.kind] @@ -144,7 +144,7 @@ class AccuWeatherSensor(CoordinatorEntity): def device_state_attributes(self): """Return the state attributes.""" if self.forecast_day is not None: - if self.kind in ["WindGustDay", "WindGustNight"]: + if self.kind in ["WindDay", "WindNight", "WindGustDay", "WindGustNight"]: self._attrs["direction"] = self.coordinator.data[ATTR_FORECAST][ self.forecast_day ][self.kind]["Direction"]["English"] diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json index e1b95b37e0..9c33637baa 100644 --- a/homeassistant/components/accuweather/translations/ca.json +++ b/homeassistant/components/accuweather/translations/ca.json @@ -31,5 +31,11 @@ "title": "Opcions d'AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor d'Accuweather accessible", + "remaining_requests": "Sol\u00b7licituds permeses restants" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/cs.json b/homeassistant/components/accuweather/translations/cs.json index d7d7d6e9b3..ea954b9f0d 100644 --- a/homeassistant/components/accuweather/translations/cs.json +++ b/homeassistant/components/accuweather/translations/cs.json @@ -31,5 +31,11 @@ "title": "Mo\u017enosti AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Lze kontaktovat AccuWeather server", + "remaining_requests": "Zb\u00fdvaj\u00edc\u00ed povolen\u00e9 \u017e\u00e1dosti" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index 9291e17e86..fe0319764a 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { @@ -10,5 +13,20 @@ "title": "AccuWeather" } } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Wettervorhersage" + } + } + } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather Server erreichen", + "remaining_requests": "Verbleibende erlaubte Anfragen" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/es.json b/homeassistant/components/accuweather/translations/es.json index 5d4522e8ce..aa24b5ff97 100644 --- a/homeassistant/components/accuweather/translations/es.json +++ b/homeassistant/components/accuweather/translations/es.json @@ -31,5 +31,11 @@ "title": "Opciones de AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcanzar el servidor AccuWeather", + "remaining_requests": "Solicitudes permitidas restantes" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index ebbceb69b0..bed28b6297 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -34,7 +34,7 @@ }, "system_health": { "info": { - "can_reach_server": "\u00dchendu Accuweatheri serveriga", + "can_reach_server": "\u00dchendus Accuweatheri serveriga", "remaining_requests": "Lubatud taotlusi on j\u00e4\u00e4nud" } } diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 40cf1ccc0b..8e63820541 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -31,5 +31,10 @@ "title": "Options AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Acc\u00e8s au serveur AccuWeather" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/it.json b/homeassistant/components/accuweather/translations/it.json index 1c22344f55..86aaa213a1 100644 --- a/homeassistant/components/accuweather/translations/it.json +++ b/homeassistant/components/accuweather/translations/it.json @@ -31,5 +31,11 @@ "title": "Opzioni AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Raggiungi il server AccuWeather", + "remaining_requests": "Richieste consentite rimanenti" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/lb.json b/homeassistant/components/accuweather/translations/lb.json index e1a9306e00..7f3855a7b9 100644 --- a/homeassistant/components/accuweather/translations/lb.json +++ b/homeassistant/components/accuweather/translations/lb.json @@ -31,5 +31,11 @@ "title": "AccuWeather Optiounen" } } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather Server ereechbar", + "remaining_requests": "Rescht vun erlaabten Ufroen" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json index 78a0d22878..50482cb3e6 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -31,5 +31,11 @@ "title": "AccuWeather-alternativer" } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 AccuWeather-serveren", + "remaining_requests": "Gjenv\u00e6rende tillatte foresp\u00f8rsler" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index 2ac9aabfc9..c6e4fb3ba8 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -31,5 +31,11 @@ "title": "Opcje AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera AccuWeather", + "remaining_requests": "Pozosta\u0142o dozwolonych \u017c\u0105da\u0144" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/pt.json b/homeassistant/components/accuweather/translations/pt.json index 084965331e..14260bd572 100644 --- a/homeassistant/components/accuweather/translations/pt.json +++ b/homeassistant/components/accuweather/translations/pt.json @@ -10,9 +10,10 @@ "step": { "user": { "data": { - "api_key": "Chave de API", + "api_key": "API Key", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "name": "Nome" } } } diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index 16623e4e70..6a675c1724 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -31,5 +31,11 @@ "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 AccuWeather", + "remaining_requests": "\u0421\u0447\u0451\u0442\u0447\u0438\u043a \u043e\u0441\u0442\u0430\u0432\u0448\u0438\u0445\u0441\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u0432" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sensor.de.json b/homeassistant/components/accuweather/translations/sensor.de.json new file mode 100644 index 0000000000..7ccc7c7360 --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.de.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "Fallend", + "rising": "Steigend", + "steady": "Gleichbleibend" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/sl.json b/homeassistant/components/accuweather/translations/sl.json new file mode 100644 index 0000000000..f41ee93aef --- /dev/null +++ b/homeassistant/components/accuweather/translations/sl.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "can_reach_server": "Dostop do AccuWeather stre\u017enika", + "remaining_requests": "Preostalo dovoljenih zahtevkov" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/zh-Hans.json b/homeassistant/components/accuweather/translations/zh-Hans.json new file mode 100644 index 0000000000..f8879f5715 --- /dev/null +++ b/homeassistant/components/accuweather/translations/zh-Hans.json @@ -0,0 +1,8 @@ +{ + "system_health": { + "info": { + "can_reach_server": "\u53ef\u8bbf\u95ee AccuWeather \u670d\u52a1\u5668", + "remaining_requests": "\u5176\u4f59\u5141\u8bb8\u7684\u8bf7\u6c42" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index db6c097e1e..ed5fa26f0c 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -31,5 +31,11 @@ "title": "AccuWeather \u9078\u9805" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u9023\u7dda AccuWeather \u4f3a\u670d\u5668", + "remaining_requests": "\u5176\u9918\u5141\u8a31\u7684\u8acb\u6c42" + } } } \ No newline at end of file diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 861e483adb..096d2c6e24 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -2,6 +2,6 @@ "domain": "acer_projector", "name": "Acer Projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector", - "requirements": ["pyserial==3.4"], + "requirements": ["pyserial==3.5"], "codeowners": [] } diff --git a/homeassistant/components/acmeda/translations/pt.json b/homeassistant/components/acmeda/translations/pt.json new file mode 100644 index 0000000000..8fcd9c1342 --- /dev/null +++ b/homeassistant/components/acmeda/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/zh-Hant.json b/homeassistant/components/acmeda/translations/zh-Hant.json index 1e7d4d0f14..2aeb94f66d 100644 --- a/homeassistant/components/acmeda/translations/zh-Hant.json +++ b/homeassistant/components/acmeda/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "step": { "user": { diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index 78db52ade4..a02601759b 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -4,6 +4,9 @@ "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig." }, + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "hassio_confirm": { "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass eine Verbindung mit AdGuard Home als Hass.io-Add-On hergestellt wird: {addon}?", diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index 6f56d996b6..5d8abfc9f5 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "hassio_confirm": { "title": "AdGuard Home via Hass.io add-on" @@ -9,7 +15,9 @@ "host": "Servidor", "password": "Palavra-passe", "port": "Porta", - "username": "Nome de Utilizador" + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar certificado SSL" } } } diff --git a/homeassistant/components/adguard/translations/zh-Hant.json b/homeassistant/components/adguard/translations/zh-Hant.json index b5c6863a94..8306b2daf7 100644 --- a/homeassistant/components/adguard/translations/zh-Hant.json +++ b/homeassistant/components/adguard/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/advantage_air/translations/de.json b/homeassistant/components/advantage_air/translations/de.json index e2a9646a0a..0d8a005240 100644 --- a/homeassistant/components/advantage_air/translations/de.json +++ b/homeassistant/components/advantage_air/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/advantage_air/translations/pt.json b/homeassistant/components/advantage_air/translations/pt.json new file mode 100644 index 0000000000..37e27fd839 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP", + "port": "Porta" + }, + "title": "Ligar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/zh-Hant.json b/homeassistant/components/advantage_air/translations/zh-Hant.json index eae6626685..9d1cd4210f 100644 --- a/homeassistant/components/advantage_air/translations/zh-Hant.json +++ b/homeassistant/components/advantage_air/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/agent_dvr/translations/de.json b/homeassistant/components/agent_dvr/translations/de.json index d4f8fc4bcc..6ea40d0fd0 100644 --- a/homeassistant/components/agent_dvr/translations/de.json +++ b/homeassistant/components/agent_dvr/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt." + "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "cannot_connect": "Verbindungsfehler" }, "step": { "user": { diff --git a/homeassistant/components/agent_dvr/translations/pt.json b/homeassistant/components/agent_dvr/translations/pt.json index ce7cbc3f54..f1ef5ef665 100644 --- a/homeassistant/components/agent_dvr/translations/pt.json +++ b/homeassistant/components/agent_dvr/translations/pt.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/agent_dvr/translations/zh-Hant.json b/homeassistant/components/agent_dvr/translations/zh-Hant.json index 16de4fd103..aa0ac965a8 100644 --- a/homeassistant/components/agent_dvr/translations/zh-Hant.json +++ b/homeassistant/components/agent_dvr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 8b3b1949ec..58d6a4295e 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -5,15 +5,17 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.const import ( + CONF_API_KEY, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + HTTP_UNAUTHORIZED, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import ( # pylint:disable=unused-import - DEFAULT_NAME, - DOMAIN, - NO_AIRLY_SENSORS, -) +from .const import DOMAIN, NO_AIRLY_SENSORS # pylint:disable=unused-import class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -22,13 +24,9 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Initialize.""" - self._errors = {} - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - self._errors = {} + errors = {} websession = async_get_clientsession(self.hass) @@ -37,75 +35,57 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): f"{user_input[CONF_LATITUDE]}-{user_input[CONF_LONGITUDE]}" ) self._abort_if_unique_id_configured() - api_key_valid = await self._test_api_key(websession, user_input["api_key"]) - if not api_key_valid: - self._errors["base"] = "invalid_api_key" - else: - location_valid = await self._test_location( + try: + location_valid = await test_location( websession, user_input["api_key"], user_input["latitude"], user_input["longitude"], ) + except AirlyError as err: + if err.status_code == HTTP_UNAUTHORIZED: + errors["base"] = "invalid_api_key" + else: if not location_valid: - self._errors["base"] = "wrong_location" + errors["base"] = "wrong_location" - if not self._errors: - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) - return self._show_config_form( - name=DEFAULT_NAME, - api_key="", - latitude=self.hass.config.latitude, - longitude=self.hass.config.longitude, - ) - - def _show_config_form(self, name=None, api_key=None, latitude=None, longitude=None): - """Show the configuration form to edit data.""" return self.async_show_form( step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_API_KEY, default=api_key): str, + vol.Required(CONF_API_KEY): str, vol.Optional( CONF_LATITUDE, default=self.hass.config.latitude ): cv.latitude, vol.Optional( CONF_LONGITUDE, default=self.hass.config.longitude ): cv.longitude, - vol.Optional(CONF_NAME, default=name): str, + vol.Optional( + CONF_NAME, default=self.hass.config.location_name + ): str, } ), - errors=self._errors, + errors=errors, ) - async def _test_api_key(self, client, api_key): - """Return true if api_key is valid.""" - with async_timeout.timeout(10): - airly = Airly(api_key, client) - measurements = airly.create_measurements_session_point( - latitude=52.24131, longitude=20.99101 - ) - try: - await measurements.update() - except AirlyError: - return False - return True +async def test_location(client, api_key, latitude, longitude): + """Return true if location is valid.""" + airly = Airly(api_key, client) + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) - async def _test_location(self, client, api_key, latitude, longitude): - """Return true if location is valid.""" + with async_timeout.timeout(10): + await measurements.update() - with async_timeout.timeout(10): - airly = Airly(api_key, client) - measurements = airly.create_measurements_session_point( - latitude=latitude, longitude=longitude - ) + current = measurements.current - await measurements.update() - current = measurements.current - if current["indexes"][0]["description"] == NO_AIRLY_SENSORS: - return False - return True + if current["indexes"][0]["description"] == NO_AIRLY_SENSORS: + return False + return True diff --git a/homeassistant/components/airly/translations/ca.json b/homeassistant/components/airly/translations/ca.json index 2aba1db84c..95400de23b 100644 --- a/homeassistant/components/airly/translations/ca.json +++ b/homeassistant/components/airly/translations/ca.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor d'Airly accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/cs.json b/homeassistant/components/airly/translations/cs.json index 86d678d31a..8b35399bcb 100644 --- a/homeassistant/components/airly/translations/cs.json +++ b/homeassistant/components/airly/translations/cs.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Lze kontaktovat Airly server" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index 19768cad7d..743a68a010 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -18,5 +18,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Airly Server erreichen" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/es.json b/homeassistant/components/airly/translations/es.json index 4fb6a0905c..a0ed36a716 100644 --- a/homeassistant/components/airly/translations/es.json +++ b/homeassistant/components/airly/translations/es.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcanzar el servidor Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/et.json b/homeassistant/components/airly/translations/et.json index 0d46a0f764..8cbfd13825 100644 --- a/homeassistant/components/airly/translations/et.json +++ b/homeassistant/components/airly/translations/et.json @@ -22,7 +22,7 @@ }, "system_health": { "info": { - "can_reach_server": "\u00dchendu Airly serveriga" + "can_reach_server": "\u00dchendus Airly serveriga" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/fr.json b/homeassistant/components/airly/translations/fr.json index 5ac31e130f..98407155f1 100644 --- a/homeassistant/components/airly/translations/fr.json +++ b/homeassistant/components/airly/translations/fr.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Acc\u00e8s au serveur Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/it.json b/homeassistant/components/airly/translations/it.json index 6f3fd919df..bf6d7a461c 100644 --- a/homeassistant/components/airly/translations/it.json +++ b/homeassistant/components/airly/translations/it.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Raggiungi il server Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/lb.json b/homeassistant/components/airly/translations/lb.json index 46eb3a91f0..dd24ee3066 100644 --- a/homeassistant/components/airly/translations/lb.json +++ b/homeassistant/components/airly/translations/lb.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Airly Server ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/no.json b/homeassistant/components/airly/translations/no.json index f0a657d33d..b38568210a 100644 --- a/homeassistant/components/airly/translations/no.json +++ b/homeassistant/components/airly/translations/no.json @@ -19,5 +19,10 @@ "title": "" } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 Airly-serveren" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/pl.json b/homeassistant/components/airly/translations/pl.json index f13c212e25..e36e6f86ec 100644 --- a/homeassistant/components/airly/translations/pl.json +++ b/homeassistant/components/airly/translations/pl.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/pt.json b/homeassistant/components/airly/translations/pt.json index ae35beabf6..6ebb22b565 100644 --- a/homeassistant/components/airly/translations/pt.json +++ b/homeassistant/components/airly/translations/pt.json @@ -1,11 +1,18 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { "user": { "data": { "api_key": "", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "name": "Nome" }, "title": "" } diff --git a/homeassistant/components/airly/translations/ru.json b/homeassistant/components/airly/translations/ru.json index a047d1e747..b1469af787 100644 --- a/homeassistant/components/airly/translations/ru.json +++ b/homeassistant/components/airly/translations/ru.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 Airly" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/sl.json b/homeassistant/components/airly/translations/sl.json index 71bea5a4d8..e1c8950139 100644 --- a/homeassistant/components/airly/translations/sl.json +++ b/homeassistant/components/airly/translations/sl.json @@ -18,5 +18,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dostop do Airly stre\u017enika" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json new file mode 100644 index 0000000000..1b6e9caa24 --- /dev/null +++ b/homeassistant/components/airly/translations/tr.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "can_reach_server": "Airly sunucusuna eri\u015fin" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/translations/zh-Hans.json b/homeassistant/components/airly/translations/zh-Hans.json index f5b95a57f0..1a57bfbadf 100644 --- a/homeassistant/components/airly/translations/zh-Hans.json +++ b/homeassistant/components/airly/translations/zh-Hans.json @@ -10,5 +10,10 @@ } } } + }, + "system_health": { + "info": { + "can_reach_server": "\u53ef\u8bbf\u95ee Airly \u670d\u52a1\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/zh-Hant.json b/homeassistant/components/airly/translations/zh-Hant.json index e8deb533de..4d60b158c4 100644 --- a/homeassistant/components/airly/translations/zh-Hant.json +++ b/homeassistant/components/airly/translations/zh-Hant.json @@ -19,5 +19,10 @@ "title": "Airly" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u9023\u7dda Airly \u4f3a\u670d\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 7c3467ca55..63012e23da 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert." }, "error": { + "cannot_connect": "Verbindungsfehler", "general_error": "Es gab einen unbekannten Fehler.", "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel bereitgestellt." }, diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index 138b84f6fd..abf4a9f62e 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Plasseringen er allerede konfigurert eller Node / Pro ID er allerede registrert.", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -31,7 +31,7 @@ "data": { "api_key": "API-n\u00f8kkel" }, - "title": "Autentiser AirVisual p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json index f7830dbe18..d6732cdddc 100644 --- a/homeassistant/components/airvisual/translations/pt.json +++ b/homeassistant/components/airvisual/translations/pt.json @@ -1,10 +1,32 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada ou Node/Pro ID j\u00e1 est\u00e1 registrado.", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "general_error": "Erro inesperado", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { + "geography": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + } + }, "node_pro": { "data": { + "ip_address": "Servidor", "password": "Palavra-passe" } + }, + "reauth_confirm": { + "data": { + "api_key": "" + } } } } diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index 913173f98c..4bdc295904 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -24,7 +24,7 @@ "ip_address": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, - "description": "\u76e3\u63a7\u500b\u4eba AirVisual \u8a2d\u5099\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u8a2d\u5099 UI \u7372\u5f97\u3002", + "description": "\u76e3\u63a7\u500b\u4eba AirVisual \u88dd\u7f6e\uff0c\u5bc6\u78bc\u53ef\u4ee5\u900f\u904e\u88dd\u7f6e UI \u7372\u5f97\u3002", "title": "\u8a2d\u5b9a AirVisual Node/Pro" }, "reauth_confirm": { diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index c00ee65c27..3f1b7ef816 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "protocol": { "data": { diff --git a/homeassistant/components/alarmdecoder/translations/et.json b/homeassistant/components/alarmdecoder/translations/et.json index 12395b1d07..d09fb725e3 100644 --- a/homeassistant/components/alarmdecoder/translations/et.json +++ b/homeassistant/components/alarmdecoder/translations/et.json @@ -59,14 +59,14 @@ "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.", + "description": "Sisesta ala {zone_number} \u00fcksikasjad. Ala {zone_number} kustutamiseks j\u00e4ta 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.", + "description": "Sisesta ala number mida soovid lisada, muuta v\u00f5i eemaldada.", "title": "Seadista AlarmDecoder" } } diff --git a/homeassistant/components/alarmdecoder/translations/pt.json b/homeassistant/components/alarmdecoder/translations/pt.json new file mode 100644 index 0000000000..8d6cb9a2eb --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/pt.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "protocol": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "zone_details": { + "data": { + "zone_name": "Nome da Zona", + "zone_type": "Tipo de Zona" + } + }, + "zone_select": { + "data": { + "zone_number": "N\u00famero da Zona" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/zh-Hant.json b/homeassistant/components/alarmdecoder/translations/zh-Hant.json index ee630bc7a1..a43e80d362 100644 --- a/homeassistant/components/alarmdecoder/translations/zh-Hant.json +++ b/homeassistant/components/alarmdecoder/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "create_entry": { "default": "\u6210\u529f\u9023\u7dda\u81f3 AlarmDecoder\u3002" @@ -12,8 +12,8 @@ "step": { "protocol": { "data": { - "device_baudrate": "\u8a2d\u5099\u901a\u8a0a\u7387", - "device_path": "\u8a2d\u5099\u8def\u5f91", + "device_baudrate": "\u88dd\u7f6e\u901a\u8a0a\u7387", + "device_path": "\u88dd\u7f6e\u8def\u5f91", "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index 7d3a3994ac..cc5c604dc8 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -45,6 +45,11 @@ class AbstractConfig(ABC): """Return if proactive mode is enabled.""" return self._unsub_proactive_report is not None + @callback + @abstractmethod + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + async def async_enable_proactive_mode(self): """Enable proactive mode.""" if self._unsub_proactive_report is None: diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 574ba6b8ba..c05d9641b9 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -329,7 +329,7 @@ class AlexaEntity: "manufacturer": "Home Assistant", "model": self.entity.domain, "softwareVersion": __version__, - "customIdentifier": self.entity_id, + "customIdentifier": f"{self.config.user_identifier()}-{self.entity_id}", }, } diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index 41ebfb340e..41738c824f 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -53,6 +53,11 @@ class AlexaConfig(AbstractConfig): """Return config locale.""" return self._config.get(CONF_LOCALE) + @core.callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "" + def should_expose(self, entity_id): """If an entity should be exposed.""" return self._config[CONF_FILTER](entity_id) diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 0360619394..1b0f03b801 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "Tilkobling mislyktes", - "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 hjelpseksjonen]({docs_url})", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/almond/translations/pt.json b/homeassistant/components/almond/translations/pt.json index 94dfbefb86..44f4923964 100644 --- a/homeassistant/components/almond/translations/pt.json +++ b/homeassistant/components/almond/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "step": { "pick_implementation": { "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" diff --git a/homeassistant/components/almond/translations/zh-Hant.json b/homeassistant/components/almond/translations/zh-Hant.json index a576b11e63..6312d4ecd1 100644 --- a/homeassistant/components/almond/translations/zh-Hant.json +++ b/homeassistant/components/almond/translations/zh-Hant.json @@ -4,7 +4,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "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" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 3492518421..fb9560832c 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -2,6 +2,7 @@ import logging import boto3 +import botocore import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider @@ -41,6 +42,7 @@ CONF_SAMPLE_RATE = "sample_rate" CONF_TEXT_TYPE = "text_type" SUPPORTED_VOICES = [ + "Olivia", # Female, Australian, Neural "Zhiyu", # Chinese "Mads", "Naja", # Danish @@ -125,6 +127,10 @@ DEFAULT_TEXT_TYPE = "text" DEFAULT_SAMPLE_RATES = {"mp3": "22050", "ogg_vorbis": "22050", "pcm": "16000"} +AWS_CONF_CONNECT_TIMEOUT = 10 +AWS_CONF_READ_TIMEOUT = 5 +AWS_CONF_MAX_POOL_CONNECTIONS = 1 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_REGION, default=DEFAULT_REGION): vol.In(SUPPORTED_REGIONS), @@ -167,6 +173,11 @@ def get_engine(hass, config, discovery_info=None): CONF_REGION: config[CONF_REGION], CONF_ACCESS_KEY_ID: config.get(CONF_ACCESS_KEY_ID), CONF_SECRET_ACCESS_KEY: config.get(CONF_SECRET_ACCESS_KEY), + "config": botocore.config.Config( + connect_timeout=AWS_CONF_CONNECT_TIMEOUT, + read_timeout=AWS_CONF_READ_TIMEOUT, + max_pool_connections=AWS_CONF_MAX_POOL_CONNECTIONS, + ), } del config[CONF_REGION] @@ -229,6 +240,7 @@ class AmazonPollyProvider(Provider): _LOGGER.error("%s does not support the %s language", voice_id, language) return None, None + _LOGGER.debug("Requesting TTS file for text: %s", message) resp = self.client.synthesize_speech( Engine=self.config[CONF_ENGINE], OutputFormat=self.config[CONF_OUTPUT_FORMAT], @@ -238,6 +250,7 @@ class AmazonPollyProvider(Provider): VoiceId=voice_id, ) + _LOGGER.debug("Reply received for TTS: %s", message) return ( CONTENT_TYPE_EXTENSIONS[resp.get("ContentType")], resp.get("AudioStream").read(), diff --git a/homeassistant/components/ambiclimate/translations/no.json b/homeassistant/components/ambiclimate/translations/no.json index 88a4a0bdbb..c39aa7637f 100644 --- a/homeassistant/components/ambiclimate/translations/no.json +++ b/homeassistant/components/ambiclimate/translations/no.json @@ -3,7 +3,7 @@ "abort": { "access_token": "Ukjent feil ved oppretting av tilgangstoken.", "already_configured": "Kontoen er allerede konfigurert", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" }, "create_entry": { "default": "Vellykket godkjenning" diff --git a/homeassistant/components/ambiclimate/translations/pt.json b/homeassistant/components/ambiclimate/translations/pt.json new file mode 100644 index 0000000000..591d8c2fea --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/pt.json b/homeassistant/components/ambient_station/translations/pt.json index 56c8b5f718..c67faa25f0 100644 --- a/homeassistant/components/ambient_station/translations/pt.json +++ b/homeassistant/components/ambient_station/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { "invalid_key": "Chave de API e/ou chave de aplica\u00e7\u00e3o inv\u00e1lidas", "no_devices": "Nenhum dispositivo encontrado na conta" diff --git a/homeassistant/components/ambient_station/translations/zh-Hant.json b/homeassistant/components/ambient_station/translations/zh-Hant.json index 51f0033b95..dab15def7b 100644 --- a/homeassistant/components/ambient_station/translations/zh-Hant.json +++ b/homeassistant/components/ambient_station/translations/zh-Hant.json @@ -5,7 +5,7 @@ }, "error": { "invalid_key": "API \u5bc6\u9470\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" }, "step": { "user": { diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 6adea1af5a..ffcaedeb5a 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ "adb-shell[async]==0.2.1", - "androidtv[async]==0.0.56", + "androidtv[async]==0.0.57", "pure-python-adb[async]==0.3.0.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 14170fdd8c..eca5e91dde 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -135,15 +136,6 @@ class AppleTVEntity(Entity): def async_device_disconnected(self): """Handle when connection was lost to device.""" - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._identifier)}, - "manufacturer": "Apple", - "name": self.name, - } - @property def name(self): """Return the name of the device.""" @@ -337,6 +329,8 @@ class AppleTVManager: self._dispatch_send(SIGNAL_CONNECTED, self.atv) self._address_updated(str(conf.address)) + await self._async_setup_device_registry() + self._connection_attempts = 0 if self._connection_was_lost: _LOGGER.info( @@ -344,6 +338,27 @@ class AppleTVManager: ) self._connection_was_lost = False + async def _async_setup_device_registry(self): + attrs = { + "identifiers": {(DOMAIN, self.config_entry.unique_id)}, + "manufacturer": "Apple", + "name": self.config_entry.data[CONF_NAME], + } + + if self.atv: + dev_info = self.atv.device_info + + attrs["model"] = "Apple TV " + dev_info.model.name.replace("Gen", "") + attrs["sw_version"] = dev_info.version + + if dev_info.mac: + attrs["connections"] = {(dr.CONNECTION_NETWORK_MAC, dev_info.mac)} + + device_registry = await dr.async_get_registry(self.hass) + device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, **attrs + ) + @property def is_connecting(self): """Return true if connection is in progress.""" diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index b7486af50e..81bb79dc50 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,7 +1,7 @@ """Support for Apple TV media player.""" import logging -from pyatv.const import DeviceState, MediaType +from pyatv.const import DeviceState, FeatureName, FeatureState, MediaType from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player.const import ( @@ -107,6 +107,22 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): self._playing = None self.async_write_ha_state() + @property + def app_id(self): + """ID of the current running app.""" + if self.atv: + if self.atv.features.in_state(FeatureState.Available, FeatureName.App): + return self.atv.metadata.app.identifier + return None + + @property + def app_name(self): + """Name of the current running app.""" + if self.atv: + if self.atv.features.in_state(FeatureState.Available, FeatureName.App): + return self.atv.metadata.app.name + return None + @property def media_content_type(self): """Content type of current playing media.""" @@ -168,11 +184,31 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity): return self._playing.title return None + @property + def media_artist(self): + """Artist of current playing media, music track only.""" + if self._is_feature_available(FeatureName.Artist): + return self._playing.artist + return None + + @property + def media_album_name(self): + """Album name of current playing media, music track only.""" + if self._is_feature_available(FeatureName.Album): + return self._playing.album + return None + @property def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_APPLE_TV + def _is_feature_available(self, feature): + """Return if a feature is available.""" + if self.atv and self._playing: + return self.atv.features.in_state(FeatureState.Available, feature) + return False + async def async_turn_on(self): """Turn the media player on.""" await self.manager.connect() diff --git a/homeassistant/components/apple_tv/translations/ca.json b/homeassistant/components/apple_tv/translations/ca.json new file mode 100644 index 0000000000..e9cd136720 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/ca.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "backoff": "En aquests moments el dispositiu no accepta sol\u00b7licituds de vinculaci\u00f3 (\u00e9s possible que hagis introdu\u00eft un codi PIN inv\u00e0lid massa vegades), torna-ho a provar m\u00e9s tard.", + "device_did_not_pair": "No s'ha fet cap intent d'acabar el proc\u00e9s de vinculaci\u00f3 des del dispositiu.", + "invalid_config": "La configuraci\u00f3 d'aquest dispositiu no est\u00e0 completa. Intenta'l tornar a afegir.", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "unknown": "Error inesperat" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "no_usable_service": "S'ha trobat un dispositiu per\u00f2 no ha pogut identificar cap manera d'establir-hi una connexi\u00f3. Si continues veient aquest missatge, prova d'especificar-ne l'adre\u00e7a IP o reinicia l'Apple TV.", + "unknown": "Error inesperat" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Est\u00e0s a punt d'afegir l'Apple TV amb nom \"{name}\" a Home Assistant.\n\n **Per completar el proc\u00e9s, \u00e9s possible que hagis d'introduir alguns codis PIN.** \n\n Tingues en compte que *no* pots apagar la teva Apple TV a trav\u00e9s d'aquesta integraci\u00f3. Nom\u00e9s es desactivar\u00e0 el reproductor de Home Assistant.", + "title": "Confirma l'addici\u00f3 de l'Apple TV" + }, + "pair_no_pin": { + "description": "Vinculaci\u00f3 necess\u00e0ria amb el servei `{protocol}`. Per continuar, introdueix el PIN {pin} a la teva Apple TV.", + "title": "Vinculaci\u00f3" + }, + "pair_with_pin": { + "data": { + "pin": "Codi PIN" + }, + "description": "Amb el protocol \"{protocol}\" \u00e9s necess\u00e0ria la vinculaci\u00f3. Introdueix el codi PIN que es mostra en pantalla. Els zeros a l'inici, si n'hi ha, s'han d'ometre; per exemple: introdueix 123 si el codi mostrat \u00e9s 0123.", + "title": "Vinculaci\u00f3" + }, + "reconfigure": { + "description": "Aquesta Apple TV est\u00e0 tenint problemes de connexi\u00f3 i s'ha de tornar a configurar.", + "title": "Reconfiguraci\u00f3 de dispositiu" + }, + "service_problem": { + "description": "S'ha produ\u00eft un problema en la vinculaci\u00f3 protocol \"{protocol}\". S'ignorar\u00e0.", + "title": "No s'ha pogut afegir el servei" + }, + "user": { + "data": { + "device_input": "Dispositiu" + }, + "description": "Comen\u00e7a introduint el nom del dispositiu (per exemple, cuina o dormitori) o l'adre\u00e7a IP de l'Apple TV que vulguis afegir. Si autom\u00e0ticament es troben dispositius a la teva xarxa, es mostra a continuaci\u00f3. \n\n Si no veus el teu dispositiu o tens problemes, prova d'especificar l'adre\u00e7a IP del dispositiu. \n\n {devices}", + "title": "Configuraci\u00f3 d'una nova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "No engeguis el dispositiu en iniciar Home Assistant" + }, + "description": "Configuraci\u00f3 dels par\u00e0metres generals del dispositiu" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/cs.json b/homeassistant/components/apple_tv/translations/cs.json new file mode 100644 index 0000000000..ef392a5a66 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/cs.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "already_configured_device": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "invalid_config": "Nastaven\u00ed tohoto za\u0159\u00edzen\u00ed je ne\u00fapln\u00e9. Zkuste jej p\u0159idat znovu.", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "title": "Potvrzen\u00ed p\u0159id\u00e1n\u00ed Apple TV" + }, + "pair_no_pin": { + "description": "Pro slu\u017ebu `{protocol}` je vy\u017eadov\u00e1no p\u00e1rov\u00e1n\u00ed. Pokra\u010dujte zad\u00e1n\u00edm k\u00f3du PIN {pin} na Apple TV.", + "title": "P\u00e1rov\u00e1n\u00ed" + }, + "pair_with_pin": { + "data": { + "pin": "PIN k\u00f3d" + }, + "description": "U protokolu `{protocol}` je vy\u017eadov\u00e1no p\u00e1rov\u00e1n\u00ed. Zadejte pros\u00edm PIN k\u00f3d zobrazen\u00fd na obrazovce. \u00davodn\u00ed nuly mus\u00ed b\u00fdt vynech\u00e1ny, tj. zadejte 123, pokud je zobrazen\u00fd k\u00f3d 0123.", + "title": "P\u00e1rov\u00e1n\u00ed" + }, + "reconfigure": { + "description": "U t\u00e9to Apple TV doch\u00e1z\u00ed k probl\u00e9m\u016fm s p\u0159ipojen\u00edm a je t\u0159eba ji znovu nastavit.", + "title": "Zm\u011bna konfigurace za\u0159\u00edzen\u00ed" + }, + "service_problem": { + "description": "P\u0159i p\u00e1rov\u00e1n\u00ed protokolu `{protocol}` do\u0161lo k probl\u00e9mu. Protokol bude ignorov\u00e1n.", + "title": "Nepoda\u0159ilo se p\u0159idat slu\u017ebu" + }, + "user": { + "data": { + "device_input": "Za\u0159\u00edzen\u00ed" + }, + "description": "Za\u010dn\u011bte zad\u00e1n\u00edm n\u00e1zvu za\u0159\u00edzen\u00ed (nap\u0159. Kuchyn\u011b nebo lo\u017enice) nebo IP adresy Apple TV, kterou chcete p\u0159idat. Pokud byla ve va\u0161\u00ed s\u00edti automaticky nalezena n\u011bkter\u00e1 za\u0159\u00edzen\u00ed, jsou uvedena n\u00ed\u017ee. \n\n Pokud nevid\u00edte sv\u00e9 za\u0159\u00edzen\u00ed nebo nastaly n\u011bjak\u00e9 probl\u00e9my, zkuste zadat IP adresu za\u0159\u00edzen\u00ed. \n\n {devices}", + "title": "Nastaven\u00ed nov\u00e9 Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Nezap\u00ednejte za\u0159\u00edzen\u00ed dokud se Home Assistant spou\u0161t\u00ed" + }, + "description": "Konfigurace obecn\u00fdch mo\u017enost\u00ed za\u0159\u00edzen\u00ed" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/de.json b/homeassistant/components/apple_tv/translations/de.json new file mode 100644 index 0000000000..464bad99d5 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/de.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "backoff": "Das Ger\u00e4t akzeptiert derzeit keine Kopplungsanfragen (M\u00f6glicherweise wurde zu oft ein ung\u00fcltiger PIN-Code eingegeben), versuche es sp\u00e4ter erneut.", + "device_did_not_pair": "Es wurde kein Versuch unternommen, den Kopplungsvorgang vom Ger\u00e4t aus abzuschlie\u00dfen.", + "invalid_config": "Die Konfiguration f\u00fcr dieses Ger\u00e4t ist unvollst\u00e4ndig. Bitte versuche, es erneut hinzuzuf\u00fcgen.", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "no_usable_service": "Es wurde ein Ger\u00e4t gefunden, aber es konnte keine M\u00f6glichkeit gefunden werden, eine Verbindung zu diesem Ger\u00e4t herzustellen. Wenn diese Meldung weiterhin erscheint, versuche, die IP-Adresse anzugeben oder den Apple TV neu zu starten.", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Es wird der Apple TV mit dem Namen \" {name} \" zu Home Assistant hinzugef\u00fcgt. \n\n ** Um den Vorgang abzuschlie\u00dfen, m\u00fcssen m\u00f6glicherweise mehrere PIN-Codes eingegeben werden. ** \n\n Bitte beachte, dass der Apple TV mit dieser Integration * nicht * ausgeschalten werden kann. Nur der Media Player in Home Assistant wird ausgeschaltet!", + "title": "Best\u00e4tige das Hinzuf\u00fcgen vom Apple TV" + }, + "pair_no_pin": { + "description": "F\u00fcr den Dienst `{protocol}` ist eine Kopplung erforderlich. Bitte gebe die PIN {pin} am Apple TV ein, um fortzufahren.", + "title": "Kopplung" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-Code" + }, + "description": "F\u00fcr das Protokoll `{protocol}` ist eine Kopplung erforderlich. Bitte gebe den auf dem Bildschirm angezeigten PIN-Code ein. F\u00fchrende Nullen m\u00fcssen weggelassen werden, d.h. gebe 123 ein, wenn der angezeigte Code 0123 lautet.", + "title": "Kopplung" + }, + "reconfigure": { + "description": "Dieser Apple TV hat Verbindungsprobleme und muss neu konfiguriert werden.", + "title": "Ger\u00e4teneukonfiguration" + }, + "service_problem": { + "description": "Beim Koppeln des Protokolls `{protocol}` ist ein Problem aufgetreten. Es wird ignoriert.", + "title": "Fehler beim Hinzuf\u00fcgen des Dienstes" + }, + "user": { + "data": { + "device_input": "Ger\u00e4t" + }, + "description": "Gebe zun\u00e4chst den Ger\u00e4tenamen (z. B. K\u00fcche oder Schlafzimmer) oder die IP-Adresse des Apple TV ein, der hinzugef\u00fcgt werden soll. Wenn Ger\u00e4te automatisch im Netzwerk gefunden wurden, werden sie unten angezeigt. \n\nWenn das Ger\u00e4t nicht sichtbar ist oder Probleme auftreten, gebe die IP-Adresse des Ger\u00e4ts an. \n\n{devices}", + "title": "Neuen Apple TV einrichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Schalte das Ger\u00e4t nicht ein, wenn Home Assistant startet" + }, + "description": "Konfiguriere die allgemeinen Ger\u00e4teeinstellungen" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/es.json b/homeassistant/components/apple_tv/translations/es.json new file mode 100644 index 0000000000..d03a77ca1c --- /dev/null +++ b/homeassistant/components/apple_tv/translations/es.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "backoff": "El dispositivo no acepta solicitudes de emparejamiento en este momento (es posible que hayas introducido un c\u00f3digo PIN no v\u00e1lido demasiadas veces), int\u00e9ntalo de nuevo m\u00e1s tarde.", + "device_did_not_pair": "No se ha intentado finalizar el proceso de emparejamiento desde el dispositivo.", + "invalid_config": "La configuraci\u00f3n para este dispositivo est\u00e1 incompleta. Intenta a\u00f1adirlo de nuevo.", + "no_devices_found": "No se encontraron dispositivos en la red", + "unknown": "Error inesperado" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "no_devices_found": "No se encontraron dispositivos en la red", + "no_usable_service": "Se encontr\u00f3 un dispositivo, pero no se pudo identificar ninguna manera de establecer una conexi\u00f3n con \u00e9l. Si sigues viendo este mensaje, intenta especificar su direcci\u00f3n IP o reiniciar el Apple TV.", + "unknown": "Error inesperado" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Est\u00e1s a punto de a\u00f1adir el Apple TV con nombre `{name}` a Home Assistant.\n\n**Para completar el proceso, puede que tengas que introducir varios c\u00f3digos PIN.**\n\nTen en cuenta que *no* podr\u00e1s apagar tu Apple TV con esta integraci\u00f3n. \u00a1S\u00f3lo se apagar\u00e1 el reproductor de medios de Home Assistant!", + "title": "Confirma la adici\u00f3n del Apple TV" + }, + "pair_no_pin": { + "description": "El emparejamiento es necesario para el servicio `{protocol}`. Introduce el PIN en tu Apple TV para continuar.", + "title": "Emparejamiento" + }, + "pair_with_pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "El emparejamiento es necesario para el protocolo `{protocol}`. Introduce el c\u00f3digo PIN que aparece en la pantalla. Los ceros iniciales deben ser omitidos, es decir, introduce 123 si el c\u00f3digo mostrado es 0123.", + "title": "Emparejamiento" + }, + "reconfigure": { + "description": "Este Apple TV est\u00e1 experimentando algunos problemas de conexi\u00f3n y debe ser reconfigurado.", + "title": "Reconfiguraci\u00f3n del dispositivo" + }, + "service_problem": { + "description": "Se ha producido un problema durante el protocolo de emparejamiento `{protocol}`. Ser\u00e1 ignorado.", + "title": "Error al a\u00f1adir el servicio" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Empieza introduciendo el nombre del dispositivo (eje. Cocina o Dormitorio) o la direcci\u00f3n IP del Apple TV que quieres a\u00f1adir. Si se han econtrado dispositivos en tu red, se mostrar\u00e1n a continuaci\u00f3n.\n\nSi no puedes ver el dispositivo o experimentas alg\u00fan problema, intente especificar la direcci\u00f3n IP del dispositivo.\n\n{devices}", + "title": "Configurar un nuevo Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "No encender el dispositivo al iniciar Home Assistant" + }, + "description": "Configurar los ajustes generales del dispositivo" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json new file mode 100644 index 0000000000..a55d37ed58 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -0,0 +1,51 @@ +{ + "config": { + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "no_devices_found": "Aucun appareil d\u00e9tect\u00e9 sur le r\u00e9seau", + "no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.", + "unknown": "Erreur innatendue" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Vous \u00eates sur le point d'ajouter l'Apple TV nomm\u00e9e \u00ab {name} \u00bb \u00e0 Home Assistant. \n\n **Pour terminer le processus, vous devrez peut-\u00eatre saisir plusieurs codes PIN.** \n\n Veuillez noter que vous ne pourrez *pas* \u00e9teindre votre Apple TV avec cette int\u00e9gration. Seul le lecteur multim\u00e9dia de Home Assistant s'\u00e9teint!", + "title": "Confirmer l'ajout d'Apple TV" + }, + "pair_no_pin": { + "description": "L'appairage est requis pour le service ` {protocol} `. Veuillez saisir le code PIN {pin} sur votre Apple TV pour continuer.", + "title": "Appairage" + }, + "pair_with_pin": { + "data": { + "pin": "Code PIN" + } + }, + "reconfigure": { + "title": "Reconfiguration de l'appareil" + }, + "service_problem": { + "description": "Un probl\u00e8me est survenu lors du couplage du protocole \u00ab {protocol} \u00bb. Il sera ignor\u00e9.", + "title": "\u00c9chec de l'ajout du service" + }, + "user": { + "data": { + "device_input": "Appareil" + }, + "description": "Commencez par entrer le nom de l'appareil (par exemple, Cuisine ou Chambre) ou l'adresse IP de l'Apple TV que vous souhaitez ajouter. Si des appareils ont \u00e9t\u00e9 d\u00e9tect\u00e9s automatiquement sur votre r\u00e9seau, ils sont affich\u00e9s ci-dessous. \n\n Si vous ne voyez pas votre appareil ou rencontrez des probl\u00e8mes, essayez de sp\u00e9cifier l'adresse IP de l'appareil. \n\n {devices}", + "title": "Configurer une nouvelle Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "N'allumez pas l'appareil lors du d\u00e9marrage de Home Assistant" + }, + "description": "Configurer les param\u00e8tres g\u00e9n\u00e9raux de l'appareil" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/hu.json b/homeassistant/components/apple_tv/translations/hu.json new file mode 100644 index 0000000000..26c02fabbb --- /dev/null +++ b/homeassistant/components/apple_tv/translations/hu.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nincs eszk\u00f6z a h\u00e1l\u00f3zaton", + "unknown": "V\u00e1ratlan hiba" + }, + "error": { + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van", + "invalid_auth": "Azonos\u00edt\u00e1s nem siker\u00fclt", + "no_devices_found": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a h\u00e1l\u00f3zaton", + "unknown": "V\u00e1ratlan hiba" + }, + "step": { + "confirm": { + "title": "Apple TV sikeresen hozz\u00e1adva" + }, + "pair_no_pin": { + "title": "P\u00e1ros\u00edt\u00e1s" + }, + "pair_with_pin": { + "data": { + "pin": "PIN K\u00f3d" + }, + "title": "P\u00e1ros\u00edt\u00e1s" + }, + "reconfigure": { + "title": "Eszk\u00f6z \u00fajrakonfigur\u00e1l\u00e1sa" + }, + "service_problem": { + "title": "Nem siker\u00fclt hozz\u00e1adni a szolg\u00e1ltat\u00e1st" + }, + "user": { + "data": { + "device_input": "Eszk\u00f6z" + }, + "title": "\u00daj Apple TV be\u00e1ll\u00edt\u00e1sa" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/it.json b/homeassistant/components/apple_tv/translations/it.json new file mode 100644 index 0000000000..7ed3306721 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/it.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "backoff": "Il dispositivo non accetta richieste di abbinamento in questo momento (potresti aver inserito un codice PIN non valido troppe volte), riprova pi\u00f9 tardi.", + "device_did_not_pair": "Nessun tentativo di completare il processo di abbinamento \u00e8 stato effettuato dal dispositivo.", + "invalid_config": "La configurazione per questo dispositivo \u00e8 incompleta. Prova ad aggiungerlo di nuovo.", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "unknown": "Errore imprevisto" + }, + "error": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "invalid_auth": "Autenticazione non valida", + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "no_usable_service": "\u00c8 stato trovato un dispositivo ma non \u00e8 stato possibile identificare alcun modo per stabilire una connessione ad esso. Se continui a vedere questo messaggio, prova a specificarne l'indirizzo IP o a riavviare l'Apple TV.", + "unknown": "Errore imprevisto" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Stai per aggiungere l'Apple TV denominata \"{name}\" a Home Assistant. \n\n **Per completare la procedura, potrebbe essere necessario inserire pi\u00f9 codici PIN.** \n\nTieni presente che *non* sarai in grado di spegnere la tua Apple TV con questa integrazione. Solo il lettore multimediale in Home Assistant si spegner\u00e0!", + "title": "Conferma l'aggiunta di Apple TV" + }, + "pair_no_pin": { + "description": "L'abbinamento \u00e8 richiesto per il servizio \"{protocol}\". Inserisci il PIN {pin} sulla tua Apple TV per continuare.", + "title": "Abbinamento" + }, + "pair_with_pin": { + "data": { + "pin": "Codice PIN" + }, + "description": "L'abbinamento \u00e8 richiesto per il protocollo \"{protocol}\". Immettere il codice PIN visualizzato sullo schermo. Gli zeri iniziali devono essere omessi, ovvero immettere 123 se il codice visualizzato \u00e8 0123.", + "title": "Abbinamento" + }, + "reconfigure": { + "description": "Questa Apple TV sta riscontrando alcune difficolt\u00e0 di connessione e deve essere riconfigurata.", + "title": "Riconfigurazione del dispositivo" + }, + "service_problem": { + "description": "Si \u00e8 verificato un problema durante l'associazione del protocollo \"{protocol}\". Sar\u00e0 ignorato.", + "title": "Impossibile aggiungere il servizio" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Inizia inserendo il nome del dispositivo (es. Cucina o Camera da letto) o l'indirizzo IP dell'Apple TV che desideri aggiungere. Se sono stati rilevati automaticamente dei dispositivi sulla rete, verranno visualizzati di seguito. \n\n Se non riesci a vedere il tuo dispositivo o riscontri problemi, prova a specificare l'indirizzo IP del dispositivo. \n\n {devices}", + "title": "Configura una nuova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Non accendere il dispositivo all'avvio di Home Assistant" + }, + "description": "Configurare le impostazioni generali del dispositivo" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/lb.json b/homeassistant/components/apple_tv/translations/lb.json new file mode 100644 index 0000000000..945f467c4c --- /dev/null +++ b/homeassistant/components/apple_tv/translations/lb.json @@ -0,0 +1,52 @@ +{ + "config": { + "abort": { + "already_configured_device": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", + "unknown": "Onerwaarte Feeler" + }, + "error": { + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Du bass um Punkt fir den Apple TV mam Numm \"{name}\" am Home Assistant dob\u00e4izesetzen.\n\n**Fir de Prozess ofzeschl\u00e9issen, muss Du vill\u00e4icht m\u00e9i PIN-Coden aginn.**\n\nNot\u00e9ier w.e.g dass Du d\u00e4in Apple TV mat d\u00ebser Integratioun *net\" ausschalten kanns. N\u00ebmmen de Mediaspiller am Home Assistant schalt aus!", + "title": "Apple TV dob\u00e4isetzen best\u00e4tegen" + }, + "pair_no_pin": { + "description": "Kopplung ass n\u00e9ideg fir de `{protocol}` Service. G\u00ebff de PIN {pin} op dengem Apple TV an fir w\u00e9iderzefueren", + "title": "Kopplung" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Code" + }, + "description": "Kopplung ass n\u00e9ideg fir de `{protocol}` Protokoll. G\u00ebff de PIN code un deen um Ecran ugewise g\u00ebtt. Nullen op der 1ter Plaatz ginn ewechgelooss, dh g\u00ebff 123 wann de gewise Code 0123 ass.", + "title": "Kopplung" + }, + "reconfigure": { + "description": "D\u00ebsen Apple TV huet e puer Verbindungsschwieregkeeten a muss nei konfigur\u00e9iert ginn.", + "title": "Apparat Rekonfiguratioun" + }, + "user": { + "data": { + "device_input": "Apparat" + }, + "description": "F\u00e4nk un andeems Du den Numm vum Apparat (z. B. Kichen oder Schlofkummer) oder IP Adress vum Apple TV deen soll dob\u00e4igesat ginn ag\u00ebss.\n\nFalls d\u00e4in Apparat nez ugewise g\u00ebtt oder iergendwelch Problemer hues, prob\u00e9ier d'IP Adress vum Apparat anzeginn.\n\n{devices}", + "title": "Neien Apple TV ariichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Schlalt den Apparat net un wann den Home Assistant start" + }, + "description": "Allgemeng Apparat Astellungen konfigur\u00e9ieren" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/nl.json b/homeassistant/components/apple_tv/translations/nl.json new file mode 100644 index 0000000000..a11488ebca --- /dev/null +++ b/homeassistant/components/apple_tv/translations/nl.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "backoff": "Het apparaat accepteert op dit moment geen koppelingsverzoeken (u heeft mogelijk te vaak een ongeldige pincode ingevoerd), probeer het later opnieuw.", + "device_did_not_pair": "Er is geen poging gedaan om het koppelingsproces te voltooien vanaf het apparaat.", + "invalid_config": "De configuratie voor dit apparaat is onvolledig. Probeer het opnieuw toe te voegen." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/no.json b/homeassistant/components/apple_tv/translations/no.json new file mode 100644 index 0000000000..88a7c98615 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/no.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "backoff": "Enheten godtar ikke parringsanmodninger for \u00f8yeblikket (du har kanskje angitt en ugyldig PIN-kode for mange ganger), pr\u00f8v igjen senere.", + "device_did_not_pair": "Ingen fors\u00f8k p\u00e5 \u00e5 fullf\u00f8re paringsprosessen ble gjort fra enheten", + "invalid_config": "Konfigurasjonen for denne enheten er ufullstendig. Pr\u00f8v \u00e5 legge den til p\u00e5 nytt.", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "unknown": "Uventet feil" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "invalid_auth": "Ugyldig godkjenning", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "no_usable_service": "En enhet ble funnet, men kunne ikke identifisere noen m\u00e5te \u00e5 etablere en tilkobling til den. Hvis du fortsetter \u00e5 se denne meldingen, kan du pr\u00f8ve \u00e5 angi IP-adressen eller starte Apple TV p\u00e5 nytt.", + "unknown": "Uventet feil" + }, + "flow_title": "", + "step": { + "confirm": { + "description": "Du er i ferd med \u00e5 legge til Apple TV med navnet {name} i Home Assistant.\n\n**For \u00e5 fullf\u00f8re prosessen m\u00e5 du kanskje angi flere PIN-koder.**\n\nV\u00e6r oppmerksom p\u00e5 at du *ikke* kan sl\u00e5 av Apple TV med denne integreringen. Bare mediespilleren i Home Assistant sl\u00e5r seg av!", + "title": "Bekreft at du legger til Apple TV" + }, + "pair_no_pin": { + "description": "Paring kreves for tjenesten {protocol}. Skriv inn PIN-koden {pin} p\u00e5 Apple TV for \u00e5 fortsette.", + "title": "Sammenkobling" + }, + "pair_with_pin": { + "data": { + "pin": "PIN kode" + }, + "description": "Paring kreves for protokollen {protocol}. Skriv inn PIN-koden som vises p\u00e5 skjermen. Ledende nuller utelates, det vil si angi 123 hvis den viste koden er 0123.", + "title": "Sammenkobling" + }, + "reconfigure": { + "description": "Denne Apple TVen har noen tilkoblingsvansker og m\u00e5 konfigureres p\u00e5 nytt", + "title": "Omkonfigurering av enheter" + }, + "service_problem": { + "description": "Det oppstod et problem under sammenkobling av protokollen \"{protocol}\". Det vil bli ignorert.", + "title": "Kunne ikke legge til tjenesten" + }, + "user": { + "data": { + "device_input": "Enhet" + }, + "description": "Start med \u00e5 skrive inn enhetsnavnet (f.eks. kj\u00f8kken eller soverom) eller IP-adressen til Apple TV-en du vil legge til. Hvis noen enheter ble funnet automatisk p\u00e5 nettverket ditt, vises de nedenfor.\n\nHvis du ikke kan se enheten eller oppleve problemer, kan du pr\u00f8ve \u00e5 angi enhetens IP-adresse.\n\n{devices}", + "title": "Konfigurere en ny Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Ikke sl\u00e5 p\u00e5 enheten n\u00e5r du starter Home Assistant" + }, + "description": "Konfigurer generelle enhetsinnstillinger" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/pl.json b/homeassistant/components/apple_tv/translations/pl.json new file mode 100644 index 0000000000..e8950d1c71 --- /dev/null +++ b/homeassistant/components/apple_tv/translations/pl.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "backoff": "Urz\u0105dzenie w tej chwili nie akceptuje \u017c\u0105da\u0144 parowania (by\u0107 mo\u017ce zbyt wiele razy wpisa\u0142e\u015b nieprawid\u0142owy kod PIN), spr\u00f3buj ponownie p\u00f3\u017aniej.", + "device_did_not_pair": "Nie podj\u0119to pr\u00f3by zako\u0144czenia procesu parowania z urz\u0105dzenia.", + "invalid_config": "Konfiguracja tego urz\u0105dzenia jest niekompletna. Spr\u00f3buj doda\u0107 go ponownie.", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "error": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "no_usable_service": "Znaleziono urz\u0105dzenie, ale nie uda\u0142o si\u0119 zidentyfikowa\u0107 \u017cadnego sposobu na nawi\u0105zanie z nim po\u0142\u0105czenia. Je\u015bli nadal widzisz t\u0119 wiadomo\u015b\u0107, spr\u00f3buj poda\u0107 jego adres IP lub uruchom ponownie Apple TV.", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Zamierzasz doda\u0107 Apple TV o nazwie \"{name}\" do Home Assistanta. \n\n **Aby uko\u0144czy\u0107 ca\u0142y proces, mo\u017ce by\u0107 konieczne wprowadzenie wielu kod\u00f3w PIN.** \n\nPami\u0119taj, \u017ce \"NIE\" b\u0119dziesz w stanie wy\u0142\u0105czy\u0107 Apple TV dzi\u0119ki tej integracji. Wy\u0142\u0105cza si\u0119 tylko sam odtwarzacz multimedialny w Home Assistant!", + "title": "Potwierdzenie dodania Apple TV" + }, + "pair_no_pin": { + "description": "Parowanie jest wymagane dla us\u0142ugi \"{protocol}\". Aby kontynuowa\u0107, wprowad\u017a kod {pin} na swoim Apple TV.", + "title": "Parowanie" + }, + "pair_with_pin": { + "data": { + "pin": "Kod PIN" + }, + "description": "Parowanie jest wymagane dla protoko\u0142u \"{protocol}\". Wprowad\u017a kod PIN wy\u015bwietlony na ekranie. Zera poprzedzaj\u0105ce nale\u017cy pomin\u0105\u0107, tj. wpisa\u0107 123, zamiast 0123.", + "title": "Parowanie" + }, + "reconfigure": { + "description": "Ten Apple TV ma pewne problemy z po\u0142\u0105czeniem i musi zosta\u0107 ponownie skonfigurowany.", + "title": "Ponowna konfiguracja urz\u0105dzenia" + }, + "service_problem": { + "description": "Wyst\u0105pi\u0142 problem podczas parowania protoko\u0142u \"{protocol}\". Zostanie on zignorowany.", + "title": "Nie uda\u0142o si\u0119 doda\u0107 us\u0142ugi" + }, + "user": { + "data": { + "device_input": "Urz\u0105dzenie" + }, + "description": "Zacznij od wprowadzenia nazwy urz\u0105dzenia (np. Kuchnia lub Sypialnia) lub adresu IP Apple TV, kt\u00f3re chcesz doda\u0107. Je\u015bli jakie\u015b urz\u0105dzenia zosta\u0142y automatycznie znalezione w Twojej sieci, s\u0105 one pokazane poni\u017cej. \n\nJe\u015bli nie widzisz swojego urz\u0105dzenia lub wyst\u0119puj\u0105 jakiekolwiek problemy, spr\u00f3buj okre\u015bli\u0107 adres IP urz\u0105dzenia. \n\n{devices}", + "title": "Konfiguracja nowego Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Nie w\u0142\u0105czaj urz\u0105dzenia podczas uruchamiania Home Assistanta" + }, + "description": "Skonfiguruj og\u00f3lne ustawienia urz\u0105dzenia" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/pt.json b/homeassistant/components/apple_tv/translations/pt.json new file mode 100644 index 0000000000..486ff0c51e --- /dev/null +++ b/homeassistant/components/apple_tv/translations/pt.json @@ -0,0 +1,61 @@ +{ + "config": { + "abort": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "unknown": "Erro inesperado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "no_usable_service": "Foi encontrado um dispositivo, mas n\u00e3o foi poss\u00edvel identificar nenhuma forma de estabelecer uma liga\u00e7\u00e3o com ele. Se continuar a ver esta mensagem, tente especificar o endere\u00e7o IP ou reiniciar a sua Apple TV.", + "unknown": "Erro inesperado" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "Est\u00e1 prestes a adicionar a Apple TV com o nome `{name}` ao Home Assistant.\n\n** Para completar o processo, poder\u00e1 ter que inserir v\u00e1rios c\u00f3digos PIN.**\n\nNote que *n\u00e3o* conseguir\u00e1 desligar a sua Apple TV com esta integra\u00e7\u00e3o. Apenas o media player no Home Assistant ser\u00e1 desligado!", + "title": "Confirme a adi\u00e7\u00e3o da Apple TV" + }, + "pair_no_pin": { + "description": "\u00c9 necess\u00e1rio fazer o emparelhamento com protocolo `{protocol}`. Insira o c\u00f3digo PIN {pin} na sua Apple TV para continuar.", + "title": "Emparelhamento" + }, + "pair_with_pin": { + "data": { + "pin": "C\u00f3digo PIN" + }, + "description": "\u00c9 necess\u00e1rio fazer o emparelhamento com protocolo `{protocol}`. Insira o c\u00f3digo PIN exibido no ecran. Os zeros iniciais devem ser omitidos, ou seja, digite 123 se o c\u00f3digo exibido for 0123.", + "title": "Emparelhamento" + }, + "reconfigure": { + "description": "Esta Apple TV apresenta dificuldades de liga\u00e7\u00e3o e precisa ser reconfigurada.", + "title": "Reconfigura\u00e7\u00e3o do dispositivo" + }, + "service_problem": { + "description": "Ocorreu um problema durante o protocolo de emparelhamento `{protocol}`. Ser\u00e1 ignorado.", + "title": "Falha ao adicionar servi\u00e7o" + }, + "user": { + "data": { + "device_input": "Dispositivo" + }, + "description": "Comece por introduzir o nome do dispositivo (por exemplo, Cozinha ou Quarto) ou o endere\u00e7o IP da Apple TV que pretende adicionar. Se algum dispositivo foi automaticamente encontrado na sua rede, ele \u00e9 mostrado abaixo.\n\nSe n\u00e3o conseguir ver o seu dispositivo ou se tiver algum problema, tente especificar o endere\u00e7o IP do dispositivo.\n\n{devices}", + "title": "Configure uma nova Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "N\u00e3o ligue o dispositivo ao iniciar o Home Assistant" + }, + "description": "Definir as configura\u00e7\u00f5es gerais do dispositivo" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/ru.json b/homeassistant/components/apple_tv/translations/ru.json new file mode 100644 index 0000000000..e3f5804ceb --- /dev/null +++ b/homeassistant/components/apple_tv/translations/ru.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "backoff": "\u0412 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u044b \u043d\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 (\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0412\u044b \u0441\u043b\u0438\u0448\u043a\u043e\u043c \u043c\u043d\u043e\u0433\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434), \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435.", + "device_did_not_pair": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u043f\u044b\u0442\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", + "invalid_config": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0433\u043e \u0435\u0449\u0451 \u0440\u0430\u0437.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "no_usable_service": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u0415\u0441\u043b\u0438 \u0412\u044b \u0443\u0436\u0435 \u0432\u0438\u0434\u0435\u043b\u0438 \u044d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0438\u043b\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u0435\u0433\u043e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "\u0412\u044b \u0441\u043e\u0431\u0438\u0440\u0430\u0435\u0442\u0435\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Apple TV `{name}` \u0432 Home Assistant. \n\n**\u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0412\u0430\u043c \u043c\u043e\u0436\u0435\u0442 \u043f\u043e\u0442\u0440\u0435\u0431\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e PIN-\u043a\u043e\u0434\u043e\u0432.** \n\n\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043d\u0438\u043c\u0430\u043d\u0438\u0435, \u0447\u0442\u043e \u0412\u044b *\u043d\u0435* \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c Apple TV \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438. \u0412 Home Assistant \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0432\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440!", + "title": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 Apple TV" + }, + "pair_no_pin": { + "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u044b`{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0435\u043d\u0438\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u0435\u043c Apple TV.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 `{protocol}`. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435. \u041f\u0435\u0440\u0432\u044b\u0435 \u043d\u0443\u043b\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043e\u043f\u0443\u0449\u0435\u043d\u044b, \u0442.\u0435. \u0432\u0432\u0435\u0434\u0438\u0442\u0435 123, \u0435\u0441\u043b\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u043a\u043e\u0434 0123.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435" + }, + "reconfigure": { + "description": "\u0423 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Apple TV \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438, \u0435\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c.", + "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + }, + "service_problem": { + "description": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u0440\u0438 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 `{protocol}`. \u042d\u0442\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e.", + "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u043b\u0443\u0436\u0431\u0443" + }, + "user": { + "data": { + "device_input": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u041d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u0432\u0432\u043e\u0434\u0430 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u041a\u0443\u0445\u043d\u044f \u0438\u043b\u0438 \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0430 Apple TV, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c. \u0415\u0441\u043b\u0438 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0431\u044b\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u044b \u0432 \u0412\u0430\u0448\u0435\u0439 \u0441\u0435\u0442\u0438, \u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u043d\u0438\u0436\u0435. \n\n\u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0435 \u0432\u0438\u0434\u0438\u0442\u0435 \u0441\u0432\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438\u043b\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0442 \u043a\u0430\u043a\u0438\u0435-\u043b\u0438\u0431\u043e \u0434\u0440\u0443\u0433\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043f\u0440\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0443\u043a\u0430\u0437\u0430\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \n\n {devices}", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043d\u043e\u0432\u043e\u0433\u043e Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u041d\u0435 \u0432\u043a\u043b\u044e\u0447\u0430\u0439\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435 Home Assistant" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/sl.json b/homeassistant/components/apple_tv/translations/sl.json new file mode 100644 index 0000000000..997d60402c --- /dev/null +++ b/homeassistant/components/apple_tv/translations/sl.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "Naprava je \u017ee name\u0161\u010dena", + "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", + "backoff": "Naprav v tem trenutku ne sprejema zahtev za seznanitev (morda ste preve\u010dkrat vnesli napa\u010den PIN). Pokusitve znova kasneje.", + "device_did_not_pair": "Iz te naprave ni bilo poskusov zaklju\u010diti seznanjanja.", + "invalid_config": "Namestitev te naprave ni bila zaklju\u010dena. Poskusite ponovno.", + "no_devices_found": "Ni najdenih naprav v omre\u017eju", + "unknown": "Nepri\u010dakovana napaka" + }, + "error": { + "already_configured": "Naprava je \u017ee name\u0161\u010dena", + "invalid_auth": "Napaka pri overjanju", + "no_devices_found": "Ni najdenih naprav v omre\u017eju", + "no_usable_service": "Najdena je bila naprava, za katero ni znan na\u010din povezovanja. \u010ce boste \u0161e vedno videli to sporo\u010dilo, poskusite dolo\u010diti IP naslov ali pa ponovno za\u017eenite Apple TV.", + "unknown": "Nepri\u010dakovana napaka" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "V Home Assistant nameravate dodati Apple TV z imenom `{name}`.\n\n**Za dokon\u010danje postopka boste morda morali ve\u010dkrat vnesti PIN kodo**\n\nS to integracijo ne boste mogli ugasniti svojega Apple TV. Ugasnjena bosta zgolj medijski predvajalnik in Home Assistant!", + "title": "Potrdite dodajanje Apple TV" + }, + "pair_no_pin": { + "description": "Protokol '{protocol}` zahteva seznanitev. Vnesite PIN {pin}, ki je prikazan na Apple TV.", + "title": "Seznanjanje" + }, + "pair_with_pin": { + "data": { + "pin": "PIN koda" + }, + "description": "Protokol '{protocol}` zahteva seznanitev. Vnesite PIN, ki je prikazan na zaslonu. Vodilnih ni\u010del ne vna\u0161ajte - vnesite 123, \u010de je prikazano 0123.", + "title": "Seznanjanje" + }, + "reconfigure": { + "description": "Ta Apple TV ima nekaj te\u017eav in mora biti ponovno konfiguriran.", + "title": "Ponovna namestitev naprave" + }, + "service_problem": { + "description": "Pri usklajevanju protokola `{protocol}` je pri\u0161lo do te\u017eave. Ta bo prezrta.", + "title": "Naprave ni mogo\u010de dodati" + }, + "user": { + "data": { + "device_input": "Naprava" + }, + "description": "Za\u010dnite z vnosom imena naprave (npr. kuhinja ali splanica) ali IP naslova Apple TV, ki bi ga radi dodali. \u010ce so katere naprave bile najdene samodejno v omre\u017eju, so prikazane spodaj.\n\n\u010ce ne vidite svoje naprave ali imate te\u017eave, poskusite dolo\u010diti nov IP.\n\n{devices}", + "title": "Namesti nov Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Ne vkpaljajte naprave ob zagonu Home Assistant-a" + }, + "description": "Konfiguracija splo\u0161nih nastavitev naprave" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json new file mode 100644 index 0000000000..0ddc466a6f --- /dev/null +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "title": "Apple TV eklemeyi onaylay\u0131n" + }, + "pair_no_pin": { + "title": "E\u015fle\u015ftirme" + }, + "pair_with_pin": { + "data": { + "pin": "PIN Kodu" + }, + "title": "E\u015fle\u015ftirme" + }, + "reconfigure": { + "description": "Bu Apple TV baz\u0131 ba\u011flant\u0131 sorunlar\u0131 ya\u015f\u0131yor ve yeniden yap\u0131land\u0131r\u0131lmas\u0131 gerekiyor.", + "title": "Cihaz\u0131n yeniden yap\u0131land\u0131r\u0131lmas\u0131" + }, + "service_problem": { + "title": "Hizmet eklenemedi" + }, + "user": { + "data": { + "device_input": "Cihaz" + }, + "title": "Yeni bir Apple TV kurun" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "Home Assistant'\u0131 ba\u015flat\u0131rken cihaz\u0131 a\u00e7may\u0131n" + }, + "description": "Genel cihaz ayarlar\u0131n\u0131 yap\u0131land\u0131r\u0131n" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json new file mode 100644 index 0000000000..bb1f8e025c --- /dev/null +++ b/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "pair_no_pin": { + "title": "\u914d\u5bf9\u4e2d" + }, + "pair_with_pin": { + "data": { + "pin": "PIN\u7801" + } + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/zh-Hant.json b/homeassistant/components/apple_tv/translations/zh-Hant.json new file mode 100644 index 0000000000..269e207e8a --- /dev/null +++ b/homeassistant/components/apple_tv/translations/zh-Hant.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "backoff": "\u88dd\u7f6e\u4e0d\u63a5\u53d7\u6b64\u6b21\u914d\u5c0d\u8acb\u6c42\uff08\u53ef\u80fd\u8f38\u5165\u592a\u591a\u6b21\u7121\u6548\u7684 PIN \u78bc\uff09\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66\u3002", + "device_did_not_pair": "\u88dd\u7f6e\u6c92\u6709\u5617\u8a66\u914d\u5c0d\u5b8c\u6210\u904e\u7a0b\u3002", + "invalid_config": "\u6b64\u88dd\u7f6e\u8a2d\u5b9a\u4e0d\u5b8c\u6574\uff0c\u8acb\u7a0d\u5019\u518d\u8a66\u4e00\u6b21\u3002", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "no_usable_service": "\u627e\u5230\u7684\u88dd\u7f6e\u7121\u6cd5\u8b58\u5225\u4ee5\u9032\u884c\u9023\u7dda\u3002\u5047\u5982\u6b64\u8a0a\u606f\u91cd\u8907\u767c\u751f\u3002\u8acb\u8a66\u8457\u6307\u5b9a\u7279\u5b9a IP \u4f4d\u5740\u6216\u91cd\u555f Apple TV\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Apple TV\uff1a{name}", + "step": { + "confirm": { + "description": "\u6b63\u8981\u65b0\u589e\u540d\u70ba `{name}` \u7684 Apple TV \u81f3 Home Assistant\u3002\n\n**\u6b32\u5b8c\u6210\u6b65\u9a5f\uff0c\u5fc5\u9808\u8f38\u5165\u591a\u7d44 PIN \u78bc\u3002**\n\n\u8acb\u6ce8\u610f\uff1a\u6b64\u6574\u5408\u4e26 *\u7121\u6cd5* \u9032\u884c Apple TV \u95dc\u6a5f\u7684\u52d5\u4f5c\uff0c\u50c5\u80fd\u65bc Home Assistant \u4e2d\u95dc\u9589\u5a92\u9ad4\u64ad\u653e\u5668\u529f\u80fd\uff01", + "title": "\u78ba\u8a8d\u65b0\u589e Apple TV" + }, + "pair_no_pin": { + "description": "`{protocol}` \u670d\u52d9\u9700\u8981\u9032\u884c\u914d\u5c0d\uff0c\u8acb\u8f38\u5165 Apple TV \u4e0a\u6240\u986f\u793a\u4e4b PIN {pin} \u4ee5\u7e7c\u7e8c\u3002", + "title": "\u914d\u5c0d\u4e2d" + }, + "pair_with_pin": { + "data": { + "pin": "PIN \u78bc" + }, + "description": "\u914d\u5c0d\u9700\u8981 `{protocol}` \u901a\u8a0a\u5354\u5b9a\u3002\u8acb\u8f38\u5165\u986f\u793a\u65bc\u756b\u9762\u4e0a\u7684 PIN \u78bc\uff0c\u524d\u65b9\u7684 0 \u53ef\u5ffd\u8996\u986f\u793a\u78bc\u70ba 0123\uff0c\u5247\u8f38\u5165 123\u3002", + "title": "\u914d\u5c0d\u4e2d" + }, + "reconfigure": { + "description": "\u6b64 Apple TV \u906d\u9047\u5230\u4e00\u4e9b\u9023\u7dda\u554f\u984c\uff0c\u5fc5\u9808\u91cd\u65b0\u8a2d\u5b9a\u3002", + "title": "\u88dd\u7f6e\u91cd\u65b0\u8a2d\u5b9a" + }, + "service_problem": { + "description": "\u7576\u914d\u5c0d `{protocol}` \u6642\u767c\u751f\u554f\u984c\uff0c\u5c07\u6703\u9032\u884c\u5ffd\u7565\u3002", + "title": "\u65b0\u589e\u670d\u52d9\u5931\u6557" + }, + "user": { + "data": { + "device_input": "\u88dd\u7f6e" + }, + "description": "\u9996\u5148\u8f38\u5165\u6240\u8981\u65b0\u589e\u7684 Apple TV \u88dd\u7f6e\u540d\u7a31\uff08\u4f8b\u5982\u5eda\u623f\u6216\u81e5\u5ba4\uff09\u6216 IP \u4f4d\u5740\u3002\u5047\u5982\u65bc\u5340\u7db2\u4e0a\u627e\u5230\u4efb\u4f55\u88dd\u7f6e\uff0c\u5c07\u6703\u986f\u793a\u65bc\u4e0b\u65b9\u3002\n\n\u5047\u5982\u7121\u6cd5\u770b\u5230\u88dd\u7f6e\u6216\u906d\u9047\u4efb\u4f55\u554f\u984c\uff0c\u8acb\u8a66\u8457\u6307\u5b9a\u88dd\u7f6e\u7684 IP \u4f4d\u5740\u3002\n\n{devices}", + "title": "\u8a2d\u5b9a\u4e00\u7d44 Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u7576\u958b\u59cb Home Assistant \u6642\u4e0d\u8981\u958b\u555f\u88dd\u7f6e" + }, + "description": "\u8a2d\u5b9a\u4e00\u822c\u88dd\u7f6e\u8a2d\u5b9a" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json index 05f5615016..92ad0e2266 100644 --- a/homeassistant/components/arcam_fmj/translations/de.json +++ b/homeassistant/components/arcam_fmj/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindungsfehler" }, "step": { "user": { diff --git a/homeassistant/components/arcam_fmj/translations/pt.json b/homeassistant/components/arcam_fmj/translations/pt.json index fdeb639b12..097e3d086d 100644 --- a/homeassistant/components/arcam_fmj/translations/pt.json +++ b/homeassistant/components/arcam_fmj/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "error": { "one": "uma", "other": "mais" diff --git a/homeassistant/components/arcam_fmj/translations/zh-Hant.json b/homeassistant/components/arcam_fmj/translations/zh-Hant.json index fd2cb2181a..853b498a51 100644 --- a/homeassistant/components/arcam_fmj/translations/zh-Hant.json +++ b/homeassistant/components/arcam_fmj/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, @@ -15,7 +15,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u7aef\u540d\u7a31\u6216 Heos \u8a2d\u5099 IP \u4f4d\u5740\u3002" + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u7aef\u540d\u7a31\u6216 Heos \u88dd\u7f6e IP \u4f4d\u5740\u3002" } } }, diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index 2b96bdfa5b..2ced7577fd 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Dieses Ger\u00e4t wurde bereits zu HomeAssistant hinzugef\u00fcgt" }, + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/atag/translations/nl.json b/homeassistant/components/atag/translations/nl.json index 96f135848e..55478f765e 100644 --- a/homeassistant/components/atag/translations/nl.json +++ b/homeassistant/components/atag/translations/nl.json @@ -12,7 +12,7 @@ "data": { "email": "Email", "host": "Host", - "port": "Poort (10000)" + "port": "Poort " }, "title": "Verbinding maken met het apparaat" } diff --git a/homeassistant/components/atag/translations/pt.json b/homeassistant/components/atag/translations/pt.json index d34bb36bc0..16752dd007 100644 --- a/homeassistant/components/atag/translations/pt.json +++ b/homeassistant/components/atag/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/atag/translations/zh-Hant.json b/homeassistant/components/atag/translations/zh-Hant.json index 164e87a964..b616437aa2 100644 --- a/homeassistant/components/atag/translations/zh-Hant.json +++ b/homeassistant/components/atag/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "unauthorized": "\u914d\u5c0d\u906d\u62d2\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a8d\u8b49\u8acb\u6c42" + "unauthorized": "\u914d\u5c0d\u906d\u62d2\uff0c\u8acb\u6aa2\u67e5\u88dd\u7f6e\u8a8d\u8b49\u8acb\u6c42" }, "step": { "user": { @@ -14,7 +14,7 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" } } } diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index c2c383468f..67649b7edb 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,7 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": ["py-august==0.25.0"], + "requirements": ["py-august==0.25.2"], "dependencies": ["configurator"], "codeowners": ["@bdraco"], "config_flow": true diff --git a/homeassistant/components/august/translations/no.json b/homeassistant/components/august/translations/no.json index 6a418ccdc9..ae314897e7 100644 --- a/homeassistant/components/august/translations/no.json +++ b/homeassistant/components/august/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/august/translations/pt.json b/homeassistant/components/august/translations/pt.json index 5560383b71..7daa90fad2 100644 --- a/homeassistant/components/august/translations/pt.json +++ b/homeassistant/components/august/translations/pt.json @@ -1,15 +1,27 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { + "login_method": "M\u00e9todo de login", "password": "Palavra-passe", "username": "Nome de Utilizador" }, "description": "Se o m\u00e9todo de login for 'email', Nome do utilizador \u00e9 o endere\u00e7o de email. Se o m\u00e9todo de login for 'telefone', Nome do utilizador ser\u00e1 o n\u00famero de telefone no formato '+NNNNNNNNN'." + }, + "validation": { + "data": { + "code": "C\u00f3digo de verifica\u00e7\u00e3o" + } } } } diff --git a/homeassistant/components/aurora/translations/de.json b/homeassistant/components/aurora/translations/de.json new file mode 100644 index 0000000000..95312fe794 --- /dev/null +++ b/homeassistant/components/aurora/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Schwellenwert (%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/pt.json b/homeassistant/components/aurora/translations/pt.json index aad75b3bed..336f6ac5f6 100644 --- a/homeassistant/components/aurora/translations/pt.json +++ b/homeassistant/components/aurora/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/auth/translations/no.json b/homeassistant/components/auth/translations/no.json index ea0f1baa06..c0252e045b 100644 --- a/homeassistant/components/auth/translations/no.json +++ b/homeassistant/components/auth/translations/no.json @@ -2,10 +2,10 @@ "mfa_setup": { "notify": { "abort": { - "no_available_service": "Ingen varslingstjenester er tilgjengelig." + "no_available_service": "Ingen varslingstjenester er tilgjengelig" }, "error": { - "invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen." + "invalid_code": "Ugyldig kode, vennligst pr\u00f8v igjen" }, "step": { "init": { @@ -25,8 +25,8 @@ }, "step": { "init": { - "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" + "description": "For \u00e5 aktivere totrinnsbekreftelse ved hjelp av tidsbaserte engangspassord, skann QR-koden med godkjenningsappen din. Hvis du ikke har en, anbefaler vi 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 totrinnsbekreftelse ved hjelp av TOTP" } }, "title": "" diff --git a/homeassistant/components/avri/.translations/en.json b/homeassistant/components/avri/.translations/en.json deleted file mode 100644 index 83cd4232d4..0000000000 --- a/homeassistant/components/avri/.translations/en.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "This address is already configured." - }, - "error": { - "invalid_country_code": "Unknown 2 letter country code.", - "invalid_house_number": "Invalid house number." - }, - "step": { - "user": { - "data": { - "country_code": "2 Letter country code", - "house_number": "House number", - "house_number_extension": "House number extension", - "zip_code": "Zip code" - }, - "description": "Enter your address", - "title": "Avri" - } - } - }, - "title": "Avri" -} \ No newline at end of file diff --git a/homeassistant/components/avri/.translations/nl.json b/homeassistant/components/avri/.translations/nl.json deleted file mode 100644 index 22798b0968..0000000000 --- a/homeassistant/components/avri/.translations/nl.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Dit adres is reeds geconfigureerd." - }, - "error": { - "invalid_country_code": "Onbekende landcode", - "invalid_house_number": "Ongeldig huisnummer." - }, - "step": { - "user": { - "data": { - "country_code": "2 Letter landcode", - "house_number": "Huisnummer", - "house_number_extension": "Huisnummer toevoeging", - "zip_code": "Postcode" - }, - "description": "Vul je adres in.", - "title": "Avri" - } - } - }, - "title": "Avri" -} \ No newline at end of file diff --git a/homeassistant/components/avri/__init__.py b/homeassistant/components/avri/__init__.py deleted file mode 100644 index f3b659ddcc..0000000000 --- a/homeassistant/components/avri/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -"""The avri component.""" -import asyncio -from datetime import timedelta - -from avri.api import Avri - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant - -from .const import ( - CONF_COUNTRY_CODE, - CONF_HOUSE_NUMBER, - CONF_HOUSE_NUMBER_EXTENSION, - CONF_ZIP_CODE, - DOMAIN, -) - -PLATFORMS = ["sensor"] -SCAN_INTERVAL = timedelta(hours=4) - - -async def async_setup(hass: HomeAssistant, config: dict): - """Set up the Avri component.""" - hass.data[DOMAIN] = {} - return True - - -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): - """Set up Avri from a config entry.""" - client = Avri( - postal_code=entry.data[CONF_ZIP_CODE], - house_nr=entry.data[CONF_HOUSE_NUMBER], - house_nr_extension=entry.data.get(CONF_HOUSE_NUMBER_EXTENSION), - country_code=entry.data[CONF_COUNTRY_CODE], - ) - - hass.data[DOMAIN][entry.entry_id] = client - - 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/avri/config_flow.py b/homeassistant/components/avri/config_flow.py deleted file mode 100644 index 987b3679b3..0000000000 --- a/homeassistant/components/avri/config_flow.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Config flow for Avri component.""" -import pycountry -import voluptuous as vol - -from homeassistant import config_entries -from homeassistant.const import CONF_ID - -from .const import ( - CONF_COUNTRY_CODE, - CONF_HOUSE_NUMBER, - CONF_HOUSE_NUMBER_EXTENSION, - CONF_ZIP_CODE, - DEFAULT_COUNTRY_CODE, -) -from .const import DOMAIN # pylint:disable=unused-import - -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_ZIP_CODE): str, - vol.Required(CONF_HOUSE_NUMBER): int, - vol.Optional(CONF_HOUSE_NUMBER_EXTENSION): str, - vol.Optional(CONF_COUNTRY_CODE, default=DEFAULT_COUNTRY_CODE): str, - } -) - - -class AvriConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Avri config flow.""" - - VERSION = 1 - - async def _show_setup_form(self, errors=None): - """Show the setup form to the user.""" - return self.async_show_form( - step_id="user", - data_schema=DATA_SCHEMA, - errors=errors or {}, - ) - - async def async_step_user(self, user_input=None): - """Handle the initial step.""" - if user_input is None: - return await self._show_setup_form() - - zip_code = user_input[CONF_ZIP_CODE].replace(" ", "").upper() - - errors = {} - if user_input[CONF_HOUSE_NUMBER] <= 0: - errors[CONF_HOUSE_NUMBER] = "invalid_house_number" - return await self._show_setup_form(errors) - if not pycountry.countries.get(alpha_2=user_input[CONF_COUNTRY_CODE]): - errors[CONF_COUNTRY_CODE] = "invalid_country_code" - return await self._show_setup_form(errors) - - unique_id = ( - f"{zip_code}" - f" " - f"{user_input[CONF_HOUSE_NUMBER]}" - f'{user_input.get(CONF_HOUSE_NUMBER_EXTENSION, "")}' - ) - - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=unique_id, - data={ - CONF_ID: unique_id, - CONF_ZIP_CODE: zip_code, - CONF_HOUSE_NUMBER: user_input[CONF_HOUSE_NUMBER], - CONF_HOUSE_NUMBER_EXTENSION: user_input.get( - CONF_HOUSE_NUMBER_EXTENSION, "" - ), - CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE], - }, - ) diff --git a/homeassistant/components/avri/const.py b/homeassistant/components/avri/const.py deleted file mode 100644 index dab3491b35..0000000000 --- a/homeassistant/components/avri/const.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Constants for the Avri integration.""" -CONF_COUNTRY_CODE = "country_code" -CONF_ZIP_CODE = "zip_code" -CONF_HOUSE_NUMBER = "house_number" -CONF_HOUSE_NUMBER_EXTENSION = "house_number_extension" -DOMAIN = "avri" -ICON = "mdi:trash-can-outline" -DEFAULT_COUNTRY_CODE = "NL" diff --git a/homeassistant/components/avri/manifest.json b/homeassistant/components/avri/manifest.json deleted file mode 100644 index 8a418bfb7b..0000000000 --- a/homeassistant/components/avri/manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "domain": "avri", - "name": "Avri", - "documentation": "https://www.home-assistant.io/integrations/avri", - "requirements": [ - "avri-api==0.1.7", - "pycountry==19.8.18" - ], - "codeowners": [ - "@timvancann" - ], - "config_flow": true -} \ No newline at end of file diff --git a/homeassistant/components/avri/sensor.py b/homeassistant/components/avri/sensor.py deleted file mode 100644 index 06519a5c45..0000000000 --- a/homeassistant/components/avri/sensor.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Support for Avri waste curbside collection pickup.""" -import logging - -from avri.api import Avri, AvriException - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_ID, DEVICE_CLASS_TIMESTAMP -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import HomeAssistantType - -from .const import DOMAIN, ICON - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_entry( - hass: HomeAssistantType, entry: ConfigEntry, async_add_entities -) -> None: - """Set up the Avri Waste platform.""" - client = hass.data[DOMAIN][entry.entry_id] - integration_id = entry.data[CONF_ID] - - try: - each_upcoming = await hass.async_add_executor_job(client.upcoming_of_each) - except AvriException as ex: - raise PlatformNotReady from ex - else: - entities = [ - AvriWasteUpcoming(client, upcoming.name, integration_id) - for upcoming in each_upcoming - ] - async_add_entities(entities, True) - - -class AvriWasteUpcoming(Entity): - """Avri Waste Sensor.""" - - def __init__(self, client: Avri, waste_type: str, integration_id: str): - """Initialize the sensor.""" - self._waste_type = waste_type - self._name = f"{self._waste_type}".title() - self._state = None - self._client = client - self._state_available = False - self._integration_id = integration_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return (f"{self._integration_id}" f"-{self._waste_type}").replace(" ", "") - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def available(self): - """Return True if entity is available.""" - return self._state_available - - @property - def device_class(self): - """Return the device class of the sensor.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def icon(self): - """Icon to use in the frontend.""" - return ICON - - async def async_update(self): - """Update the data.""" - if not self.enabled: - return - - try: - pickup_events = self._client.upcoming_of_each() - except AvriException as ex: - _LOGGER.error( - "There was an error retrieving upcoming garbage pickups: %s", ex - ) - self._state_available = False - self._state = None - else: - self._state_available = True - matched_events = list( - filter(lambda event: event.name == self._waste_type, pickup_events) - ) - if not matched_events: - self._state = None - else: - self._state = matched_events[0].day.date() diff --git a/homeassistant/components/avri/strings.json b/homeassistant/components/avri/strings.json deleted file mode 100644 index e00409ffa2..0000000000 --- a/homeassistant/components/avri/strings.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" - }, - "error": { - "invalid_house_number": "Invalid house number.", - "invalid_country_code": "Unknown 2 letter country code." - }, - "step": { - "user": { - "data": { - "zip_code": "Zip code", - "house_number": "House number", - "house_number_extension": "House number extension", - "country_code": "2 Letter country code" - }, - "description": "Enter your address", - "title": "Avri" - } - } - } -} diff --git a/homeassistant/components/avri/translations/ar.json b/homeassistant/components/avri/translations/ar.json deleted file mode 100644 index b23bf7e897..0000000000 --- a/homeassistant/components/avri/translations/ar.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u062a\u0645 \u062a\u0643\u0648\u064a\u0646 \u0647\u0630\u0627 \u0627\u0644\u0639\u0646\u0648\u0627\u0646 \u0628\u0627\u0644\u0641\u0639\u0644." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/ca.json b/homeassistant/components/avri/translations/ca.json deleted file mode 100644 index 77edbd4990..0000000000 --- a/homeassistant/components/avri/translations/ca.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada" - }, - "error": { - "invalid_country_code": "Codi de pa\u00eds desconegut.", - "invalid_house_number": "N\u00famero de casa no v\u00e0lid." - }, - "step": { - "user": { - "data": { - "country_code": "Codi de pa\u00eds de 2 lletres", - "house_number": "N\u00famero de casa", - "house_number_extension": "Ampliaci\u00f3 de n\u00famero de casa", - "zip_code": "Codi postal" - }, - "description": "Introdueix la teva adre\u00e7a", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/cs.json b/homeassistant/components/avri/translations/cs.json deleted file mode 100644 index e46abc942c..0000000000 --- a/homeassistant/components/avri/translations/cs.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Um\u00edst\u011bn\u00ed je ji\u017e nastaveno" - }, - "error": { - "invalid_country_code": "Nezn\u00e1m\u00fd dvoup\u00edsmenn\u00fd k\u00f3d zem\u011b.", - "invalid_house_number": "Neplatn\u00e9 \u010d\u00edslo domu." - }, - "step": { - "user": { - "data": { - "country_code": "2p\u00edsmenn\u00fd k\u00f3d zem\u011b", - "house_number": "\u010c\u00edslo domu", - "house_number_extension": "Roz\u0161\u00ed\u0159en\u00ed \u010d\u00edsla domu", - "zip_code": "PS\u010c" - }, - "description": "Zadejte svou adresu", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/de.json b/homeassistant/components/avri/translations/de.json deleted file mode 100644 index fc0ece086a..0000000000 --- a/homeassistant/components/avri/translations/de.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Position ist bereits konfiguriert" - }, - "error": { - "invalid_house_number": "Ung\u00fcltige Hausnummer" - }, - "step": { - "user": { - "data": { - "house_number": "Hausnummer", - "zip_code": "Postleitzahl" - }, - "description": "Gibt deine Adresse ein", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/en.json b/homeassistant/components/avri/translations/en.json deleted file mode 100644 index 832849a706..0000000000 --- a/homeassistant/components/avri/translations/en.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Location is already configured" - }, - "error": { - "invalid_country_code": "Unknown 2 letter country code.", - "invalid_house_number": "Invalid house number." - }, - "step": { - "user": { - "data": { - "country_code": "2 Letter country code", - "house_number": "House number", - "house_number_extension": "House number extension", - "zip_code": "Zip code" - }, - "description": "Enter your address", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/es.json b/homeassistant/components/avri/translations/es.json deleted file mode 100644 index 11539723fa..0000000000 --- a/homeassistant/components/avri/translations/es.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Esta direcci\u00f3n ya est\u00e1 configurada." - }, - "error": { - "invalid_country_code": "C\u00f3digo de pa\u00eds de 2 letras desconocido.", - "invalid_house_number": "N\u00famero de casa no v\u00e1lido." - }, - "step": { - "user": { - "data": { - "country_code": "C\u00f3digo de pa\u00eds de 2 letras", - "house_number": "N\u00famero de casa", - "house_number_extension": "Extensi\u00f3n del n\u00famero de casa", - "zip_code": "C\u00f3digo postal" - }, - "description": "Introduce tu direccion", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/et.json b/homeassistant/components/avri/translations/et.json deleted file mode 100644 index 0e83b89364..0000000000 --- a/homeassistant/components/avri/translations/et.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Aadress on juba m\u00e4\u00e4ratud" - }, - "error": { - "invalid_country_code": "Tundmatu kahet\u00e4heline riigikood.", - "invalid_house_number": "Tundmatu majanumber." - }, - "step": { - "user": { - "data": { - "country_code": "Kahet\u00e4heline riigikood", - "house_number": "Maja number", - "house_number_extension": "Maja numbri laiendus", - "zip_code": "Postiindeks" - }, - "description": "Sisesta oma aadress", - "title": "" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/fr.json b/homeassistant/components/avri/translations/fr.json deleted file mode 100644 index 188f82beae..0000000000 --- a/homeassistant/components/avri/translations/fr.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Cette adresse est d\u00e9j\u00e0 configur\u00e9e." - }, - "error": { - "invalid_country_code": "Code pays \u00e0 2 lettres inconnu.", - "invalid_house_number": "Num\u00e9ro de maison invalide." - }, - "step": { - "user": { - "data": { - "country_code": "Code pays \u00e0 2 lettres", - "house_number": "Num\u00e9ro de maison", - "house_number_extension": "Extension de num\u00e9ro de maison", - "zip_code": "Code postal" - }, - "description": "Entrez votre adresse", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/it.json b/homeassistant/components/avri/translations/it.json deleted file mode 100644 index 50c92e0678..0000000000 --- a/homeassistant/components/avri/translations/it.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "La posizione \u00e8 gi\u00e0 configurata" - }, - "error": { - "invalid_country_code": "Codice paese di 2 lettere sconosciuto.", - "invalid_house_number": "Numero civico non valido." - }, - "step": { - "user": { - "data": { - "country_code": "Codice paese di 2 lettere", - "house_number": "Numero civico", - "house_number_extension": "Estensione del numero civico", - "zip_code": "Codice di avviamento postale" - }, - "description": "Inserisci il tuo indirizzo", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/ko.json b/homeassistant/components/avri/translations/ko.json deleted file mode 100644 index ab6504519d..0000000000 --- a/homeassistant/components/avri/translations/ko.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\uc774 \uc8fc\uc18c\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." - }, - "error": { - "invalid_country_code": "\uc54c \uc218 \uc5c6\ub294 \uad6d\uac00\ucf54\ub4dc\uc785\ub2c8\ub2e4.", - "invalid_house_number": "\uc9d1 \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, - "step": { - "user": { - "data": { - "country_code": "2 \ubb38\uc790 \uad6d\uac00\ucf54\ub4dc", - "house_number": "\uc9d1 \ubc88\ud638", - "house_number_extension": "\uc9d1 \ubc88\ud638 \ucd94\uac00\uc815\ubcf4", - "zip_code": "\uc6b0\ud3b8 \ubc88\ud638" - }, - "description": "\uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/lb.json b/homeassistant/components/avri/translations/lb.json deleted file mode 100644 index 657640c2be..0000000000 --- a/homeassistant/components/avri/translations/lb.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Standuert ass scho konfigur\u00e9iert." - }, - "error": { - "invalid_country_code": "Onbekannte Zweestellege L\u00e4nner Code", - "invalid_house_number": "Ong\u00eblteg Haus Nummer" - }, - "step": { - "user": { - "data": { - "country_code": "Zweestellege L\u00e4nner Code", - "house_number": "Haus Nummer", - "house_number_extension": "Haus Nummer Extensioun", - "zip_code": "Postleitzuel" - }, - "description": "G\u00ebff deng Adresse un", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/nl.json b/homeassistant/components/avri/translations/nl.json deleted file mode 100644 index a5be62bfc1..0000000000 --- a/homeassistant/components/avri/translations/nl.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Locatie is al geconfigureerd" - }, - "error": { - "invalid_country_code": "Onbekende 2-letterige landcode." - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/no.json b/homeassistant/components/avri/translations/no.json deleted file mode 100644 index 3f1edaf4c7..0000000000 --- a/homeassistant/components/avri/translations/no.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Plasseringen er allerede konfigurert" - }, - "error": { - "invalid_country_code": "Ukjent landskode p\u00e5 2 bokstaver.", - "invalid_house_number": "Ugyldig husnummer." - }, - "step": { - "user": { - "data": { - "country_code": "2 Bokstavs landskode", - "house_number": "Husnummer", - "house_number_extension": "Utvidelse av husnummer", - "zip_code": "Postnummer" - }, - "description": "Skriv inn adressen din", - "title": "" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/pl.json b/homeassistant/components/avri/translations/pl.json deleted file mode 100644 index dfe3f85a38..0000000000 --- a/homeassistant/components/avri/translations/pl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Lokalizacja jest ju\u017c skonfigurowana" - }, - "error": { - "invalid_country_code": "Nieznany dwuliterowy kod kraju", - "invalid_house_number": "Nieprawid\u0142owy numer domu" - }, - "step": { - "user": { - "data": { - "country_code": "Dwuliterowy kod kraju", - "house_number": "Numer domu", - "house_number_extension": "Numer mieszkania", - "zip_code": "Kod pocztowy" - }, - "description": "Wpisz sw\u00f3j adres", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/ru.json b/homeassistant/components/avri/translations/ru.json deleted file mode 100644 index 01003d0e9d..0000000000 --- a/homeassistant/components/avri/translations/ru.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." - }, - "error": { - "invalid_country_code": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0439 \u0434\u0432\u0443\u0445\u0431\u0443\u043a\u0432\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b.", - "invalid_house_number": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0434\u043e\u043c\u0430." - }, - "step": { - "user": { - "data": { - "country_code": "\u0414\u0432\u0443\u0445\u0431\u0443\u043a\u0432\u0435\u043d\u043d\u044b\u0439 \u043a\u043e\u0434 \u0441\u0442\u0440\u0430\u043d\u044b", - "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0434\u043e\u043c\u0430", - "house_number_extension": "\u041b\u0438\u0442\u0435\u0440 \u0434\u043e\u043c\u0430 / \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435", - "zip_code": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" - }, - "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 Avri.", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/zh-Hant.json b/homeassistant/components/avri/translations/zh-Hant.json deleted file mode 100644 index 566a9e43dc..0000000000 --- a/homeassistant/components/avri/translations/zh-Hant.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "\u5ea7\u6a19\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" - }, - "error": { - "invalid_country_code": "\u672a\u77e5\u570b\u78bc\uff08\u5169\u5b57\u6bcd\uff09\u3002", - "invalid_house_number": "\u9580\u724c\u865f\u78bc\u932f\u8aa4\u3002" - }, - "step": { - "user": { - "data": { - "country_code": "\u570b\u78bc\uff08\u5169\u5b57\u6bcd\uff09", - "house_number": "\u9580\u724c\u865f\u78bc", - "house_number_extension": "\u9580\u724c\u865f\u78bc\u5206\u865f", - "zip_code": "\u90f5\u905e\u5340\u865f" - }, - "description": "\u8f38\u5165\u5730\u5740", - "title": "Avri" - } - } - } -} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/no.json b/homeassistant/components/awair/translations/no.json index 43ffe5960c..98486a28b0 100644 --- a/homeassistant/components/awair/translations/no.json +++ b/homeassistant/components/awair/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_access_token": "Ugyldig tilgangstoken", diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json index a637b68b0b..ea99bbf016 100644 --- a/homeassistant/components/awair/translations/pt.json +++ b/homeassistant/components/awair/translations/pt.json @@ -2,11 +2,17 @@ "config": { "abort": { "already_configured": "Conta j\u00e1 configurada", + "no_devices_found": "Nenhum dispositivo encontrado na rede", "reauth_successful": "Token de Acesso actualizado com sucesso" }, + "error": { + "invalid_access_token": "Token de acesso inv\u00e1lido", + "unknown": "Erro inesperado" + }, "step": { "reauth": { "data": { + "access_token": "Token de Acesso", "email": "Email" } }, diff --git a/homeassistant/components/awair/translations/zh-Hant.json b/homeassistant/components/awair/translations/zh-Hant.json index 8b40a8edef..11fe9ff88b 100644 --- a/homeassistant/components/awair/translations/zh-Hant.json +++ b/homeassistant/components/awair/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json index d030914090..4706350cdb 100644 --- a/homeassistant/components/axis/translations/de.json +++ b/homeassistant/components/axis/translations/de.json @@ -7,7 +7,8 @@ }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt." + "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "cannot_connect": "Verbindungsfehler" }, "flow_title": "Achsenger\u00e4t: {name} ({host})", "step": { diff --git a/homeassistant/components/axis/translations/pt.json b/homeassistant/components/axis/translations/pt.json index b7cbb547b0..8ba642263a 100644 --- a/homeassistant/components/axis/translations/pt.json +++ b/homeassistant/components/axis/translations/pt.json @@ -5,7 +5,10 @@ "link_local_address": "Eendere\u00e7os de liga\u00e7\u00e3o local n\u00e3o s\u00e3o suportados" }, "error": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "user": { diff --git a/homeassistant/components/axis/translations/zh-Hans.json b/homeassistant/components/axis/translations/zh-Hans.json index 32d738d838..0ed34907b1 100644 --- a/homeassistant/components/axis/translations/zh-Hans.json +++ b/homeassistant/components/axis/translations/zh-Hans.json @@ -7,6 +7,7 @@ "step": { "user": { "data": { + "host": "\u4e3b\u673a\u7aef", "password": "\u5bc6\u7801", "port": "\u7aef\u53e3", "username": "\u7528\u6237\u540d" diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json index 07cc81cc0f..1d7aaa7c74 100644 --- a/homeassistant/components/axis/translations/zh-Hant.json +++ b/homeassistant/components/axis/translations/zh-Hant.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", - "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099" + "not_axis_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Axis \u88dd\u7f6e" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "Axis \u8a2d\u5099\uff1a{name} ({host})", + "flow_title": "Axis \u88dd\u7f6e\uff1a{name} ({host})", "step": { "user": { "data": { @@ -20,7 +20,7 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "title": "\u8a2d\u5b9a Axis \u8a2d\u5099" + "title": "\u8a2d\u5b9a Axis \u88dd\u7f6e" } } }, @@ -30,7 +30,7 @@ "data": { "stream_profile": "\u9078\u64c7\u6240\u8981\u4f7f\u7528\u7684\u4e32\u6d41\u8a2d\u5b9a" }, - "title": "Axis \u8a2d\u5099\u5f71\u50cf\u4e32\u6d41\u9078\u9805" + "title": "Axis \u88dd\u7f6e\u5f71\u50cf\u4e32\u6d41\u9078\u9805" } } } diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json index cd849b9f93..1c940ea7a3 100644 --- a/homeassistant/components/azure_devops/translations/de.json +++ b/homeassistant/components/azure_devops/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "reauth": { "title": "Erneute Authentifizierung" diff --git a/homeassistant/components/azure_devops/translations/no.json b/homeassistant/components/azure_devops/translations/no.json index bc649dcadf..50ee7a7a2a 100644 --- a/homeassistant/components/azure_devops/translations/no.json +++ b/homeassistant/components/azure_devops/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", @@ -13,10 +13,10 @@ "step": { "reauth": { "data": { - "personal_access_token": "Token for personlig tilgang (PAT)" + "personal_access_token": "Personlig tilgangstoken (PAT)" }, - "description": "Autentiseringen mislyktes for {project_url} . Vennligst skriv inn gjeldende legitimasjon.", - "title": "Reautorisasjon" + "description": "Autentiseringen mislyktes for {project_url}. Vennligst skriv inn gjeldende legitimasjon.", + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/pt.json b/homeassistant/components/azure_devops/translations/pt.json index 50d5409ef8..2af1f54844 100644 --- a/homeassistant/components/azure_devops/translations/pt.json +++ b/homeassistant/components/azure_devops/translations/pt.json @@ -3,6 +3,10 @@ "abort": { "already_configured": "Conta j\u00e1 configurada", "reauth_successful": "Token de Acesso atualizado com sucesso" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/translations/nl.json b/homeassistant/components/binary_sensor/translations/nl.json index dc8ff4ec51..36dbb530fd 100644 --- a/homeassistant/components/binary_sensor/translations/nl.json +++ b/homeassistant/components/binary_sensor/translations/nl.json @@ -98,6 +98,10 @@ "off": "Normaal", "on": "Laag" }, + "battery_charging": { + "off": "Niet aan het opladen", + "on": "Opladen" + }, "cold": { "off": "Normaal", "on": "Koud" @@ -123,6 +127,7 @@ "on": "Heet" }, "light": { + "off": "Geen licht", "on": "Licht gedetecteerd" }, "lock": { @@ -137,6 +142,10 @@ "off": "Niet gedetecteerd", "on": "Gedetecteerd" }, + "moving": { + "off": "Niet bewegend", + "on": "In beweging" + }, "occupancy": { "off": "Niet gedetecteerd", "on": "Gedetecteerd" @@ -145,6 +154,10 @@ "off": "Gesloten", "on": "Open" }, + "plug": { + "off": "Unplugged", + "on": "Ingeplugd" + }, "presence": { "off": "Afwezig", "on": "Thuis" diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index cedcdc2732..9d7fdda100 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -98,6 +98,10 @@ "off": "Normal", "on": "Baixo" }, + "battery_charging": { + "off": "Sem carregar", + "on": "A carregar" + }, "cold": { "off": "Normal", "on": "Frio" @@ -122,6 +126,10 @@ "off": "Normal", "on": "Quente" }, + "light": { + "off": "Sem luz", + "on": "Com luz" + }, "lock": { "off": "Trancada", "on": "Destrancada" @@ -134,6 +142,10 @@ "off": "Limpo", "on": "Detectado" }, + "moving": { + "off": "Parado", + "on": "Em movimento" + }, "occupancy": { "off": "Limpo", "on": "Detectado" @@ -142,6 +154,10 @@ "off": "Fechado", "on": "Aberto" }, + "plug": { + "off": "Desligado", + "on": "Ligado" + }, "presence": { "off": "Fora", "on": "Casa" diff --git a/homeassistant/components/blebox/translations/pt.json b/homeassistant/components/blebox/translations/pt.json index b7fc26165a..9c2be6fd04 100644 --- a/homeassistant/components/blebox/translations/pt.json +++ b/homeassistant/components/blebox/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado", "unsupported_version": "O dispositivo BleBox possui firmware desatualizado. Atualize-o primeiro." }, "step": { diff --git a/homeassistant/components/blebox/translations/zh-Hant.json b/homeassistant/components/blebox/translations/zh-Hant.json index 5d11c2e9a7..b84105745a 100644 --- a/homeassistant/components/blebox/translations/zh-Hant.json +++ b/homeassistant/components/blebox/translations/zh-Hant.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "address_already_configured": "\u4f4d\u65bc {address} \u7684 BleBox \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "address_already_configured": "\u4f4d\u65bc {address} \u7684 BleBox \u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4", - "unsupported_version": "BleBox \u8a2d\u5099\u97cc\u9ad4\u904e\u820a\uff0c\u8acb\u5148\u9032\u884c\u66f4\u65b0\u3002" + "unsupported_version": "BleBox \u88dd\u7f6e\u97cc\u9ad4\u904e\u820a\uff0c\u8acb\u5148\u9032\u884c\u66f4\u65b0\u3002" }, - "flow_title": "BleBox \u8a2d\u5099\uff1a{name} ({host})", + "flow_title": "BleBox \u88dd\u7f6e\uff1a{name} ({host})", "step": { "user": { "data": { @@ -17,7 +17,7 @@ "port": "\u901a\u8a0a\u57e0" }, "description": "\u8a2d\u5b9a BleBox \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", - "title": "\u8a2d\u5b9a BleBox \u8a2d\u5099" + "title": "\u8a2d\u5b9a BleBox \u88dd\u7f6e" } } } diff --git a/homeassistant/components/blink/config_flow.py b/homeassistant/components/blink/config_flow.py index d244c31648..5c77add311 100644 --- a/homeassistant/components/blink/config_flow.py +++ b/homeassistant/components/blink/config_flow.py @@ -36,6 +36,7 @@ def _send_blink_2fa_pin(auth, pin): """Send 2FA pin to blink servers.""" blink = Blink() blink.auth = auth + blink.setup_login_ids() blink.setup_urls() return auth.send_auth_key(blink, pin) diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json index ec5ad6c53c..f5116110a0 100644 --- a/homeassistant/components/blink/translations/de.json +++ b/homeassistant/components/blink/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindungsfehler", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/blink/translations/no.json b/homeassistant/components/blink/translations/no.json index e2be7cf409..0b99005c38 100644 --- a/homeassistant/components/blink/translations/no.json +++ b/homeassistant/components/blink/translations/no.json @@ -12,10 +12,10 @@ "step": { "2fa": { "data": { - "2fa": "To-faktorskode" + "2fa": "Totrinnsbekreftelse kode" }, "description": "Skriv inn pin-koden som ble sendt til din e-posten", - "title": "Totrinnsverifisering" + "title": "Totrinnsbekreftelse" }, "user": { "data": { diff --git a/homeassistant/components/blink/translations/pt.json b/homeassistant/components/blink/translations/pt.json index 188effb27a..76c420a584 100644 --- a/homeassistant/components/blink/translations/pt.json +++ b/homeassistant/components/blink/translations/pt.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", - "invalid_access_token": "Token de acesso inv\u00e1lido" + "invalid_access_token": "Token de acesso inv\u00e1lido", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "2fa": { @@ -10,7 +14,8 @@ }, "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/blink/translations/zh-Hant.json b/homeassistant/components/blink/translations/zh-Hant.json index 5736c91714..3d05dc82ab 100644 --- a/homeassistant/components/blink/translations/zh-Hant.json +++ b/homeassistant/components/blink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index 5f38c420ff..265ec01b6d 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -169,9 +169,9 @@ class BME280Sensor(Entity): await self.hass.async_add_executor_job(self.bme280_client.update) if self.bme280_client.sensor.sample_ok: if self.type == SENSOR_TEMP: - temperature = round(self.bme280_client.sensor.temperature, 1) + temperature = round(self.bme280_client.sensor.temperature, 2) if self.temp_unit == TEMP_FAHRENHEIT: - temperature = round(celsius_to_fahrenheit(temperature), 1) + temperature = round(celsius_to_fahrenheit(temperature), 2) self._state = temperature elif self.type == SENSOR_HUMID: self._state = round(self.bme280_client.sensor.humidity, 1) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index c72d1ce40f..e9f6a0d7f6 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,29 +1,50 @@ """Reads vehicle status from BMW connected drive portal.""" +import asyncio import logging from bimmer_connected.account import ConnectedDriveAccount from bimmer_connected.country_selector import get_region_from_name import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_utc_time_change +from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from .const import ( + ATTRIBUTION, + CONF_ACCOUNT, + CONF_ALLOWED_REGIONS, + CONF_READ_ONLY, + CONF_REGION, + CONF_USE_LOCATION, + DATA_ENTRIES, + DATA_HASS_CONFIG, +) + _LOGGER = logging.getLogger(__name__) DOMAIN = "bmw_connected_drive" -CONF_REGION = "region" -CONF_READ_ONLY = "read_only" ATTR_VIN = "vin" ACCOUNT_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_REGION): vol.Any("north_america", "china", "rest_of_world"), - vol.Optional(CONF_READ_ONLY, default=False): cv.boolean, + vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS), + vol.Optional(CONF_READ_ONLY): cv.boolean, } ) @@ -31,8 +52,12 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: ACCOUNT_SCHEMA}}, extra=vol.ALLO SERVICE_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) +DEFAULT_OPTIONS = { + CONF_READ_ONLY: False, + CONF_USE_LOCATION: False, +} -BMW_COMPONENTS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"] +BMW_PLATFORMS = ["binary_sensor", "device_tracker", "lock", "notify", "sensor"] UPDATE_INTERVAL = 5 # in minutes SERVICE_UPDATE_STATE = "update_state" @@ -44,49 +69,162 @@ _SERVICE_MAP = { "find_vehicle": "trigger_remote_vehicle_finder", } +UNDO_UPDATE_LISTENER = "undo_update_listener" -def setup(hass, config: dict): - """Set up the BMW connected drive components.""" - accounts = [] - for name, account_config in config[DOMAIN].items(): - accounts.append(setup_account(account_config, hass, name)) - hass.data[DOMAIN] = accounts +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the BMW Connected Drive component from configuration.yaml.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][DATA_HASS_CONFIG] = config - def _update_all(call) -> None: - """Update all BMW accounts.""" - for cd_account in hass.data[DOMAIN]: - cd_account.update() - - # Service to manually trigger updates for all accounts. - hass.services.register(DOMAIN, SERVICE_UPDATE_STATE, _update_all) - - _update_all(None) - - for component in BMW_COMPONENTS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + if DOMAIN in config: + for entry_config in config[DOMAIN].values(): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config + ) + ) return True -def setup_account(account_config: dict, hass, name: str) -> "BMWConnectedDriveAccount": +@callback +def _async_migrate_options_from_data_if_missing(hass, entry): + data = dict(entry.data) + options = dict(entry.options) + + if CONF_READ_ONLY in data or list(options) != list(DEFAULT_OPTIONS): + options = dict(DEFAULT_OPTIONS, **options) + options[CONF_READ_ONLY] = data.pop(CONF_READ_ONLY, False) + + hass.config_entries.async_update_entry(entry, data=data, options=options) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up BMW Connected Drive from a config entry.""" + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN].setdefault(DATA_ENTRIES, {}) + + _async_migrate_options_from_data_if_missing(hass, entry) + + try: + account = await hass.async_add_executor_job( + setup_account, entry, hass, entry.data[CONF_USERNAME] + ) + except OSError as ex: + raise ConfigEntryNotReady from ex + + async def _async_update_all(service_call=None): + """Update all BMW accounts.""" + await hass.async_add_executor_job(_update_all) + + def _update_all() -> None: + """Update all BMW accounts.""" + for entry in hass.data[DOMAIN][DATA_ENTRIES].values(): + entry[CONF_ACCOUNT].update() + + # Add update listener for config entry changes (options) + undo_listener = entry.add_update_listener(update_listener) + + hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id] = { + CONF_ACCOUNT: account, + UNDO_UPDATE_LISTENER: undo_listener, + } + + # Service to manually trigger updates for all accounts. + hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, _async_update_all) + + await _async_update_all() + + for platform in BMW_PLATFORMS: + if platform != NOTIFY_DOMAIN: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + # set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + hass.async_create_task( + discovery.async_load_platform( + hass, + NOTIFY_DOMAIN, + DOMAIN, + {CONF_NAME: DOMAIN}, + hass.data[DOMAIN][DATA_HASS_CONFIG], + ) + ) + + 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 BMW_PLATFORMS + if component != NOTIFY_DOMAIN + ] + ) + ) + + # Only remove services if it is the last account and not read only + if ( + len(hass.data[DOMAIN][DATA_ENTRIES]) == 1 + and not hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id][CONF_ACCOUNT].read_only + ): + services = list(_SERVICE_MAP) + [SERVICE_UPDATE_STATE] + for service in services: + hass.services.async_remove(DOMAIN, service) + + for vehicle in hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id][ + CONF_ACCOUNT + ].account.vehicles: + hass.services.async_remove(NOTIFY_DOMAIN, slugify(f"{DOMAIN}_{vehicle.name}")) + + if unload_ok: + hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id][UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN][DATA_ENTRIES].pop(entry.entry_id) + + return unload_ok + + +async def update_listener(hass, config_entry): + """Handle options update.""" + await hass.config_entries.async_reload(config_entry.entry_id) + + +def setup_account(entry: ConfigEntry, hass, name: str) -> "BMWConnectedDriveAccount": """Set up a new BMWConnectedDriveAccount based on the config.""" - username = account_config[CONF_USERNAME] - password = account_config[CONF_PASSWORD] - region = account_config[CONF_REGION] - read_only = account_config[CONF_READ_ONLY] + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + region = entry.data[CONF_REGION] + read_only = entry.options[CONF_READ_ONLY] + use_location = entry.options[CONF_USE_LOCATION] _LOGGER.debug("Adding new account %s", name) - cd_account = BMWConnectedDriveAccount(username, password, region, name, read_only) + + pos = ( + (hass.config.latitude, hass.config.longitude) if use_location else (None, None) + ) + cd_account = BMWConnectedDriveAccount( + username, password, region, name, read_only, *pos + ) def execute_service(call): - """Execute a service for a vehicle. - - This must be a member function as we need access to the cd_account - object here. - """ + """Execute a service for a vehicle.""" vin = call.data[ATTR_VIN] - vehicle = cd_account.account.get_vehicle(vin) + vehicle = None + # Double check for read_only accounts as another account could create the services + for entry_data in [ + e + for e in hass.data[DOMAIN][DATA_ENTRIES].values() + if not e[CONF_ACCOUNT].read_only + ]: + vehicle = entry_data[CONF_ACCOUNT].account.get_vehicle(vin) + if vehicle: + break if not vehicle: _LOGGER.error("Could not find a vehicle for VIN %s", vin) return @@ -111,6 +249,9 @@ def setup_account(account_config: dict, hass, name: str) -> "BMWConnectedDriveAc second=now.second, ) + # Initialize + cd_account.update() + return cd_account @@ -118,7 +259,14 @@ class BMWConnectedDriveAccount: """Representation of a BMW vehicle.""" def __init__( - self, username: str, password: str, region_str: str, name: str, read_only + self, + username: str, + password: str, + region_str: str, + name: str, + read_only: bool, + lat=None, + lon=None, ) -> None: """Initialize account.""" region = get_region_from_name(region_str) @@ -128,6 +276,12 @@ class BMWConnectedDriveAccount: self.name = name self._update_listeners = [] + # Set observer position once for older cars to be in range for + # GPS position (pre-7/2014, <2km) and get new data from API + if lat and lon: + self.account.set_observer_position(lat, lon) + self.account.update_vehicle_states() + def update(self, *_): """Update the state of all vehicles. @@ -152,3 +306,51 @@ class BMWConnectedDriveAccount: def add_update_listener(self, listener): """Add a listener for update notifications.""" self._update_listeners.append(listener) + + +class BMWConnectedDriveBaseEntity(Entity): + """Common base for BMW entities.""" + + def __init__(self, account, vehicle): + """Initialize sensor.""" + self._account = account + self._vehicle = vehicle + self._attrs = { + "car": self._vehicle.name, + "vin": self._vehicle.vin, + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + @property + def device_info(self) -> dict: + """Return info for device registry.""" + return { + "identifiers": {(DOMAIN, self._vehicle.vin)}, + "name": f'{self._vehicle.attributes.get("brand")} {self._vehicle.name}', + "model": self._vehicle.name, + "manufacturer": self._vehicle.attributes.get("brand"), + } + + @property + def device_state_attributes(self): + """Return the state attributes of the sensor.""" + return self._attrs + + @property + def should_poll(self): + """Do not poll this class. + + Updates are triggered from BMWConnectedDriveAccount. + """ + return False + + def update_callback(self): + """Schedule a state update.""" + self.schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add callback after being added to hass. + + Show latest data after startup. + """ + self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 31ef2dacf3..cad5426d54 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,10 +9,10 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PROBLEM, BinarySensorEntity, ) -from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS +from homeassistant.const import LENGTH_KILOMETERS -from . import DOMAIN as BMW_DOMAIN -from .const import ATTRIBUTION +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) @@ -41,41 +41,40 @@ SENSOR_TYPES_ELEC = { SENSOR_TYPES_ELEC.update(SENSOR_TYPES) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the BMW sensors.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) - devices = [] - for account in accounts: - for vehicle in account.account.vehicles: - if vehicle.has_hv_battery: - _LOGGER.debug("BMW with a high voltage battery") - for key, value in sorted(SENSOR_TYPES_ELEC.items()): - if key in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1], value[2] - ) - devices.append(device) - elif vehicle.has_internal_combustion_engine: - _LOGGER.debug("BMW with an internal combustion engine") - for key, value in sorted(SENSOR_TYPES.items()): - if key in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1], value[2] - ) - devices.append(device) - add_entities(devices, True) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the BMW ConnectedDrive binary sensors from config entry.""" + account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + entities = [] + + for vehicle in account.account.vehicles: + if vehicle.has_hv_battery: + _LOGGER.debug("BMW with a high voltage battery") + for key, value in sorted(SENSOR_TYPES_ELEC.items()): + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + entities.append(device) + elif vehicle.has_internal_combustion_engine: + _LOGGER.debug("BMW with an internal combustion engine") + for key, value in sorted(SENSOR_TYPES.items()): + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + entities.append(device) + async_add_entities(entities, True) -class BMWConnectedDriveSensor(BinarySensorEntity): +class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): """Representation of a BMW vehicle binary sensor.""" def __init__( self, account, vehicle, attribute: str, sensor_name, device_class, icon ): """Initialize sensor.""" - self._account = account - self._vehicle = vehicle + super().__init__(account, vehicle) + self._attribute = attribute self._name = f"{self._vehicle.name} {self._attribute}" self._unique_id = f"{self._vehicle.vin}-{self._attribute}" @@ -84,14 +83,6 @@ class BMWConnectedDriveSensor(BinarySensorEntity): self._icon = icon self._state = None - @property - def should_poll(self) -> bool: - """Return False. - - Data update is triggered from BMWConnectedDriveEntity. - """ - return False - @property def unique_id(self): """Return the unique ID of the binary sensor.""" @@ -121,10 +112,7 @@ class BMWConnectedDriveSensor(BinarySensorEntity): def device_state_attributes(self): """Return the state attributes of the binary sensor.""" vehicle_state = self._vehicle.state - result = { - "car": self._vehicle.name, - ATTR_ATTRIBUTION: ATTRIBUTION, - } + result = self._attrs.copy() if self._attribute == "lids": for lid in vehicle_state.lids: @@ -205,14 +193,3 @@ class BMWConnectedDriveSensor(BinarySensorEntity): f"{service_type} distance" ] = f"{distance} {self.hass.config.units.length_unit}" return result - - def update_callback(self): - """Schedule a state update.""" - self.schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Add callback after being added to hass. - - Show latest data after startup. - """ - self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py new file mode 100644 index 0000000000..a6081d5ccc --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -0,0 +1,119 @@ +"""Config flow for BMW ConnectedDrive integration.""" +import logging + +from bimmer_connected.account import ConnectedDriveAccount +from bimmer_connected.country_selector import get_region_from_name +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME +from homeassistant.core import callback + +from . import DOMAIN # pylint: disable=unused-import +from .const import CONF_ALLOWED_REGIONS, CONF_READ_ONLY, CONF_REGION, CONF_USE_LOCATION + +_LOGGER = logging.getLogger(__name__) + + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_REGION): vol.In(CONF_ALLOWED_REGIONS), + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + try: + await hass.async_add_executor_job( + ConnectedDriveAccount, + data[CONF_USERNAME], + data[CONF_PASSWORD], + get_region_from_name(data[CONF_REGION]), + ) + except OSError as ex: + raise CannotConnect from ex + + # Return info that you want to store in the config entry. + return {"title": f"{data[CONF_USERNAME]}{data.get(CONF_SOURCE, '')}"} + + +class BMWConnectedDriveConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for BMW ConnectedDrive.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}" + + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + info = None + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + + if info: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Return a BWM ConnectedDrive option flow.""" + return BMWConnectedDriveOptionsFlow(config_entry) + + +class BMWConnectedDriveOptionsFlow(config_entries.OptionsFlow): + """Handle a option flow for BMW ConnectedDrive.""" + + def __init__(self, config_entry): + """Initialize BMW ConnectedDrive option flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the options.""" + return await self.async_step_account_options() + + async def async_step_account_options(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + return self.async_show_form( + step_id="account_options", + data_schema=vol.Schema( + { + vol.Optional( + CONF_READ_ONLY, + default=self.config_entry.options.get(CONF_READ_ONLY, False), + ): bool, + vol.Optional( + CONF_USE_LOCATION, + default=self.config_entry.options.get(CONF_USE_LOCATION, False), + ): bool, + } + ), + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index d1a44b5e5c..65dc7fde59 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -1,2 +1,12 @@ """Const file for the BMW Connected Drive integration.""" ATTRIBUTION = "Data provided by BMW Connected Drive" + +CONF_REGION = "region" +CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] +CONF_READ_ONLY = "read_only" +CONF_USE_LOCATION = "use_location" + +CONF_ACCOUNT = "account" + +DATA_HASS_CONFIG = "hass_config" +DATA_ENTRIES = "entries" diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index fa732b64e7..7f069e741b 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -1,51 +1,83 @@ """Device tracker for BMW Connected Drive vehicles.""" import logging -from homeassistant.util import slugify +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity -from . import DOMAIN as BMW_DOMAIN +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the BMW tracker.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) - for account in accounts: - for vehicle in account.account.vehicles: - tracker = BMWDeviceTracker(see, vehicle) - account.add_update_listener(tracker.update) - tracker.update() - return True +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the BMW ConnectedDrive tracker from config entry.""" + account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + entities = [] + + for vehicle in account.account.vehicles: + entities.append(BMWDeviceTracker(account, vehicle)) + if not vehicle.state.is_vehicle_tracking_enabled: + _LOGGER.info( + "Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown", + vehicle.name, + vehicle.vin, + ) + async_add_entities(entities, True) -class BMWDeviceTracker: +class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): """BMW Connected Drive device tracker.""" - def __init__(self, see, vehicle): + def __init__(self, account, vehicle): """Initialize the Tracker.""" - self._see = see - self.vehicle = vehicle + super().__init__(account, vehicle) - def update(self) -> None: - """Update the device info. - - Only update the state in Home Assistant if tracking in - the car is enabled. - """ - dev_id = slugify(self.vehicle.name) - - if not self.vehicle.state.is_vehicle_tracking_enabled: - _LOGGER.debug("Tracking is disabled for vehicle %s", dev_id) - return - - _LOGGER.debug("Updating %s", dev_id) - attrs = {"vin": self.vehicle.vin} - self._see( - dev_id=dev_id, - host_name=self.vehicle.name, - gps=self.vehicle.state.gps_position, - attributes=attrs, - icon="mdi:car", + self._unique_id = vehicle.vin + self._location = ( + vehicle.state.gps_position if vehicle.state.gps_position else (None, None) + ) + self._name = vehicle.name + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._location[0] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._location[1] + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:car" + + @property + def force_update(self): + """All updates do not need to be written to the state machine.""" + return False + + def update(self): + """Update state of the decvice tracker.""" + self._location = ( + self._vehicle.state.gps_position + if self._vehicle.state.is_vehicle_tracking_enabled + else (None, None) ) diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index d30f1702ae..0d281e78f1 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -4,35 +4,34 @@ import logging from bimmer_connected.state import LockState from homeassistant.components.lock import LockEntity -from homeassistant.const import ATTR_ATTRIBUTION, STATE_LOCKED, STATE_UNLOCKED +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED -from . import DOMAIN as BMW_DOMAIN -from .const import ATTRIBUTION +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from .const import CONF_ACCOUNT, DATA_ENTRIES DOOR_LOCK_STATE = "door_lock_state" _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the BMW Connected Drive lock.""" - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) - devices = [] - for account in accounts: - if not account.read_only: - for vehicle in account.account.vehicles: - device = BMWLock(account, vehicle, "lock", "BMW lock") - devices.append(device) - add_entities(devices, True) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the BMW ConnectedDrive binary sensors from config entry.""" + account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + entities = [] + + if not account.read_only: + for vehicle in account.account.vehicles: + device = BMWLock(account, vehicle, "lock", "BMW lock") + entities.append(device) + async_add_entities(entities, True) -class BMWLock(LockEntity): +class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): """Representation of a BMW vehicle lock.""" def __init__(self, account, vehicle, attribute: str, sensor_name): """Initialize the lock.""" - self._account = account - self._vehicle = vehicle + super().__init__(account, vehicle) + self._attribute = attribute self._name = f"{self._vehicle.name} {self._attribute}" self._unique_id = f"{self._vehicle.vin}-{self._attribute}" @@ -42,14 +41,6 @@ class BMWLock(LockEntity): DOOR_LOCK_STATE in self._vehicle.available_attributes ) - @property - def should_poll(self): - """Do not poll this class. - - Updates are triggered from BMWConnectedDriveAccount. - """ - return False - @property def unique_id(self): """Return the unique ID of the lock.""" @@ -64,10 +55,8 @@ class BMWLock(LockEntity): def device_state_attributes(self): """Return the state attributes of the lock.""" vehicle_state = self._vehicle.state - result = { - "car": self._vehicle.name, - ATTR_ATTRIBUTION: ATTRIBUTION, - } + result = self._attrs.copy() + if self.door_lock_state_available: result["door_lock_state"] = vehicle_state.door_lock_state.value result["last_update_reason"] = vehicle_state.last_update_reason @@ -76,7 +65,11 @@ class BMWLock(LockEntity): @property def is_locked(self): """Return true if lock is locked.""" - return self._state == STATE_LOCKED + if self.door_lock_state_available: + result = self._state == STATE_LOCKED + else: + result = None + return result def lock(self, **kwargs): """Lock the car.""" @@ -107,14 +100,3 @@ class BMWLock(LockEntity): if vehicle_state.door_lock_state in [LockState.LOCKED, LockState.SECURED] else STATE_UNLOCKED ) - - def update_callback(self): - """Schedule a state update.""" - self.schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Add callback after being added to hass. - - Show latest data after startup. - """ - self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index cb17459e10..5bce904e1c 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -3,5 +3,6 @@ "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "requirements": ["bimmer_connected==0.7.13"], - "codeowners": ["@gerard33", "@rikroe"] + "codeowners": ["@gerard33", "@rikroe"], + "config_flow": true } diff --git a/homeassistant/components/bmw_connected_drive/notify.py b/homeassistant/components/bmw_connected_drive/notify.py index 9cf2bca2df..3fd40f3801 100644 --- a/homeassistant/components/bmw_connected_drive/notify.py +++ b/homeassistant/components/bmw_connected_drive/notify.py @@ -11,6 +11,7 @@ from homeassistant.components.notify import ( from homeassistant.const import ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, ATTR_NAME from . import DOMAIN as BMW_DOMAIN +from .const import CONF_ACCOUNT, DATA_ENTRIES ATTR_LAT = "lat" ATTR_LOCATION_ATTRIBUTES = ["street", "city", "postal_code", "country"] @@ -23,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) def get_service(hass, config, discovery_info=None): """Get the BMW notification service.""" - accounts = hass.data[BMW_DOMAIN] + accounts = [e[CONF_ACCOUNT] for e in hass.data[BMW_DOMAIN][DATA_ENTRIES].values()] _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) svc = BMWNotificationService() svc.setup(accounts) diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 4668b1da6e..480aac34eb 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -4,7 +4,6 @@ import logging from bimmer_connected.state import ChargingState from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, @@ -16,8 +15,8 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -from . import DOMAIN as BMW_DOMAIN -from .const import ATTRIBUTION +from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity +from .const import CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) @@ -48,48 +47,39 @@ ATTR_TO_HA_IMPERIAL = { } -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the BMW sensors.""" +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the BMW ConnectedDrive sensors from config entry.""" if hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: attribute_info = ATTR_TO_HA_IMPERIAL else: attribute_info = ATTR_TO_HA_METRIC - accounts = hass.data[BMW_DOMAIN] - _LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts])) - devices = [] - for account in accounts: - for vehicle in account.account.vehicles: - for attribute_name in vehicle.drive_train_attributes: - if attribute_name in vehicle.available_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info - ) - devices.append(device) - add_entities(devices, True) + account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT] + entities = [] + + for vehicle in account.account.vehicles: + for attribute_name in vehicle.drive_train_attributes: + if attribute_name in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info + ) + entities.append(device) + async_add_entities(entities, True) -class BMWConnectedDriveSensor(Entity): +class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, Entity): """Representation of a BMW vehicle sensor.""" def __init__(self, account, vehicle, attribute: str, attribute_info): """Initialize BMW vehicle sensor.""" - self._vehicle = vehicle - self._account = account + super().__init__(account, vehicle) + self._attribute = attribute self._state = None self._name = f"{self._vehicle.name} {self._attribute}" self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attribute_info = attribute_info - @property - def should_poll(self) -> bool: - """Return False. - - Data update is triggered from BMWConnectedDriveEntity. - """ - return False - @property def unique_id(self): """Return the unique ID of the sensor.""" @@ -128,14 +118,6 @@ class BMWConnectedDriveSensor(Entity): unit = self._attribute_info.get(self._attribute, [None, None])[1] return unit - @property - def device_state_attributes(self): - """Return the state attributes of the sensor.""" - return { - "car": self._vehicle.name, - ATTR_ATTRIBUTION: ATTRIBUTION, - } - def update(self) -> None: """Read new state data from the library.""" _LOGGER.debug("Updating %s", self._vehicle.name) @@ -152,14 +134,3 @@ class BMWConnectedDriveSensor(Entity): self._state = round(value_converted) else: self._state = getattr(vehicle_state, self._attribute) - - def update_callback(self): - """Schedule a state update.""" - self.schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Add callback after being added to hass. - - Show latest data after startup. - """ - self._account.add_update_listener(self.update_callback) diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json new file mode 100644 index 0000000000..c0c45b814a --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "region": "ConnectedDrive Region" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Read-only (only sensors and notify, no execution of services, no lock)", + "use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)" + } + } + } + } +} diff --git a/homeassistant/components/bmw_connected_drive/translations/en.json b/homeassistant/components/bmw_connected_drive/translations/en.json new file mode 100644 index 0000000000..f194c8a344 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Account is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "password": "Password", + "read_only": "Read-only", + "region": "ConnectedDrive Region", + "username": "Username" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Read-only (only sensors and notify, no execution of services, no lock)", + "use_location": "Use Home Assistant location for car location polls (required for non i3/i8 vehicles produced before 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json index cbb42aee92..af652c5450 100644 --- a/homeassistant/components/bond/translations/zh-Hant.json +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "old_firmware": "Bond \u8a2d\u5099\u4f7f\u7528\u4e0d\u652f\u63f4\u7684\u820a\u7248\u672c\u97cc\u9ad4 - \u8acb\u66f4\u65b0\u5f8c\u518d\u7e7c\u7e8c", + "old_firmware": "Bond \u88dd\u7f6e\u4f7f\u7528\u4e0d\u652f\u63f4\u7684\u820a\u7248\u672c\u97cc\u9ad4 - \u8acb\u66f4\u65b0\u5f8c\u518d\u7e7c\u7e8c", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "Bond\uff1a{bond_id} ({host})", diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index fc725b11ad..0c960a850e 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -12,7 +12,7 @@ "step": { "authorize": { "data": { - "pin": "PIN-kode" + "pin": "PIN kode" }, "description": "Angi PIN-koden som vises p\u00e5 Sony Bravia TV. \n\nHvis PIN-koden ikke vises, m\u00e5 du avregistrere Home Assistant p\u00e5 TV-en, g\u00e5 til: Innstillinger -> Nettverk -> Innstillinger for ekstern enhet -> Avregistrere ekstern enhet.", "title": "Godkjenn Sony Bravia TV" diff --git a/homeassistant/components/braviatv/translations/pt.json b/homeassistant/components/braviatv/translations/pt.json index 9d37ff831d..5e5f1367f5 100644 --- a/homeassistant/components/braviatv/translations/pt.json +++ b/homeassistant/components/braviatv/translations/pt.json @@ -10,6 +10,9 @@ }, "step": { "authorize": { + "data": { + "pin": "C\u00f3digo PIN" + }, "description": "Digite o c\u00f3digo PIN mostrado na TV Sony Bravia. \n\nSe o c\u00f3digo PIN n\u00e3o for exibido, \u00e9 necess\u00e1rio cancelar o registro do Home Assistant na TV, v\u00e1 para: Configura\u00e7\u00f5es -> Rede -> Configura\u00e7\u00f5es do dispositivo remoto -> Cancelar registro do dispositivo remoto.", "title": "Autorizar TV Sony Bravia" }, diff --git a/homeassistant/components/braviatv/translations/zh-Hant.json b/homeassistant/components/braviatv/translations/zh-Hant.json index eafe98f154..53dc9ead65 100644 --- a/homeassistant/components/braviatv/translations/zh-Hant.json +++ b/homeassistant/components/braviatv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_ip_control": "\u96fb\u8996\u4e0a\u7684 IP \u5df2\u95dc\u9589\u6216\u4e0d\u652f\u63f4\u6b64\u6b3e\u96fb\u8996\u3002" }, "error": { diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index e34db6d726..f915040635 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -1,11 +1,15 @@ { "config": { "abort": { + "cannot_connect": "Verbindungsfehler", "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", - "not_supported": "Ger\u00e4t nicht unterst\u00fctzt" + "not_supported": "Ger\u00e4t nicht unterst\u00fctzt", + "unknown": "Unerwarteter Fehler" }, "error": { - "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse" + "cannot_connect": "Verbindungsfehler", + "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", + "unknown": "Unerwarteter Fehler" }, "step": { "auth": { diff --git a/homeassistant/components/broadlink/translations/no.json b/homeassistant/components/broadlink/translations/no.json index 8c72e9d92f..d64fedecc5 100644 --- a/homeassistant/components/broadlink/translations/no.json +++ b/homeassistant/components/broadlink/translations/no.json @@ -16,7 +16,7 @@ "flow_title": "{name} ({model} p\u00e5 {host})", "step": { "auth": { - "title": "Autentiser til enheten" + "title": "Godkjenning til enheten" }, "finish": { "data": { @@ -25,14 +25,14 @@ "title": "Velg et navn p\u00e5 enheten" }, "reset": { - "description": "{name} ( {model} p\u00e5 {host} ) er l\u00e5st. Du m\u00e5 l\u00e5se opp enheten for \u00e5 autentisere og fullf\u00f8re konfigurasjonen. Bruksanvisning:\n 1. \u00c5pne Broadlink-appen.\n 2. Klikk p\u00e5 enheten.\n 3. Klikk p\u00e5 `...` \u00f8verst til h\u00f8yre.\n 4. Bla til bunnen av siden.\n 5. Deaktiver l\u00e5sen.", + "description": "{name} ({model} p\u00e5 {host}) er l\u00e5st. Du m\u00e5 l\u00e5se opp enheten for \u00e5 godkjenne og fullf\u00f8re konfigurasjonen. Bruksanvisning:\n 1. \u00c5pne Broadlink-appen\n 2. Klikk p\u00e5 enheten\n 3. Klikk p\u00e5 `...` \u00f8verst til h\u00f8yre\n 4. Bla til bunnen av siden\n 5. Deaktiver l\u00e5sen", "title": "L\u00e5s opp enheten" }, "unlock": { "data": { "unlock": "Ja, gj\u00f8r det." }, - "description": "{name} ( {model} p\u00e5 {host} ) er l\u00e5st. Dette kan f\u00f8re til autentiseringsproblemer i Home Assistant. Vil du l\u00e5se opp den?", + "description": "{name} ({model} p\u00e5 {host}) er l\u00e5st. Dette kan f\u00f8re til godkjenningsproblemer i Home Assistant. Vil du l\u00e5se den opp?", "title": "L\u00e5s opp enheten (valgfritt)" }, "user": { diff --git a/homeassistant/components/broadlink/translations/pt.json b/homeassistant/components/broadlink/translations/pt.json index bf246b55b3..45fb03ad04 100644 --- a/homeassistant/components/broadlink/translations/pt.json +++ b/homeassistant/components/broadlink/translations/pt.json @@ -2,11 +2,14 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unknown": "Erro inesperado" }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", "unknown": "Erro inesperado" }, "flow_title": "{name} ({model} em {host})", diff --git a/homeassistant/components/broadlink/translations/zh-Hant.json b/homeassistant/components/broadlink/translations/zh-Hant.json index 8781b90c3d..2e0864c9f7 100644 --- a/homeassistant/components/broadlink/translations/zh-Hant.json +++ b/homeassistant/components/broadlink/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740", - "not_supported": "\u8a2d\u5099\u4e0d\u652f\u63f4", + "not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { @@ -16,31 +16,31 @@ "flow_title": "{name}\uff08\u4f4d\u65bc {host} \u4e4b {model} \uff09", "step": { "auth": { - "title": "\u8a8d\u8b49\u8a2d\u5099" + "title": "\u8a8d\u8b49\u88dd\u7f6e" }, "finish": { "data": { "name": "\u540d\u7a31" }, - "title": "\u9078\u64c7\u8a2d\u5099\u540d\u7a31" + "title": "\u9078\u64c7\u88dd\u7f6e\u540d\u7a31" }, "reset": { - "description": "{name}\uff08\u4f4d\u65bc {host} \u7684 {model}\uff09\u8a2d\u5099\u5df2\u9396\u5b9a\uff0c\u9700\u8981\u89e3\u9396\u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u8207\u5b8c\u6210\u8a2d\u5b9a\uff0c\u8acb\u8ddf\u96a8\u6307\u793a\uff1a\n1. \u958b\u555f Broadlink App\u3002\n2. \u9ede\u9078\u8a2d\u5099\u3002\n3. \u9ede\u9078\u53f3\u4e0a\u65b9\u7684 `...`\u3002\n4. \u6372\u52d5\u81f3\u6700\u5e95\u9801\u3002\n5. \u95dc\u9589\u9396\u5b9a\u3002", - "title": "\u89e3\u9396\u8a2d\u5099" + "description": "{name}\uff08\u4f4d\u65bc {host} \u7684 {model}\uff09\u88dd\u7f6e\u5df2\u9396\u5b9a\uff0c\u9700\u8981\u89e3\u9396\u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u8207\u5b8c\u6210\u8a2d\u5b9a\uff0c\u8acb\u8ddf\u96a8\u6307\u793a\uff1a\n1. \u958b\u555f Broadlink App\u3002\n2. \u9ede\u9078\u88dd\u7f6e\u3002\n3. \u9ede\u9078\u53f3\u4e0a\u65b9\u7684 `...`\u3002\n4. \u6372\u52d5\u81f3\u6700\u5e95\u9801\u3002\n5. \u95dc\u9589\u9396\u5b9a\u3002", + "title": "\u89e3\u9396\u88dd\u7f6e" }, "unlock": { "data": { "unlock": "\u662f\uff0c\u57f7\u884c\u3002" }, - "description": "{name}\uff08\u4f4d\u65bc {host} \u7684 {model}\uff09\u8a2d\u5099\u5df2\u9396\u5b9a\uff0c\u53ef\u80fd\u5c0e\u81f4 Home Assistant \u8a8d\u8b49\u554f\u984c\uff0c\u662f\u5426\u8981\u89e3\u9396\uff1f", - "title": "\u89e3\u9396\u8a2d\u5099\uff08\u9078\u9805\uff09" + "description": "{name}\uff08\u4f4d\u65bc {host} \u7684 {model}\uff09\u88dd\u7f6e\u5df2\u9396\u5b9a\uff0c\u53ef\u80fd\u5c0e\u81f4 Home Assistant \u8a8d\u8b49\u554f\u984c\uff0c\u662f\u5426\u8981\u89e3\u9396\uff1f", + "title": "\u89e3\u9396\u88dd\u7f6e\uff08\u9078\u9805\uff09" }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", "timeout": "\u903e\u6642" }, - "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" } } } diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 0e534147cb..9bb9ba0026 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "codeowners": ["@bieniu"], "requirements": ["brother==0.1.20"], - "zeroconf": [{"type": "_printer._tcp.local.", "name":"Brother*"}], + "zeroconf": [{ "type": "_printer._tcp.local.", "name": "brother*" }], "config_flow": true, "quality_scale": "platinum" } diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index 4c07d1a299..72bd052cc1 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -5,6 +5,7 @@ "unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt." }, "error": { + "cannot_connect": "Verbindungsfehler", "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" }, diff --git a/homeassistant/components/brother/translations/pt.json b/homeassistant/components/brother/translations/pt.json index 5e4c740d66..f9c19c6be3 100644 --- a/homeassistant/components/brother/translations/pt.json +++ b/homeassistant/components/brother/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "wrong_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." }, "step": { @@ -9,6 +13,11 @@ "host": "Servidor", "type": "Tipo de impressora" } + }, + "zeroconf_confirm": { + "data": { + "type": "Tipo da impressora" + } } } } diff --git a/homeassistant/components/brother/translations/zh-Hant.json b/homeassistant/components/brother/translations/zh-Hant.json index 79dc4c81b2..d8208e6ce4 100644 --- a/homeassistant/components/brother/translations/zh-Hant.json +++ b/homeassistant/components/brother/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u5370\u8868\u6a5f\u3002" }, "error": { diff --git a/homeassistant/components/bsblan/translations/ca.json b/homeassistant/components/bsblan/translations/ca.json index 0cce690257..e217787ba1 100644 --- a/homeassistant/components/bsblan/translations/ca.json +++ b/homeassistant/components/bsblan/translations/ca.json @@ -12,7 +12,9 @@ "data": { "host": "Amfitri\u00f3", "passkey": "String Passkey", - "port": "Port" + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" }, "description": "Configura un dispositiu BSB-Lan per a integrar-lo amb Home Assistant.", "title": "Connexi\u00f3 amb dispositiu BSB-Lan" diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index 39be96b84d..5fd61c0bfe 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -3,11 +3,16 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { "host": "Host", - "port": "Port Nummer" + "password": "Passwort", + "port": "Port Nummer", + "username": "Benutzername" } } } diff --git a/homeassistant/components/bsblan/translations/fr.json b/homeassistant/components/bsblan/translations/fr.json index d650d6596f..0c54aecdd8 100644 --- a/homeassistant/components/bsblan/translations/fr.json +++ b/homeassistant/components/bsblan/translations/fr.json @@ -12,7 +12,9 @@ "data": { "host": "Nom d'h\u00f4te ou adresse IP", "passkey": "Cha\u00eene de cl\u00e9 d'acc\u00e8s", - "port": "Port" + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" }, "description": "Configurez votre appareil BSB-Lan pour l'int\u00e9grer \u00e0 HomeAssistant.", "title": "Connectez-vous \u00e0 l'appareil BSB-Lan" diff --git a/homeassistant/components/bsblan/translations/it.json b/homeassistant/components/bsblan/translations/it.json index 1f27531f76..3eb7feec61 100644 --- a/homeassistant/components/bsblan/translations/it.json +++ b/homeassistant/components/bsblan/translations/it.json @@ -12,7 +12,9 @@ "data": { "host": "Host", "passkey": "Stringa passkey", - "port": "Porta" + "password": "Password", + "port": "Porta", + "username": "Nome utente" }, "description": "Configura il tuo dispositivo BSB-Lan per l'integrazione con Home Assistant.", "title": "Collegamento al dispositivo BSB-Lan" diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index f681da4210..5461f20737 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -1,10 +1,18 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { "host": "Servidor", - "port": "Porta" + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/bsblan/translations/sl.json b/homeassistant/components/bsblan/translations/sl.json index 2bf2dd68b4..8eaa5185eb 100644 --- a/homeassistant/components/bsblan/translations/sl.json +++ b/homeassistant/components/bsblan/translations/sl.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "port": "Vrata" + "password": "Geslo", + "port": "Vrata", + "username": "Uporabni\u0161ko ime" } } } diff --git a/homeassistant/components/bsblan/translations/tr.json b/homeassistant/components/bsblan/translations/tr.json new file mode 100644 index 0000000000..94acde2d0a --- /dev/null +++ b/homeassistant/components/bsblan/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/zh-Hant.json b/homeassistant/components/bsblan/translations/zh-Hant.json index 7ada76c1d2..3fefe08f98 100644 --- a/homeassistant/components/bsblan/translations/zh-Hant.json +++ b/homeassistant/components/bsblan/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -16,8 +16,8 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8a2d\u5b9a BSB-Lan \u8a2d\u5099\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", - "title": "\u9023\u7dda\u81f3 BSB-Lan \u8a2d\u5099" + "description": "\u8a2d\u5b9a BSB-Lan \u88dd\u7f6e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7dda\u81f3 BSB-Lan \u88dd\u7f6e" } } } diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index ae182c62dc..ec35a44840 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,11 +1,12 @@ """Preference management for camera component.""" +from homeassistant.helpers.typing import UNDEFINED + from .const import DOMAIN, PREF_PRELOAD_STREAM # mypy: allow-untyped-defs, no-check-untyped-defs STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -_UNDEF = object() class CameraEntityPreferences: @@ -44,14 +45,14 @@ class CameraPreferences: self._prefs = prefs async def async_update( - self, entity_id, *, preload_stream=_UNDEF, stream_options=_UNDEF + self, entity_id, *, preload_stream=UNDEFINED, stream_options=UNDEFINED ): """Update camera preferences.""" if not self._prefs.get(entity_id): self._prefs[entity_id] = {} for key, value in ((PREF_PRELOAD_STREAM, preload_stream),): - if value is not _UNDEF: + if value is not UNDEFINED: self._prefs[entity_id][key] = value await self._store.async_save(self._prefs) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index fd2f08c148..0493a964cc 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -127,11 +127,14 @@ class CanaryCamera(CoordinatorEntity, Camera): async def async_camera_image(self): """Return a still image response from the camera.""" await self.hass.async_add_executor_job(self.renew_live_stream_session) + live_stream_url = await self.hass.async_add_executor_job( + getattr, self._live_stream_session, "live_stream_url" + ) ffmpeg = ImageFrame(self._ffmpeg.binary) image = await asyncio.shield( ffmpeg.get_image( - self._live_stream_session.live_stream_url, + live_stream_url, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments, ) diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index b4598d6408..af6b0ce54b 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -2,7 +2,7 @@ "domain": "canary", "name": "Canary", "documentation": "https://www.home-assistant.io/integrations/canary", - "requirements": ["py-canary==0.5.0"], + "requirements": ["py-canary==0.5.1"], "dependencies": ["ffmpeg"], "codeowners": [], "config_flow": true diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json index 159f961c3a..eebc9bd5fc 100644 --- a/homeassistant/components/canary/translations/de.json +++ b/homeassistant/components/canary/translations/de.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindungsfehler" + }, "flow_title": "Canary: {name}", "step": { "user": { diff --git a/homeassistant/components/canary/translations/pt.json b/homeassistant/components/canary/translations/pt.json new file mode 100644 index 0000000000..e328e4f580 --- /dev/null +++ b/homeassistant/components/canary/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/zh-Hant.json b/homeassistant/components/canary/translations/zh-Hant.json index 07463bc8a1..c53ffd8327 100644 --- a/homeassistant/components/canary/translations/zh-Hant.json +++ b/homeassistant/components/canary/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 3a795b6042..8072e06c2e 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.5.1"], + "requirements": ["pychromecast==7.6.0"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b76dbcaf20..e68800efb4 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -665,9 +665,7 @@ class CastDevice(MediaPlayerEntity): images = media_status.images - return ( - images[0].url.replace("http://", "//") if images and images[0].url else None - ) + return images[0].url if images and images[0].url else None @property def media_image_remotely_accessible(self) -> bool: diff --git a/homeassistant/components/cast/translations/zh-Hant.json b/homeassistant/components/cast/translations/zh-Hant.json index 91a0dc60be..90c98e491d 100644 --- a/homeassistant/components/cast/translations/zh-Hant.json +++ b/homeassistant/components/cast/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/cert_expiry/translations/pt.json b/homeassistant/components/cert_expiry/translations/pt.json index af42481b25..9f00493666 100644 --- a/homeassistant/components/cert_expiry/translations/pt.json +++ b/homeassistant/components/cert_expiry/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { + "connection_timeout": "Tempo excedido a tentar ligar ao servidor.", "resolve_failed": "N\u00e3o \u00e9 possivel resolver o servidor" }, "step": { @@ -11,5 +15,6 @@ } } } - } + }, + "title": "Validade do Certificado" } \ No newline at end of file diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index 972903e53e..b34daaa6d1 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -2,6 +2,6 @@ "domain": "cisco_mobility_express", "name": "Cisco Mobility Express", "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", - "requirements": ["ciscomobilityexpress==0.3.3"], + "requirements": ["ciscomobilityexpress==0.3.9"], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 1bb74053ea..7abbefe85f 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -5,7 +5,7 @@ import logging import aiohttp import async_timeout -from hass_nabucasa import cloud_api +from hass_nabucasa import Cloud, cloud_api from homeassistant.components.alexa import ( config as alexa_config, @@ -14,7 +14,7 @@ from homeassistant.components.alexa import ( state_report as alexa_state_report, ) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST -from homeassistant.core import callback, split_entity_id +from homeassistant.core import HomeAssistant, callback, split_entity_id from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow @@ -32,10 +32,18 @@ SYNC_DELAY = 1 class AlexaConfig(alexa_config.AbstractConfig): """Alexa Configuration.""" - def __init__(self, hass, config, prefs: CloudPreferences, cloud): + def __init__( + self, + hass: HomeAssistant, + config: dict, + cloud_user: str, + prefs: CloudPreferences, + cloud: Cloud, + ): """Initialize the Alexa config.""" super().__init__(hass) self._config = config + self._cloud_user = cloud_user self._prefs = prefs self._cloud = cloud self._token = None @@ -85,6 +93,11 @@ class AlexaConfig(alexa_config.AbstractConfig): """Return entity config.""" return self._config.get(CONF_ENTITY_CONFIG) or {} + @callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return self._cloud_user + def should_expose(self, entity_id): """If an entity should be exposed.""" if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES: diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 2a2d383f36..155a39e49b 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -79,13 +79,15 @@ class CloudClient(Interface): """Return true if we want start a remote connection.""" return self._prefs.remote_enabled - @property - def alexa_config(self) -> alexa_config.AlexaConfig: + async def get_alexa_config(self) -> alexa_config.AlexaConfig: """Return Alexa config.""" if self._alexa_config is None: assert self.cloud is not None + + cloud_user = await self._prefs.get_cloud_user() + self._alexa_config = alexa_config.AlexaConfig( - self._hass, self.alexa_user_config, self._prefs, self.cloud + self._hass, self.alexa_user_config, cloud_user, self._prefs, self.cloud ) return self._alexa_config @@ -110,8 +112,9 @@ class CloudClient(Interface): async def enable_alexa(_): """Enable Alexa.""" + aconf = await self.get_alexa_config() try: - await self.alexa_config.async_enable_proactive_mode() + await aconf.async_enable_proactive_mode() except aiohttp.ClientError as err: # If no internet available yet if self._hass.is_running: logging.getLogger(__package__).warning( @@ -133,7 +136,7 @@ class CloudClient(Interface): tasks = [] - if self.alexa_config.enabled and self.alexa_config.should_report_state: + if self._prefs.alexa_enabled and self._prefs.alexa_report_state: tasks.append(enable_alexa) if self._prefs.google_enabled: @@ -164,9 +167,10 @@ class CloudClient(Interface): async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]: """Process cloud alexa message to client.""" cloud_user = await self._prefs.get_cloud_user() + aconfig = await self.get_alexa_config() return await alexa_sh.async_handle_message( self._hass, - self.alexa_config, + aconfig, payload, context=Context(user_id=cloud_user), enabled=self._prefs.alexa_enabled, diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 4b5891359b..2ac0bc4025 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__) class CloudGoogleConfig(AbstractConfig): """HA Cloud Configuration for Google Assistant.""" - def __init__(self, hass, config, cloud_user, prefs: CloudPreferences, cloud): + def __init__(self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud): """Initialize the Google config.""" super().__init__(hass) self._config = config diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index 3075f6a3f9..a4d8b84b1a 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -397,9 +397,10 @@ async def websocket_update_prefs(hass, connection, msg): # If we turn alexa linking on, validate that we can fetch access token if changes.get(PREF_ALEXA_REPORT_STATE): + alexa_config = await cloud.client.get_alexa_config() try: with async_timeout.timeout(10): - await cloud.client.alexa_config.async_get_access_token() + await alexa_config.async_get_access_token() except asyncio.TimeoutError: connection.send_error( msg["id"], "alexa_timeout", "Timeout validating Alexa access token." @@ -555,7 +556,8 @@ async def google_assistant_update(hass, connection, msg): async def alexa_list(hass, connection, msg): """List all alexa entities.""" cloud = hass.data[DOMAIN] - entities = alexa_entities.async_get_entities(hass, cloud.client.alexa_config) + alexa_config = await cloud.client.get_alexa_config() + entities = alexa_entities.async_get_entities(hass, alexa_config) result = [] @@ -603,10 +605,11 @@ async def alexa_update(hass, connection, msg): async def alexa_sync(hass, connection, msg): """Sync with Alexa.""" cloud = hass.data[DOMAIN] + alexa_config = await cloud.client.get_alexa_config() with async_timeout.timeout(10): try: - success = await cloud.client.alexa_config.async_sync_entities() + success = await alexa_config.async_sync_entities() except alexa_errors.NoTokenAvailable: connection.send_error( msg["id"], diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 0a41f8e2a8..6e0e78839c 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -5,6 +5,7 @@ from typing import List, Optional from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.auth.models import User from homeassistant.core import callback +from homeassistant.helpers.typing import UNDEFINED from homeassistant.util.logging import async_create_catching_coro from .const import ( @@ -36,7 +37,6 @@ from .const import ( STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 -_UNDEF = object() class CloudPreferences: @@ -74,18 +74,18 @@ class CloudPreferences: async def async_update( self, *, - google_enabled=_UNDEF, - alexa_enabled=_UNDEF, - remote_enabled=_UNDEF, - google_secure_devices_pin=_UNDEF, - cloudhooks=_UNDEF, - cloud_user=_UNDEF, - google_entity_configs=_UNDEF, - alexa_entity_configs=_UNDEF, - alexa_report_state=_UNDEF, - google_report_state=_UNDEF, - alexa_default_expose=_UNDEF, - google_default_expose=_UNDEF, + google_enabled=UNDEFINED, + alexa_enabled=UNDEFINED, + remote_enabled=UNDEFINED, + google_secure_devices_pin=UNDEFINED, + cloudhooks=UNDEFINED, + cloud_user=UNDEFINED, + google_entity_configs=UNDEFINED, + alexa_entity_configs=UNDEFINED, + alexa_report_state=UNDEFINED, + google_report_state=UNDEFINED, + alexa_default_expose=UNDEFINED, + google_default_expose=UNDEFINED, ): """Update user preferences.""" prefs = {**self._prefs} @@ -104,7 +104,7 @@ class CloudPreferences: (PREF_ALEXA_DEFAULT_EXPOSE, alexa_default_expose), (PREF_GOOGLE_DEFAULT_EXPOSE, google_default_expose), ): - if value is not _UNDEF: + if value is not UNDEFINED: prefs[key] = value if remote_enabled is True and self._has_local_trusted_network: @@ -121,10 +121,10 @@ class CloudPreferences: self, *, entity_id, - override_name=_UNDEF, - disable_2fa=_UNDEF, - aliases=_UNDEF, - should_expose=_UNDEF, + override_name=UNDEFINED, + disable_2fa=UNDEFINED, + aliases=UNDEFINED, + should_expose=UNDEFINED, ): """Update config for a Google entity.""" entities = self.google_entity_configs @@ -137,7 +137,7 @@ class CloudPreferences: (PREF_ALIASES, aliases), (PREF_SHOULD_EXPOSE, should_expose), ): - if value is not _UNDEF: + if value is not UNDEFINED: changes[key] = value if not changes: @@ -149,7 +149,7 @@ class CloudPreferences: await self.async_update(google_entity_configs=updated_entities) async def async_update_alexa_entity_config( - self, *, entity_id, should_expose=_UNDEF + self, *, entity_id, should_expose=UNDEFINED ): """Update config for an Alexa entity.""" entities = self.alexa_entity_configs @@ -157,7 +157,7 @@ class CloudPreferences: changes = {} for key, value in ((PREF_SHOULD_EXPOSE, should_expose),): - if value is not _UNDEF: + if value is not UNDEFINED: changes[key] = value if not changes: diff --git a/homeassistant/components/cloud/translations/et.json b/homeassistant/components/cloud/translations/et.json index 07e7748e13..19f8f40b9d 100644 --- a/homeassistant/components/cloud/translations/et.json +++ b/homeassistant/components/cloud/translations/et.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa on lubatud", - "can_reach_cert_server": "\u00dchendu serdiserveriga", - "can_reach_cloud": "\u00dchendu Home Assistant Cloudiga", - "can_reach_cloud_auth": "\u00dchendu tuvastusserveriga", + "can_reach_cert_server": "\u00dchendus serdiserveriga", + "can_reach_cloud": "\u00dchendus Home Assistant Cloudiga", + "can_reach_cloud_auth": "\u00dchendus tuvastusserveriga", "google_enabled": "Google on lubatud", "logged_in": "Sisse logitud", "relayer_connected": "Edastaja on \u00fchendatud", diff --git a/homeassistant/components/cloud/translations/hu.json b/homeassistant/components/cloud/translations/hu.json index 5dfc087c7b..a2bea167b5 100644 --- a/homeassistant/components/cloud/translations/hu.json +++ b/homeassistant/components/cloud/translations/hu.json @@ -5,6 +5,9 @@ "can_reach_cloud_auth": "Hiteles\u00edt\u00e9si kiszolg\u00e1l\u00f3 el\u00e9r\u00e9se", "google_enabled": "Google enged\u00e9lyezve", "logged_in": "Bejelentkezve", + "relayer_connected": "K\u00f6zvet\u00edt\u0151 csatlakoztatva", + "remote_connected": "T\u00e1voli csatlakoz\u00e1s", + "remote_enabled": "T\u00e1voli hozz\u00e1f\u00e9r\u00e9s enged\u00e9lyezve", "subscription_expiration": "El\u0151fizet\u00e9s lej\u00e1rata" } } diff --git a/homeassistant/components/cloud/translations/it.json b/homeassistant/components/cloud/translations/it.json index 320ca70b81..fbe13abc41 100644 --- a/homeassistant/components/cloud/translations/it.json +++ b/homeassistant/components/cloud/translations/it.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa abilitato", - "can_reach_cert_server": "Raggiungi il server dei certificati", - "can_reach_cloud": "Raggiungi Home Assistant Cloud", - "can_reach_cloud_auth": "Raggiungi il server di autenticazione", + "can_reach_cert_server": "Server dei Certificati raggiungibile", + "can_reach_cloud": "Home Assistant Cloud raggiungibile", + "can_reach_cloud_auth": "Server di Autenticazione raggiungibile", "google_enabled": "Google abilitato", "logged_in": "Accesso effettuato", "relayer_connected": "Relayer connesso", diff --git a/homeassistant/components/cloud/translations/no.json b/homeassistant/components/cloud/translations/no.json index 585811f0eb..63779e7fa9 100644 --- a/homeassistant/components/cloud/translations/no.json +++ b/homeassistant/components/cloud/translations/no.json @@ -4,7 +4,7 @@ "alexa_enabled": "Alexa aktivert", "can_reach_cert_server": "N\u00e5 sertifikatserver", "can_reach_cloud": "N\u00e5 Home Assistant Cloud", - "can_reach_cloud_auth": "N\u00e5 autentiseringsserver", + "can_reach_cloud_auth": "N\u00e5 godkjenningsserver", "google_enabled": "Google aktivert", "logged_in": "Logget inn", "relayer_connected": "Relayer tilkoblet", diff --git a/homeassistant/components/cloud/translations/tr.json b/homeassistant/components/cloud/translations/tr.json new file mode 100644 index 0000000000..0acb1e6a9a --- /dev/null +++ b/homeassistant/components/cloud/translations/tr.json @@ -0,0 +1,11 @@ +{ + "system_health": { + "info": { + "logged_in": "Giri\u015f Yapt\u0131", + "relayer_connected": "Yeniden Katman ba\u011fl\u0131", + "remote_connected": "Uzaktan Ba\u011fl\u0131", + "remote_enabled": "Uzaktan Etkinle\u015ftirildi", + "subscription_expiration": "Aboneli\u011fin Sona Ermesi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json index 68b1856815..809dad5da4 100644 --- a/homeassistant/components/cloudflare/translations/de.json +++ b/homeassistant/components/cloudflare/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unknown": "Unerwarteter Fehler" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_zone": "Ung\u00fcltige Zone" diff --git a/homeassistant/components/cloudflare/translations/pt.json b/homeassistant/components/cloudflare/translations/pt.json new file mode 100644 index 0000000000..158cd3f3f7 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "api_token": "API Token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/tr.json b/homeassistant/components/cloudflare/translations/tr.json new file mode 100644 index 0000000000..b7c7b43880 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "invalid_zone": "Ge\u00e7ersiz b\u00f6lge" + }, + "flow_title": "Cloudflare: {name}", + "step": { + "records": { + "data": { + "records": "Kay\u0131tlar" + }, + "title": "G\u00fcncellenecek Kay\u0131tlar\u0131 Se\u00e7in" + }, + "user": { + "title": "Cloudflare'ye ba\u011flan\u0131n" + }, + "zone": { + "data": { + "zone": "B\u00f6lge" + }, + "title": "G\u00fcncellenecek B\u00f6lgeyi Se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloudflare/translations/zh-Hant.json b/homeassistant/components/cloudflare/translations/zh-Hant.json index e84966b8d5..1be70def03 100644 --- a/homeassistant/components/cloudflare/translations/zh-Hant.json +++ b/homeassistant/components/cloudflare/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json index 1653a11c3e..f9a5783cd9 100644 --- a/homeassistant/components/control4/translations/de.json +++ b/homeassistant/components/control4/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/control4/translations/zh-Hant.json b/homeassistant/components/control4/translations/zh-Hant.json index f52e877a9d..bc955f119e 100644 --- a/homeassistant/components/control4/translations/zh-Hant.json +++ b/homeassistant/components/control4/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/coolmaster/translations/de.json b/homeassistant/components/coolmaster/translations/de.json index 19a57c3180..908dfaa448 100644 --- a/homeassistant/components/coolmaster/translations/de.json +++ b/homeassistant/components/coolmaster/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Verbindungsfehler", "no_units": "Es wurden keine HVAC-Ger\u00e4te im CoolMasterNet-Host gefunden." }, "step": { diff --git a/homeassistant/components/coolmaster/translations/pt.json b/homeassistant/components/coolmaster/translations/pt.json index ce7cbc3f54..f13cad90ed 100644 --- a/homeassistant/components/coolmaster/translations/pt.json +++ b/homeassistant/components/coolmaster/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/coolmaster/translations/zh-Hant.json b/homeassistant/components/coolmaster/translations/zh-Hant.json index 03f9cb3cfb..42278561d5 100644 --- a/homeassistant/components/coolmaster/translations/zh-Hant.json +++ b/homeassistant/components/coolmaster/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "no_units": "\u7121\u6cd5\u65bc CoolMasterNet \u4e3b\u6a5f\u627e\u5230\u4efb\u4f55 HVAC \u8a2d\u5099\u3002" + "no_units": "\u7121\u6cd5\u65bc CoolMasterNet \u4e3b\u6a5f\u627e\u5230\u4efb\u4f55 HVAC \u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/translations/pt.json b/homeassistant/components/coronavirus/translations/pt.json new file mode 100644 index 0000000000..e03867478c --- /dev/null +++ b/homeassistant/components/coronavirus/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "country": "Pa\u00eds" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/nl.json b/homeassistant/components/cover/translations/nl.json index 7d68d78641..679d9360a8 100644 --- a/homeassistant/components/cover/translations/nl.json +++ b/homeassistant/components/cover/translations/nl.json @@ -28,7 +28,7 @@ "state": { "_": { "closed": "Gesloten", - "closing": "Sluit", + "closing": "Sluiten", "open": "Open", "opening": "Opent", "stopped": "Gestopt" diff --git a/homeassistant/components/cover/translations/zh-Hans.json b/homeassistant/components/cover/translations/zh-Hans.json index 7c5675dad3..04b25ad7cb 100644 --- a/homeassistant/components/cover/translations/zh-Hans.json +++ b/homeassistant/components/cover/translations/zh-Hans.json @@ -1,6 +1,9 @@ { "device_automation": { "action_type": { + "close": "\u5173\u95ed {entity_name}", + "open": "\u6253\u5f00 {entity_name}", + "set_position": "\u8bbe\u7f6e {entity_name} \u7684\u4f4d\u7f6e", "stop": "\u505c\u6b62 {entity_name}" }, "condition_type": { @@ -12,7 +15,11 @@ "is_tilt_position": "{entity_name} \u5f53\u524d\u503e\u659c\u4f4d\u7f6e\u4e3a" }, "trigger_type": { - "closed": "{entity_name}\u5df2\u5173\u95ed" + "closed": "{entity_name} \u5df2\u5173\u95ed", + "closing": "{entity_name} \u6b63\u5728\u5173\u95ed", + "opened": "{entity_name} \u5df2\u6253\u5f00", + "opening": "{entity_name} \u6b63\u5728\u6253\u5f00", + "position": "{entity_name} \u7684\u4f4d\u7f6e\u53d8\u5316" } }, "state": { diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index a69871a1ef..ebf31967cc 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.3.1"], + "requirements": ["pydaikin==2.4.0"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum" diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index b39d7f27c5..5e0e1b5761 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -1,9 +1,13 @@ """Support for Daikin AirBase zones.""" +from homeassistant.components.switch import SwitchEntity from homeassistant.helpers.entity import ToggleEntity from . import DOMAIN as DAIKIN_DOMAIN ZONE_ICON = "mdi:home-circle" +STREAMER_ICON = "mdi:air-filter" +DAIKIN_ATTR_ADVANCED = "adv" +DAIKIN_ATTR_STREAMER = "streamer" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -17,15 +21,23 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Daikin climate based on config_entry.""" daikin_api = hass.data[DAIKIN_DOMAIN][entry.entry_id] + switches = [] zones = daikin_api.device.zones if zones: - async_add_entities( + switches.extend( [ DaikinZoneSwitch(daikin_api, zone_id) for zone_id, zone in enumerate(zones) if zone != ("-", "0") ] ) + if daikin_api.device.support_advanced_modes: + # It isn't possible to find out from the API responses if a specific + # device supports the streamer, so assume so if it does support + # advanced modes. + switches.append(DaikinStreamerSwitch(daikin_api)) + if switches: + async_add_entities(switches) class DaikinZoneSwitch(ToggleEntity): @@ -72,3 +84,50 @@ class DaikinZoneSwitch(ToggleEntity): async def async_turn_off(self, **kwargs): """Turn the zone off.""" await self._api.device.set_zone(self._zone_id, "0") + + +class DaikinStreamerSwitch(SwitchEntity): + """Streamer state.""" + + def __init__(self, daikin_api): + """Initialize streamer switch.""" + self._api = daikin_api + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._api.device.mac}-streamer" + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return STREAMER_ICON + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._api.name} streamer" + + @property + def is_on(self): + """Return the state of the sensor.""" + return ( + DAIKIN_ATTR_STREAMER in self._api.device.represent(DAIKIN_ATTR_ADVANCED)[1] + ) + + @property + def device_info(self): + """Return a device description for device registry.""" + return self._api.device_info + + async def async_update(self): + """Retrieve latest state.""" + await self._api.async_update() + + async def async_turn_on(self, **kwargs): + """Turn the zone on.""" + await self._api.device.set_streamer("on") + + async def async_turn_off(self, **kwargs): + """Turn the zone off.""" + await self._api.device.set_streamer("off") diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index 1d9ede292f..bbac113eb4 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -1,9 +1,11 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindungsfehler" }, "error": { + "cannot_connect": "Verbindungsfehler", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index 617aed245e..dd9b538ae8 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -4,9 +4,15 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { + "api_key": "API Key", "host": "Servidor", "password": "Palavra-passe" }, diff --git a/homeassistant/components/daikin/translations/zh-Hant.json b/homeassistant/components/daikin/translations/zh-Hant.json index 1949bd98b2..b1a19792a0 100644 --- a/homeassistant/components/daikin/translations/zh-Hant.json +++ b/homeassistant/components/daikin/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { @@ -16,7 +16,7 @@ "host": "\u4e3b\u6a5f\u7aef", "password": "\u5bc6\u78bc" }, - "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abfIP \u4f4d\u5740\u3002\n\n\u8acb\u6ce8\u610f\uff1aBRP072Cxx \u8207 SKYFi \u8a2d\u5099\u4e4b API \u5bc6\u9470\u8207\u5bc6\u78bc\u70ba\u5206\u958b\u4f7f\u7528\u3002", + "description": "\u8f38\u5165\u60a8\u7684\u5927\u91d1\u7a7a\u8abfIP \u4f4d\u5740\u3002\n\n\u8acb\u6ce8\u610f\uff1aBRP072Cxx \u8207 SKYFi \u88dd\u7f6e\u4e4b API \u5bc6\u9470\u8207\u5bc6\u78bc\u70ba\u5206\u958b\u4f7f\u7528\u3002", "title": "\u8a2d\u5b9a\u5927\u91d1\u7a7a\u8abf" } } diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 507b48da9d..fec7b82e36 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -1,8 +1,8 @@ """Support for deCONZ devices.""" import voluptuous as vol -from homeassistant.config_entries import _UNDEF from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.typing import UNDEFINED from .config_flow import get_master_gateway from .const import CONF_BRIDGE_ID, CONF_GROUP_ID_BASE, CONF_MASTER_GATEWAY, DOMAIN @@ -39,7 +39,7 @@ async def async_setup_entry(hass, config_entry): # 0.104 introduced config entry unique id, this makes upgrading possible if config_entry.unique_id is None: - new_data = _UNDEF + new_data = UNDEFINED if CONF_BRIDGE_ID in config_entry.data: new_data = dict(config_entry.data) new_data[CONF_GROUP_ID_BASE] = config_entry.data[CONF_BRIDGE_ID] diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 3e1e174873..98e3864e19 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -32,7 +32,7 @@ from .gateway import get_gateway_from_config_entry DECONZ_FAN_SMART = "smart" -FAN_MODES = { +FAN_MODE_TO_DECONZ = { DECONZ_FAN_SMART: "smart", FAN_AUTO: "auto", FAN_HIGH: "high", @@ -42,7 +42,9 @@ FAN_MODES = { FAN_OFF: "off", } -HVAC_MODES = { +DECONZ_TO_FAN_MODE = {value: key for key, value in FAN_MODE_TO_DECONZ.items()} + +HVAC_MODE_TO_DECONZ = { HVAC_MODE_AUTO: "auto", HVAC_MODE_COOL: "cool", HVAC_MODE_HEAT: "heat", @@ -54,7 +56,7 @@ DECONZ_PRESET_COMPLEX = "complex" DECONZ_PRESET_HOLIDAY = "holiday" DECONZ_PRESET_MANUAL = "manual" -PRESET_MODES = { +PRESET_MODE_TO_DECONZ = { DECONZ_PRESET_AUTO: "auto", PRESET_BOOST: "boost", PRESET_COMFORT: "comfort", @@ -64,6 +66,8 @@ PRESET_MODES = { DECONZ_PRESET_MANUAL: "manual", } +DECONZ_TO_PRESET_MODE = {value: key for key, value in PRESET_MODE_TO_DECONZ.items()} + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ climate devices. @@ -111,14 +115,17 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): """Set up thermostat device.""" super().__init__(device, gateway) - self._hvac_modes = dict(HVAC_MODES) + self._hvac_mode_to_deconz = dict(HVAC_MODE_TO_DECONZ) if "mode" not in device.raw["config"]: - self._hvac_modes = { + self._hvac_mode_to_deconz = { HVAC_MODE_HEAT: True, HVAC_MODE_OFF: False, } elif "coolsetpoint" not in device.raw["config"]: - self._hvac_modes.pop(HVAC_MODE_COOL) + self._hvac_mode_to_deconz.pop(HVAC_MODE_COOL) + self._deconz_to_hvac_mode = { + value: key for key, value in self._hvac_mode_to_deconz.items() + } self._features = SUPPORT_TARGET_TEMPERATURE @@ -138,26 +145,21 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def fan_mode(self) -> str: """Return fan operation.""" - for hass_fan_mode, fan_mode in FAN_MODES.items(): - if self._device.fanmode == fan_mode: - return hass_fan_mode - - if self._device.state_on: - return FAN_ON - - return FAN_OFF + return DECONZ_TO_FAN_MODE.get( + self._device.fanmode, FAN_ON if self._device.state_on else FAN_OFF + ) @property def fan_modes(self) -> list: """Return the list of available fan operation modes.""" - return list(FAN_MODES) + return list(FAN_MODE_TO_DECONZ) async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - if fan_mode not in FAN_MODES: + if fan_mode not in FAN_MODE_TO_DECONZ: raise ValueError(f"Unsupported fan mode {fan_mode}") - data = {"fanmode": FAN_MODES[fan_mode]} + data = {"fanmode": FAN_MODE_TO_DECONZ[fan_mode]} await self._device.async_set_config(data) @@ -169,28 +171,24 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): Need to be one of HVAC_MODE_*. """ - for hass_hvac_mode, device_mode in self._hvac_modes.items(): - if self._device.mode == device_mode: - return hass_hvac_mode - - if self._device.state_on: - return HVAC_MODE_HEAT - - return HVAC_MODE_OFF + return self._deconz_to_hvac_mode.get( + self._device.mode, + HVAC_MODE_HEAT if self._device.state_on else HVAC_MODE_OFF, + ) @property def hvac_modes(self) -> list: """Return the list of available hvac operation modes.""" - return list(self._hvac_modes) + return list(self._hvac_mode_to_deconz) async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - if hvac_mode not in self._hvac_modes: + if hvac_mode not in self._hvac_mode_to_deconz: raise ValueError(f"Unsupported HVAC mode {hvac_mode}") - data = {"mode": self._hvac_modes[hvac_mode]} - if len(self._hvac_modes) == 2: # Only allow turn on and off thermostat - data = {"on": self._hvac_modes[hvac_mode]} + data = {"mode": self._hvac_mode_to_deconz[hvac_mode]} + if len(self._hvac_mode_to_deconz) == 2: # Only allow turn on and off thermostat + data = {"on": self._hvac_mode_to_deconz[hvac_mode]} await self._device.async_set_config(data) @@ -199,23 +197,19 @@ class DeconzThermostat(DeconzDevice, ClimateEntity): @property def preset_mode(self) -> Optional[str]: """Return preset mode.""" - for hass_preset_mode, preset_mode in PRESET_MODES.items(): - if self._device.preset == preset_mode: - return hass_preset_mode - - return None + return DECONZ_TO_PRESET_MODE.get(self._device.preset) @property def preset_modes(self) -> list: """Return the list of available preset modes.""" - return list(PRESET_MODES) + return list(PRESET_MODE_TO_DECONZ) async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" - if preset_mode not in PRESET_MODES: + if preset_mode not in PRESET_MODE_TO_DECONZ: raise ValueError(f"Unsupported preset mode {preset_mode}") - data = {"preset": PRESET_MODES[preset_mode]} + data = {"preset": PRESET_MODE_TO_DECONZ[preset_mode]} await self._device.async_set_config(data) diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index ce6d6df890..725ce07a1b 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -2,12 +2,18 @@ "config": { "abort": { "already_configured": "Bridge j\u00e1 est\u00e1 configurada", - "no_bridges": "Nenhum hub deCONZ descoberto" + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_bridges": "Nenhum hub deCONZ descoberto", + "not_deconz_bridge": "N\u00e3o \u00e9 uma bridge deCONZ" }, "error": { - "no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API" + "no_key": "N\u00e3o foi poss\u00edvel obter uma API Key" }, "step": { + "hassio_confirm": { + "description": "Deseja configurar o Home Assistant para se conectar ao gateway deCONZ fornecido pelo addon Hass.io {addon} ?", + "title": "Gateway Zigbee deCONZ via addon Hass.io" + }, "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", "title": "Liga\u00e7\u00e3o com deCONZ" @@ -48,7 +54,18 @@ "remote_awakened": "Dispositivo acordou", "remote_button_double_press": "Bot\u00e3o \"{subtype}\" clicado duas vezes", "remote_button_long_press": "Bot\u00e3o \"{subtype}\" pressionado continuamente", - "remote_falling": "Dispositivo em queda livre" + "remote_falling": "Dispositivo em queda livre", + "remote_gyro_activated": "Dispositivo agitado" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configure a visibilidade dos tipos de dispositivos deCONZ" + } } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/zh-Hant.json b/homeassistant/components/deconz/translations/zh-Hant.json index f6695fc2af..335aa73a67 100644 --- a/homeassistant/components/deconz/translations/zh-Hant.json +++ b/homeassistant/components/deconz/translations/zh-Hant.json @@ -4,9 +4,9 @@ "already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_bridges": "\u672a\u641c\u5c0b\u5230 deCONZ Bridfe", - "no_hardware_available": "deCONZ \u6c92\u6709\u4efb\u4f55\u7121\u7dda\u96fb\u8a2d\u5099\u9023\u7dda", - "not_deconz_bridge": "\u975e deCONZ Bridge \u8a2d\u5099", - "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u8a2d\u5099" + "no_hardware_available": "deCONZ \u6c92\u6709\u4efb\u4f55\u7121\u7dda\u96fb\u88dd\u7f6e\u9023\u7dda", + "not_deconz_bridge": "\u975e deCONZ Bridge \u88dd\u7f6e", + "updated_instance": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0 deCONZ \u88dd\u7f6e" }, "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" @@ -59,7 +59,7 @@ "turn_on": "\u958b\u555f" }, "trigger_type": { - "remote_awakened": "\u8a2d\u5099\u5df2\u559a\u9192", + "remote_awakened": "\u88dd\u7f6e\u5df2\u559a\u9192", "remote_button_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca", "remote_button_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b", "remote_button_long_release": "\u9577\u6309\u5f8c\u91cb\u653e \"{subtype}\" \u6309\u9215", @@ -71,22 +71,22 @@ "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", - "remote_double_tap": "\u8a2d\u5099 \"{subtype}\" \u96d9\u6572", - "remote_double_tap_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u96d9\u9ede\u9078", - "remote_falling": "\u8a2d\u5099\u81ea\u7531\u843d\u4e0b", - "remote_flip_180_degrees": "\u8a2d\u5099\u65cb\u8f49 180 \u5ea6", - "remote_flip_90_degrees": "\u8a2d\u5099\u65cb\u8f49 90 \u5ea6", - "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643", - "remote_moved": "\u8a2d\u5099\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", - "remote_moved_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u671d\u4e0a", - "remote_rotate_from_side_1": "\u8a2d\u5099\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_2": "\u8a2d\u5099\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_3": "\u8a2d\u5099\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_4": "\u8a2d\u5099\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_5": "\u8a2d\u5099\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_turned_clockwise": "\u8a2d\u5099\u9806\u6642\u91dd\u65cb\u8f49", - "remote_turned_counter_clockwise": "\u8a2d\u5099\u9006\u6642\u91dd\u65cb\u8f49" + "remote_double_tap": "\u88dd\u7f6e \"{subtype}\" \u96d9\u6572", + "remote_double_tap_any_side": "\u88dd\u7f6e\u4efb\u4e00\u9762\u96d9\u9ede\u9078", + "remote_falling": "\u88dd\u7f6e\u81ea\u7531\u843d\u4e0b", + "remote_flip_180_degrees": "\u88dd\u7f6e\u65cb\u8f49 180 \u5ea6", + "remote_flip_90_degrees": "\u88dd\u7f6e\u65cb\u8f49 90 \u5ea6", + "remote_gyro_activated": "\u88dd\u7f6e\u6416\u6643", + "remote_moved": "\u88dd\u7f6e\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", + "remote_moved_any_side": "\u88dd\u7f6e\u4efb\u4e00\u9762\u671d\u4e0a", + "remote_rotate_from_side_1": "\u88dd\u7f6e\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_2": "\u88dd\u7f6e\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_3": "\u88dd\u7f6e\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_4": "\u88dd\u7f6e\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_5": "\u88dd\u7f6e\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_rotate_from_side_6": "\u88dd\u7f6e\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_turned_clockwise": "\u88dd\u7f6e\u9806\u6642\u91dd\u65cb\u8f49", + "remote_turned_counter_clockwise": "\u88dd\u7f6e\u9006\u6642\u91dd\u65cb\u8f49" } }, "options": { @@ -95,9 +95,9 @@ "data": { "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44", - "allow_new_devices": "\u5141\u8a31\u81ea\u52d5\u5316\u65b0\u589e\u8a2d\u5099" + "allow_new_devices": "\u5141\u8a31\u81ea\u52d5\u5316\u65b0\u589e\u88dd\u7f6e" }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b", + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b", "title": "deCONZ \u9078\u9805" } } diff --git a/homeassistant/components/demo/number.py b/homeassistant/components/demo/number.py index a8b9cb0ac4..5a6ce5f5c6 100644 --- a/homeassistant/components/demo/number.py +++ b/homeassistant/components/demo/number.py @@ -98,7 +98,7 @@ class DemoNumber(NumberEntity): return self._assumed @property - def state(self): + def value(self): """Return the current value.""" return self._state diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 31085292fb..44cbd69bcd 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.8", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.9", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index b5990dede2..73fe0f2152 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -338,6 +338,9 @@ class DenonDevice(MediaPlayerEntity): def select_source(self, source): """Select input source.""" + # Ensure that the AVR is turned on, which is necessary for input + # switch to work. + self.turn_on() return self._receiver.set_input_func(source) def select_sound_mode(self, sound_mode): diff --git a/homeassistant/components/denonavr/translations/pt.json b/homeassistant/components/denonavr/translations/pt.json index 34a23569b9..4a00952aaa 100644 --- a/homeassistant/components/denonavr/translations/pt.json +++ b/homeassistant/components/denonavr/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" + }, "step": { "select": { "data": { @@ -12,5 +16,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "zone2": "Configurar a Zona 2", + "zone3": "Configurar a Zona 3" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index fab16780a5..1aaa5b0407 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u95dc\u9589\u4e3b\u96fb\u6e90\u3001\u5c07\u4e59\u592a\u7db2\u8def\u65b7\u7dda\u5f8c\u91cd\u65b0\u9023\u7dda\uff0c\u53ef\u80fd\u6703\u6709\u6240\u5e6b\u52a9", "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u63a2\u7d22\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", diff --git a/homeassistant/components/device_tracker/translations/nl.json b/homeassistant/components/device_tracker/translations/nl.json index 99c0652d98..a28c8bdbbb 100644 --- a/homeassistant/components/device_tracker/translations/nl.json +++ b/homeassistant/components/device_tracker/translations/nl.json @@ -3,6 +3,10 @@ "condition_type": { "is_home": "{entity_name} is thuis", "is_not_home": "{entity_name} is niet thuis" + }, + "trigger_type": { + "enters": "{entity_name} gaat een zone binnen", + "leaves": "{entity_name} verlaat een zone" } }, "state": { diff --git a/homeassistant/components/device_tracker/translations/tr.json b/homeassistant/components/device_tracker/translations/tr.json index 6bb5ae1460..87042b6500 100644 --- a/homeassistant/components/device_tracker/translations/tr.json +++ b/homeassistant/components/device_tracker/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "enters": "{entity_name} bir b\u00f6lgeye girdi", + "leaves": "{entity_name} bir b\u00f6lgeden ayr\u0131l\u0131yor" + } + }, "state": { "_": { "home": "Evde", diff --git a/homeassistant/components/device_tracker/translations/zh-Hant.json b/homeassistant/components/device_tracker/translations/zh-Hant.json index e80c32afd0..b0e44bedac 100644 --- a/homeassistant/components/device_tracker/translations/zh-Hant.json +++ b/homeassistant/components/device_tracker/translations/zh-Hant.json @@ -15,5 +15,5 @@ "not_home": "\u96e2\u5bb6" } }, - "title": "\u8a2d\u5099\u8ffd\u8e64\u5668" + "title": "\u88dd\u7f6e\u8ffd\u8e64\u5668" } \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 96d52b57e8..e5ee902930 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -12,7 +12,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTAN from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.typing import HomeAssistantType -from .const import CONF_MYDEVOLO, DOMAIN, PLATFORMS +from .const import CONF_MYDEVOLO, DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS async def async_setup(hass, config): @@ -22,13 +22,9 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up the devolo account from a config entry.""" - conf = entry.data hass.data.setdefault(DOMAIN, {}) - mydevolo = Mydevolo() - mydevolo.user = conf[CONF_USERNAME] - mydevolo.password = conf[CONF_PASSWORD] - mydevolo.url = conf[CONF_MYDEVOLO] + mydevolo = _mydevolo(entry.data) credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) @@ -40,6 +36,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids) + if GATEWAY_SERIAL_PATTERN.match(entry.unique_id): + uuid = await hass.async_add_executor_job(mydevolo.uuid) + hass.config_entries.async_update_entry(entry, unique_id=uuid) + try: zeroconf_instance = await zeroconf.async_get_instance(hass) hass.data[DOMAIN][entry.entry_id] = {"gateways": [], "listener": None} @@ -95,3 +95,12 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo hass.data[DOMAIN][entry.entry_id]["listener"]() hass.data[DOMAIN].pop(entry.entry_id) return unload + + +def _mydevolo(conf: dict) -> Mydevolo: + """Configure mydevolo.""" + mydevolo = Mydevolo() + mydevolo.user = conf[CONF_USERNAME] + mydevolo.password = conf[CONF_PASSWORD] + mydevolo.url = conf[CONF_MYDEVOLO] + return mydevolo diff --git a/homeassistant/components/devolo_home_control/config_flow.py b/homeassistant/components/devolo_home_control/config_flow.py index 67803ec56b..3f51a9c088 100644 --- a/homeassistant/components/devolo_home_control/config_flow.py +++ b/homeassistant/components/devolo_home_control/config_flow.py @@ -55,8 +55,8 @@ class DevoloHomeControlFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not credentials_valid: return self._show_form({"base": "invalid_auth"}) _LOGGER.debug("Credentials valid") - gateway_ids = await self.hass.async_add_executor_job(mydevolo.get_gateway_ids) - await self.async_set_unique_id(gateway_ids[0]) + uuid = await self.hass.async_add_executor_job(mydevolo.uuid) + await self.async_set_unique_id(uuid) self._abort_if_unique_id_configured() return self.async_create_entry( diff --git a/homeassistant/components/devolo_home_control/const.py b/homeassistant/components/devolo_home_control/const.py index ea46ea4484..3a7d26435f 100644 --- a/homeassistant/components/devolo_home_control/const.py +++ b/homeassistant/components/devolo_home_control/const.py @@ -1,6 +1,8 @@ """Constants for the devolo_home_control integration.""" +import re DOMAIN = "devolo_home_control" DEFAULT_MYDEVOLO = "https://www.mydevolo.com" PLATFORMS = ["binary_sensor", "climate", "cover", "light", "sensor", "switch"] CONF_MYDEVOLO = "mydevolo_url" +GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}") diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json index b8a454fbab..ca6b9a6542 100644 --- a/homeassistant/components/devolo_home_control/translations/pt.json +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -1,9 +1,18 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { - "password": "Palavra-passe" + "home_control_url": "Home Control [VOID]", + "mydevolo_url": "mydevolo [VOID]", + "password": "Palavra-passe", + "username": "Email / devolo ID" } } } diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index 3b5744ba1a..fadb459a3d 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Konto ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindungsfehler", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/dexcom/translations/pt.json b/homeassistant/components/dexcom/translations/pt.json index af953a1caa..8af2ff4345 100644 --- a/homeassistant/components/dexcom/translations/pt.json +++ b/homeassistant/components/dexcom/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/dialogflow/translations/et.json b/homeassistant/components/dialogflow/translations/et.json index f0b6c3eade..989db1c256 100644 --- a/homeassistant/components/dialogflow/translations/et.json +++ b/homeassistant/components/dialogflow/translations/et.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, "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} )." + "default": "S\u00fcndmuste saatmiseks Home Assistantile peate seadistama [Dialogflow'i veebihaagii integreerimine] ( {dialogflow_url} ). \n\n Sisesta 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": { diff --git a/homeassistant/components/dialogflow/translations/pt.json b/homeassistant/components/dialogflow/translations/pt.json index 09ab0e6711..56c91431f1 100644 --- a/homeassistant/components/dialogflow/translations/pt.json +++ b/homeassistant/components/dialogflow/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar o [Dialogflow Webhook] ({dialogflow_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/json\n\n Veja [a documenta\u00e7\u00e3o] ({docs_url}) para obter mais detalhes." }, diff --git a/homeassistant/components/dialogflow/translations/zh-Hant.json b/homeassistant/components/dialogflow/translations/zh-Hant.json index ab790dafe9..4584a38313 100644 --- a/homeassistant/components/dialogflow/translations/zh-Hant.json +++ b/homeassistant/components/dialogflow/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/directv/translations/pt.json b/homeassistant/components/directv/translations/pt.json index 96a0956765..7880adf5ff 100644 --- a/homeassistant/components/directv/translations/pt.json +++ b/homeassistant/components/directv/translations/pt.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "unknown": "Erro inesperado" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/directv/translations/zh-Hant.json b/homeassistant/components/directv/translations/zh-Hant.json index 9be7ac31e6..e19ff18b36 100644 --- a/homeassistant/components/directv/translations/zh-Hant.json +++ b/homeassistant/components/directv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 11f83d8017..b7fd193afa 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -78,7 +78,7 @@ class DiscordNotificationService(BaseNotificationService): ) or discord_bot.get_user(channelid) if channel is None: - _LOGGER.warning("Channel not found for id: %s", channelid) + _LOGGER.warning("Channel not found for ID: %s", channelid) continue # Must create new instances of File for each channel. files = None diff --git a/homeassistant/components/doorbird/translations/pt.json b/homeassistant/components/doorbird/translations/pt.json index 3f200f4109..ceb6c92004 100644 --- a/homeassistant/components/doorbird/translations/pt.json +++ b/homeassistant/components/doorbird/translations/pt.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { "host": "Servidor", "name": "Nome do dispositivo", - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/doorbird/translations/zh-Hant.json b/homeassistant/components/doorbird/translations/zh-Hant.json index a4b3bd2fd8..bb1d109bb8 100644 --- a/homeassistant/components/doorbird/translations/zh-Hant.json +++ b/homeassistant/components/doorbird/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", - "not_doorbird_device": "\u6b64\u8a2d\u5099\u4e26\u975e DoorBird" + "not_doorbird_device": "\u6b64\u88dd\u7f6e\u4e26\u975e DoorBird" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -15,7 +15,7 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "name": "\u8a2d\u5099\u540d\u7a31", + "name": "\u88dd\u7f6e\u540d\u7a31", "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, diff --git a/homeassistant/components/dsmr/config_flow.py b/homeassistant/components/dsmr/config_flow.py index 912deb7ffe..f089959835 100644 --- a/homeassistant/components/dsmr/config_flow.py +++ b/homeassistant/components/dsmr/config_flow.py @@ -35,11 +35,15 @@ class DSMRConnection: self._port = port self._dsmr_version = dsmr_version self._telegram = {} + if dsmr_version == "5L": + self._equipment_identifier = obis_ref.LUXEMBOURG_EQUIPMENT_IDENTIFIER + else: + self._equipment_identifier = obis_ref.EQUIPMENT_IDENTIFIER def equipment_identifier(self): """Equipment identifier.""" - if obis_ref.EQUIPMENT_IDENTIFIER in self._telegram: - dsmr_object = self._telegram[obis_ref.EQUIPMENT_IDENTIFIER] + if self._equipment_identifier in self._telegram: + dsmr_object = self._telegram[self._equipment_identifier] return getattr(dsmr_object, "value", None) def equipment_identifier_gas(self): @@ -52,7 +56,7 @@ class DSMRConnection: """Test if we can validate connection with the device.""" def update_telegram(telegram): - if obis_ref.EQUIPMENT_IDENTIFIER in telegram: + if self._equipment_identifier in telegram: self._telegram = telegram transport.close() diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index cc1877fb5b..78cd317bb3 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_DSMR_VERSION, default=DEFAULT_DSMR_VERSION): vol.All( - cv.string, vol.In(["5B", "5", "4", "2.2"]) + cv.string, vol.In(["5L", "5B", "5", "4", "2.2"]) ), vol.Optional(CONF_RECONNECT_INTERVAL, default=DEFAULT_RECONNECT_INTERVAL): int, vol.Optional(CONF_PRECISION, default=DEFAULT_PRECISION): vol.Coerce(int), @@ -85,7 +85,6 @@ async def async_setup_entry( ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], - ["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], ["Energy Consumption (tarif 1)", obis_ref.ELECTRICITY_USED_TARIFF_1], ["Energy Consumption (tarif 2)", obis_ref.ELECTRICITY_USED_TARIFF_2], ["Energy Production (tarif 1)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], @@ -112,6 +111,24 @@ async def async_setup_entry( ["Current Phase L3", obis_ref.INSTANTANEOUS_CURRENT_L3], ] + if dsmr_version == "5L": + obis_mapping.extend( + [ + [ + "Energy Consumption (total)", + obis_ref.LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, + ], + [ + "Energy Production (total)", + obis_ref.LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + ], + ] + ) + else: + obis_mapping.extend( + [["Energy Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL]] + ) + # Generate device entities devices = [ DSMREntity(name, DEVICE_NAME_ENERGY, config[CONF_SERIAL_ID], obis, config) @@ -120,7 +137,7 @@ async def async_setup_entry( # Protocol version specific obis if CONF_SERIAL_ID_GAS in config: - if dsmr_version in ("4", "5"): + if dsmr_version in ("4", "5", "5L"): gas_obis = obis_ref.HOURLY_GAS_METER_READING elif dsmr_version in ("5B",): gas_obis = obis_ref.BELGIUM_HOURLY_GAS_METER_READING @@ -180,6 +197,10 @@ async def async_setup_entry( async def connect_and_reconnect(): """Connect to DSMR and keep reconnecting until Home Assistant stops.""" + stop_listener = None + transport = None + protocol = None + while hass.state != CoreState.stopping: # Start DSMR asyncio.Protocol reader try: @@ -194,10 +215,9 @@ async def async_setup_entry( # Wait for reader to close await protocol.wait_closed() - # Unexpected disconnect - if transport: - # remove listener - stop_listener() + # Unexpected disconnect + if not hass.is_stopping: + stop_listener() transport = None protocol = None @@ -217,7 +237,7 @@ async def async_setup_entry( protocol = None except CancelledError: if stop_listener: - stop_listener() + stop_listener() # pylint: disable=not-callable if transport: transport.close() diff --git a/homeassistant/components/dsmr/translations/es.json b/homeassistant/components/dsmr/translations/es.json index 364953d39d..a85293e93a 100644 --- a/homeassistant/components/dsmr/translations/es.json +++ b/homeassistant/components/dsmr/translations/es.json @@ -2,6 +2,10 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "step": { + "one": "Vac\u00edo", + "other": "Vac\u00edo" } }, "options": { diff --git a/homeassistant/components/dsmr/translations/nl.json b/homeassistant/components/dsmr/translations/nl.json index 41edcd176d..ba31fa36fd 100644 --- a/homeassistant/components/dsmr/translations/nl.json +++ b/homeassistant/components/dsmr/translations/nl.json @@ -11,5 +11,15 @@ "one": "Leeg", "other": "Leeg" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Minimumtijd tussen entiteitsupdates [s]" + }, + "title": "DSMR-opties" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/pt.json b/homeassistant/components/dsmr/translations/pt.json new file mode 100644 index 0000000000..ce8a928727 --- /dev/null +++ b/homeassistant/components/dsmr/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json new file mode 100644 index 0000000000..94c31d0e15 --- /dev/null +++ b/homeassistant/components/dsmr/translations/tr.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Varl\u0131k g\u00fcncellemeleri [ler] aras\u0131ndaki minimum s\u00fcre" + }, + "title": "DSMR Se\u00e7enekleri" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/zh-Hant.json b/homeassistant/components/dsmr/translations/zh-Hant.json index e35c96a7bf..cbbc3dc8f5 100644 --- a/homeassistant/components/dsmr/translations/zh-Hant.json +++ b/homeassistant/components/dsmr/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" } }, "options": { diff --git a/homeassistant/components/dunehd/translations/pt.json b/homeassistant/components/dunehd/translations/pt.json index ce7cbc3f54..7fe3a6078c 100644 --- a/homeassistant/components/dunehd/translations/pt.json +++ b/homeassistant/components/dunehd/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/dunehd/translations/zh-Hant.json b/homeassistant/components/dunehd/translations/zh-Hant.json index 855cefaa77..ce7a120122 100644 --- a/homeassistant/components/dunehd/translations/zh-Hant.json +++ b/homeassistant/components/dunehd/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740" }, diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index d2c23f4609..a71c124c63 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -2,6 +2,7 @@ import logging from libpurecool.const import ( + AutoMode, FanPower, FanSpeed, FanState, @@ -333,7 +334,10 @@ class DysonPureHotCoolEntity(ClimateEntity): @property def fan_mode(self): """Return the fan setting.""" - if self._device.state.fan_state == FanState.FAN_OFF.value: + if ( + self._device.state.auto_mode != AutoMode.AUTO_ON.value + and self._device.state.fan_state == FanState.FAN_OFF.value + ): return FAN_OFF return SPEED_MAP[self._device.state.speed] @@ -368,7 +372,7 @@ class DysonPureHotCoolEntity(ClimateEntity): elif fan_mode == FAN_HIGH: self._device.set_fan_speed(FanSpeed.FAN_SPEED_10) elif fan_mode == FAN_AUTO: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_AUTO) + self._device.enable_auto_mode() def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index 4d9fe2eba2..ca685f36a1 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -257,7 +257,7 @@ class DysonPureCoolLinkDevice(FanEntity): def is_on(self): """Return true if the entity is on.""" if self._device.state: - return self._device.state.fan_mode == "FAN" + return self._device.state.fan_mode in ["FAN", "AUTO"] return False @property diff --git a/homeassistant/components/eafm/translations/zh-Hant.json b/homeassistant/components/eafm/translations/zh-Hant.json index 5da4b6d7c0..73083d2b73 100644 --- a/homeassistant/components/eafm/translations/zh-Hant.json +++ b/homeassistant/components/eafm/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_stations": "\u627e\u4e0d\u5230\u7b26\u5408\u7684\u76e3\u63a7\u7ad9\u3002" }, "step": { diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 94396bbf88..6bb7dc1a87 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -65,6 +65,11 @@ AWAY_MODE = "awayMode" PRESET_HOME = "home" PRESET_SLEEP = "sleep" +DEFAULT_MIN_HUMIDITY = 15 +DEFAULT_MAX_HUMIDITY = 50 +HUMIDIFIER_MANUAL_MODE = "manual" + + # Order matters, because for reverse mapping we don't want to map HEAT to AUX ECOBEE_HVAC_TO_HASS = collections.OrderedDict( [ @@ -162,7 +167,6 @@ SUPPORT_FLAGS = ( | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_FAN_MODE - | SUPPORT_TARGET_HUMIDITY ) @@ -332,6 +336,8 @@ class Thermostat(ClimateEntity): @property def supported_features(self): """Return the list of supported features.""" + if self.has_humidifier_control: + return SUPPORT_FLAGS | SUPPORT_TARGET_HUMIDITY return SUPPORT_FLAGS @property @@ -391,6 +397,31 @@ class Thermostat(ClimateEntity): return self.thermostat["runtime"]["desiredCool"] / 10.0 return None + @property + def has_humidifier_control(self): + """Return true if humidifier connected to thermostat and set to manual/on mode.""" + return ( + self.thermostat["settings"]["hasHumidifier"] + and self.thermostat["settings"]["humidifierMode"] == HUMIDIFIER_MANUAL_MODE + ) + + @property + def target_humidity(self) -> Optional[int]: + """Return the desired humidity set point.""" + if self.has_humidifier_control: + return self.thermostat["runtime"]["desiredHumidity"] + return None + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return DEFAULT_MIN_HUMIDITY + + @property + def max_humidity(self) -> int: + """Return the maximum humidity.""" + return DEFAULT_MAX_HUMIDITY + @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -653,7 +684,13 @@ class Thermostat(ClimateEntity): def set_humidity(self, humidity): """Set the humidity level.""" + if humidity not in range(0, 101): + raise ValueError( + f"Invalid set_humidity value (must be in range 0-100): {humidity}" + ) + self.data.ecobee.set_humidity(self.thermostat_index, int(humidity)) + self.update_without_throttle = True def set_hvac_mode(self, hvac_mode): """Set HVAC mode (auto, auxHeatOnly, cool, heat, off).""" diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 38d6b4577b..040744b27a 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -3,6 +3,6 @@ "name": "ecobee", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", - "requirements": ["python-ecobee-api==0.2.7"], + "requirements": ["python-ecobee-api==0.2.8"], "codeowners": ["@marthoc"] } diff --git a/homeassistant/components/ecobee/translations/pt.json b/homeassistant/components/ecobee/translations/pt.json index 20bba0ede4..f6e4d5f5dc 100644 --- a/homeassistant/components/ecobee/translations/pt.json +++ b/homeassistant/components/ecobee/translations/pt.json @@ -1,10 +1,14 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "step": { "user": { "data": { "api_key": "Chave da API" - } + }, + "title": "ecobee API Key" } } } diff --git a/homeassistant/components/ecobee/translations/zh-Hant.json b/homeassistant/components/ecobee/translations/zh-Hant.json index 54cad2049f..e9789c855d 100644 --- a/homeassistant/components/ecobee/translations/zh-Hant.json +++ b/homeassistant/components/ecobee/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "pin_request_failed": "ecobee \u6240\u9700\u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u8a8d\u5bc6\u9470\u6b63\u78ba\u6027\u3002", diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json index 4d10424216..7497460445 100644 --- a/homeassistant/components/elgato/translations/de.json +++ b/homeassistant/components/elgato/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert." + "already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert.", + "cannot_connect": "Verbindungsfehler" + }, + "error": { + "cannot_connect": "Verbindungsfehler" }, "flow_title": "Elgato Key Light: {serial_number}", "step": { diff --git a/homeassistant/components/elgato/translations/pt.json b/homeassistant/components/elgato/translations/pt.json index c4d1cc35cc..0bf8113cca 100644 --- a/homeassistant/components/elgato/translations/pt.json +++ b/homeassistant/components/elgato/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/translations/zh-Hant.json b/homeassistant/components/elgato/translations/zh-Hant.json index e25b4cd7c8..8f301b73b3 100644 --- a/homeassistant/components/elgato/translations/zh-Hant.json +++ b/homeassistant/components/elgato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { @@ -17,8 +17,8 @@ "description": "\u8a2d\u5b9a Elgato Key \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato Key \u7167\u660e\u8a2d\u5099\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato Key \u7167\u660e\u8a2d\u5099" + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Elgato Key \u7167\u660e\u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato Key \u7167\u660e\u88dd\u7f6e" } } } diff --git a/homeassistant/components/elkm1/translations/pt.json b/homeassistant/components/elkm1/translations/pt.json index 2f61dbc37e..48d278ac35 100644 --- a/homeassistant/components/elkm1/translations/pt.json +++ b/homeassistant/components/elkm1/translations/pt.json @@ -1,12 +1,15 @@ { "config": { "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { "password": "Palavra-passe (segura apenas)", + "protocol": "Protocolo", "username": "Nome de utilizador (apenas seguro)." } } diff --git a/homeassistant/components/emulated_roku/translations/pt.json b/homeassistant/components/emulated_roku/translations/pt.json index 8c9b894c4b..479685ff7e 100644 --- a/homeassistant/components/emulated_roku/translations/pt.json +++ b/homeassistant/components/emulated_roku/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/emulated_roku/translations/zh-Hant.json b/homeassistant/components/emulated_roku/translations/zh-Hant.json index 8c4ac5a0d7..ee877f7896 100644 --- a/homeassistant/components/emulated_roku/translations/zh-Hant.json +++ b/homeassistant/components/emulated_roku/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "step": { "user": { diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 86b0614897..da6765368a 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -2,6 +2,6 @@ "domain": "enigma2", "name": "Enigma2 (OpenWebif)", "documentation": "https://www.home-assistant.io/integrations/enigma2", - "requirements": ["openwebifpy==3.1.1"], + "requirements": ["openwebifpy==3.2.7"], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/enigma2/media_player.py b/homeassistant/components/enigma2/media_player.py index 8bb0486cd2..4baa6aaf04 100644 --- a/homeassistant/components/enigma2/media_player.py +++ b/homeassistant/components/enigma2/media_player.py @@ -126,6 +126,11 @@ class Enigma2Device(MediaPlayerEntity): """Return the name of the device.""" return self._name + @property + def unique_id(self): + """Return the unique ID for this entity.""" + return self.e2_box.mac_address + @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/enocean/translations/no.json b/homeassistant/components/enocean/translations/no.json index 775443d8f5..eefa1fd2dd 100644 --- a/homeassistant/components/enocean/translations/no.json +++ b/homeassistant/components/enocean/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "invalid_dongle_path": "Ugyldig donglesti", + "invalid_dongle_path": "Ugyldig donglebane", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { diff --git a/homeassistant/components/enocean/translations/zh-Hant.json b/homeassistant/components/enocean/translations/zh-Hant.json index bc51c7f0bb..6000b968e5 100644 --- a/homeassistant/components/enocean/translations/zh-Hant.json +++ b/homeassistant/components/enocean/translations/zh-Hant.json @@ -1,24 +1,24 @@ { "config": { "abort": { - "invalid_dongle_path": "\u8a2d\u5099\u8def\u5f91\u7121\u6548", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "invalid_dongle_path": "\u88dd\u7f6e\u8def\u5f91\u7121\u6548", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { - "invalid_dongle_path": "\u6b64\u8def\u5f91\u7121\u6709\u6548\u8a2d\u5099" + "invalid_dongle_path": "\u6b64\u8def\u5f91\u7121\u6709\u6548\u88dd\u7f6e" }, "step": { "detect": { "data": { - "path": "USB \u8a2d\u5099\u8def\u5f91" + "path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "title": "\u9078\u64c7 ENOcean \u8a2d\u5099\u8def\u5f91" + "title": "\u9078\u64c7 ENOcean \u88dd\u7f6e\u8def\u5f91" }, "manual": { "data": { - "path": "USB \u8a2d\u5099\u8def\u5f91" + "path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "title": "\u8f38\u5165 ENOcean \u8a2d\u5099\u8def\u5f91" + "title": "\u8f38\u5165 ENOcean \u88dd\u7f6e\u8def\u5f91" } } } diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index b339013a69..9e9760560d 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -2,7 +2,7 @@ "domain": "enphase_envoy", "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": ["envoy_reader==0.17.3"], + "requirements": ["envoy_reader==0.18.3"], "codeowners": [ "@gtdiehl" ] diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index a2b50f20eb..64b4fdf66a 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -1,8 +1,11 @@ """Support for Enphase Envoy solar energy monitor.""" + +from datetime import timedelta import logging +import async_timeout from envoy_reader.envoy_reader import EnvoyReader -import requests +import httpx import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -15,8 +18,13 @@ from homeassistant.const import ( ENERGY_WATT_HOUR, POWER_WATT, ) +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) _LOGGER = logging.getLogger(__name__) @@ -38,10 +46,11 @@ SENSORS = { "inverters": ("Envoy Inverter", POWER_WATT), } - ICON = "mdi:flash" CONST_DEFAULT_HOST = "envoy" +SCAN_INTERVAL = timedelta(seconds=60) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, @@ -55,7 +64,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + homeassistant, config, async_add_entities, discovery_info=None +): """Set up the Enphase Envoy sensor.""" ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] @@ -63,55 +74,99 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= username = config[CONF_USERNAME] password = config[CONF_PASSWORD] - envoy_reader = EnvoyReader(ip_address, username, password) + if "inverters" in monitored_conditions: + envoy_reader = EnvoyReader(ip_address, username, password, inverters=True) + else: + envoy_reader = EnvoyReader(ip_address, username, password) + + try: + await envoy_reader.getData() + except httpx.HTTPStatusError as err: + _LOGGER.error("Authentication failure during setup: %s", err) + return + except httpx.HTTPError as err: + raise PlatformNotReady from err + + async def async_update_data(): + """Fetch data from API endpoint.""" + data = {} + async with async_timeout.timeout(30): + try: + await envoy_reader.getData() + except httpx.HTTPError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + for condition in monitored_conditions: + if condition != "inverters": + data[condition] = await getattr(envoy_reader, condition)() + else: + data["inverters_production"] = await getattr( + envoy_reader, "inverters_production" + )() + + _LOGGER.debug("Retrieved data from API: %s", data) + + return data + + coordinator = DataUpdateCoordinator( + homeassistant, + _LOGGER, + name="sensor", + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ) + + await coordinator.async_refresh() + + if coordinator.data is None: + raise PlatformNotReady entities = [] - # Iterate through the list of sensors for condition in monitored_conditions: - if condition == "inverters": - try: - inverters = await envoy_reader.inverters_production() - except requests.exceptions.HTTPError: - _LOGGER.warning( - "Authentication for Inverter data failed during setup: %s", - ip_address, - ) - continue - - if isinstance(inverters, dict): - for inverter in inverters: - entities.append( - Envoy( - envoy_reader, - condition, - f"{name}{SENSORS[condition][0]} {inverter}", - SENSORS[condition][1], - ) + entity_name = "" + if ( + condition == "inverters" + and coordinator.data.get("inverters_production") is not None + ): + for inverter in coordinator.data["inverters_production"]: + entity_name = f"{name}{SENSORS[condition][0]} {inverter}" + split_name = entity_name.split(" ") + serial_number = split_name[-1] + entities.append( + Envoy( + condition, + entity_name, + serial_number, + SENSORS[condition][1], + coordinator, ) - - else: + ) + elif condition != "inverters": + entity_name = f"{name}{SENSORS[condition][0]}" entities.append( Envoy( - envoy_reader, condition, - f"{name}{SENSORS[condition][0]}", + entity_name, + None, SENSORS[condition][1], + coordinator, ) ) + async_add_entities(entities) -class Envoy(Entity): - """Implementation of the Enphase Envoy sensors.""" +class Envoy(CoordinatorEntity): + """Envoy entity.""" - def __init__(self, envoy_reader, sensor_type, name, unit): - """Initialize the sensor.""" - self._envoy_reader = envoy_reader + def __init__(self, sensor_type, name, serial_number, unit, coordinator): + """Initialize Envoy entity.""" self._type = sensor_type self._name = name + self._serial_number = serial_number self._unit_of_measurement = unit - self._state = None - self._last_reported = None + + super().__init__(coordinator) @property def name(self): @@ -121,7 +176,20 @@ class Envoy(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + if self._type != "inverters": + value = self.coordinator.data.get(self._type) + + elif ( + self._type == "inverters" + and self.coordinator.data.get("inverters_production") is not None + ): + value = self.coordinator.data.get("inverters_production").get( + self._serial_number + )[0] + else: + return None + + return value @property def unit_of_measurement(self): @@ -136,33 +204,13 @@ class Envoy(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - if self._type == "inverters": - return {"last_reported": self._last_reported} + if ( + self._type == "inverters" + and self.coordinator.data.get("inverters_production") is not None + ): + value = self.coordinator.data.get("inverters_production").get( + self._serial_number + )[1] + return {"last_reported": value} return None - - async def async_update(self): - """Get the energy production data from the Enphase Envoy.""" - if self._type != "inverters": - _state = await getattr(self._envoy_reader, self._type)() - if isinstance(_state, int): - self._state = _state - else: - _LOGGER.error(_state) - self._state = None - - elif self._type == "inverters": - try: - inverters = await (self._envoy_reader.inverters_production()) - except requests.exceptions.HTTPError: - _LOGGER.warning( - "Authentication for Inverter data failed during update: %s", - self._envoy_reader.host, - ) - - if isinstance(inverters, dict): - serial_number = self._name.split(" ")[2] - self._state = inverters[serial_number][0] - self._last_reported = inverters[serial_number][1] - else: - self._state = None diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json new file mode 100644 index 0000000000..c03615a39f --- /dev/null +++ b/homeassistant/components/epson/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, + "step": { + "user": { + "data": { + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/pt.json b/homeassistant/components/epson/translations/pt.json new file mode 100644 index 0000000000..352e98916f --- /dev/null +++ b/homeassistant/components/epson/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json new file mode 100644 index 0000000000..aafc2e2b30 --- /dev/null +++ b/homeassistant/components/epson/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "name": "\u0130sim", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index a12754a87f..fcfb4cf7ff 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -257,7 +257,12 @@ async def _setup_auto_reconnect_logic( try: await cli.connect(on_stop=try_connect, login=True) except APIConnectionError as error: - _LOGGER.info("Can't connect to ESPHome API for %s: %s", host, error) + _LOGGER.info( + "Can't connect to ESPHome API for %s (%s): %s", + entry.unique_id, + host, + error, + ) # Schedule re-connect in event loop in order not to delay HA # startup. First connect is scheduled in tracked tasks. data.reconnect_task = hass.loop.create_task( diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 5d831d7aaa..d3501c496e 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -15,7 +15,7 @@ "data": { "password": "Passord" }, - "description": "Vennligst fyll inn passordet du har angitt i din konfigurasjon for {name}." + "description": "Vennligst fyll inn passordet du har angitt i din konfigurasjon for {name}" }, "discovery_confirm": { "description": "\u00d8nsker du \u00e5 legge ESPHome noden `{name}` til Home Assistant?", diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index e010af99a0..6ff4d78644 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -1,12 +1,15 @@ { "config": { "abort": { - "already_configured": "O ESP j\u00e1 est\u00e1 configurado" + "already_configured": "O ESP j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" }, "error": { "connection_error": "N\u00e3o \u00e9 poss\u00edvel conectar-se ao ESP. Por favor, verifique se o seu arquivo YAML cont\u00e9m uma linha 'api:'.", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/esphome/translations/zh-Hant.json b/homeassistant/components/esphome/translations/zh-Hant.json index a60ef4fdeb..4e719a7957 100644 --- a/homeassistant/components/esphome/translations/zh-Hant.json +++ b/homeassistant/components/esphome/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index f1b873d58a..97d2b594cc 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -11,8 +11,14 @@ from typing import Optional import voluptuous as vol from homeassistant.components import history -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import ( + DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, + DOMAIN as SENSOR_DOMAIN, + PLATFORM_SCHEMA, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, @@ -132,7 +138,9 @@ FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ENTITY_ID): vol.Any( + cv.entity_domain(SENSOR_DOMAIN), cv.entity_domain(BINARY_SENSOR_DOMAIN) + ), vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_FILTERS): vol.All( cv.ensure_list, @@ -178,16 +186,28 @@ class SensorFilter(Entity): self._state = None self._filters = filters self._icon = None + self._device_class = None @callback def _update_filter_sensor_state_event(self, event): """Handle device state changes.""" + _LOGGER.debug("Update filter on event: %s", event) self._update_filter_sensor_state(event.data.get("new_state")) @callback def _update_filter_sensor_state(self, new_state, update_ha=True): """Process device state changes.""" - if new_state is None or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + if new_state is None: + _LOGGER.warning( + "While updating filter %s, the new_state is None", self._name + ) + self._state = None + self.async_write_ha_state() + return + + if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + self._state = new_state.state + self.async_write_ha_state() return temp_state = new_state @@ -206,7 +226,11 @@ class SensorFilter(Entity): return temp_state = filtered_state except ValueError: - _LOGGER.error("Could not convert state: %s to number", self._state) + _LOGGER.error( + "Could not convert state: %s (%s) to number", + new_state.state, + type(new_state.state), + ) return self._state = temp_state.state @@ -214,6 +238,12 @@ class SensorFilter(Entity): if self._icon is None: self._icon = new_state.attributes.get(ATTR_ICON, ICON) + if ( + self._device_class is None + and new_state.attributes.get(ATTR_DEVICE_CLASS) in SENSOR_DEVICE_CLASSES + ): + self._device_class = new_state.attributes.get(ATTR_DEVICE_CLASS) + if self._unit_of_measurement is None: self._unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT @@ -283,7 +313,8 @@ class SensorFilter(Entity): # Replay history through the filter chain for state in history_list: - self._update_filter_sensor_state(state, False) + if state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, None]: + self._update_filter_sensor_state(state, False) self.async_on_remove( async_track_state_change_event( @@ -321,6 +352,11 @@ class SensorFilter(Entity): """Return the state attributes of the sensor.""" return {ATTR_ENTITY_ID: self._entity} + @property + def device_class(self): + """Return device class.""" + return self._device_class + class FilterState: """State abstraction for filter usage.""" @@ -401,7 +437,7 @@ class Filter: """Implement a common interface for filters.""" fstate = FilterState(new_state) if self._only_numbers and not isinstance(fstate.state, Number): - raise ValueError + raise ValueError(f"State <{fstate.state}> is not a Number") filtered = self._filter_state(fstate) filtered.set_precision(self.precision) diff --git a/homeassistant/components/fireservicerota/translations/de.json b/homeassistant/components/fireservicerota/translations/de.json new file mode 100644 index 0000000000..737fbc5ff5 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Account wurde schon konfiguriert", + "reauth_successful": "Neuauthentifizierung erfolgreich" + }, + "create_entry": { + "default": "Authentifizierung erfolgreich" + }, + "error": { + "invalid_auth": "Authentifizienung ung\u00fcltig" + }, + "step": { + "reauth": { + "data": { + "password": "Passwort" + }, + "description": "Authentifizierungs-Tokens sind ung\u00fcltig, melde dich an, um sie neu zu erstellen." + }, + "user": { + "data": { + "password": "Passwort", + "url": "Webseite", + "username": "Nutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/hu.json b/homeassistant/components/fireservicerota/translations/hu.json new file mode 100644 index 0000000000..63c887ff28 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "url": "Weboldal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/no.json b/homeassistant/components/fireservicerota/translations/no.json index 5a4635e1ed..af1ceba2c9 100644 --- a/homeassistant/components/fireservicerota/translations/no.json +++ b/homeassistant/components/fireservicerota/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Kontoen er allerede konfigurert", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "create_entry": { "default": "Vellykket godkjenning" @@ -15,7 +15,7 @@ "data": { "password": "Passord" }, - "description": "Autentiseringstokener for baceame er ugyldige, logg inn for \u00e5 gjenskape dem." + "description": "Godkjenningstokener ble ugyldige, logg inn for \u00e5 gjenopprette dem" }, "user": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/pt.json b/homeassistant/components/fireservicerota/translations/pt.json new file mode 100644 index 0000000000..c78c9a5aba --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/pt.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "reauth": { + "data": { + "password": "Palavra-passe" + } + }, + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/sl.json b/homeassistant/components/fireservicerota/translations/sl.json new file mode 100644 index 0000000000..e38e7f9916 --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/sl.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Ra\u010dun je \u017ee overjen", + "reauth_successful": "Ponovno overjanje je bilo uspe\u0161no" + }, + "create_entry": { + "default": "Uspe\u0161na overitev" + }, + "error": { + "invalid_auth": "Napaka pri overjanju" + }, + "step": { + "reauth": { + "data": { + "password": "Geslo" + }, + "description": "Overitveni \u017eetoni niso ve\u010d veljavni, ponovno se prijavite, da jih znova ustvarite." + }, + "user": { + "data": { + "password": "Geslo", + "url": "Spletna stran", + "username": "Uporabni\u0161ko ime" + } + } + } + } +} \ 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 b69e8de8f7..ed0ef205ff 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -11,6 +11,7 @@ "step": { "user": { "data": { + "client_id": "Client-ID (optional)", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/flick_electric/translations/pt.json b/homeassistant/components/flick_electric/translations/pt.json index 1e3d9138c8..c2bf0536cc 100644 --- a/homeassistant/components/flick_electric/translations/pt.json +++ b/homeassistant/components/flick_electric/translations/pt.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/flo/translations/de.json b/homeassistant/components/flo/translations/de.json index 6f39806287..3821567570 100644 --- a/homeassistant/components/flo/translations/de.json +++ b/homeassistant/components/flo/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flo/translations/zh-Hant.json b/homeassistant/components/flo/translations/zh-Hant.json index 8bf65ef6ee..cad7d736a9 100644 --- a/homeassistant/components/flo/translations/zh-Hant.json +++ b/homeassistant/components/flo/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/flume/translations/pt.json b/homeassistant/components/flume/translations/pt.json index 4a071063d4..c2bf0536cc 100644 --- a/homeassistant/components/flume/translations/pt.json +++ b/homeassistant/components/flume/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index e7dc1f6cd2..cd2934170c 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Diese Koordinaten sind bereits registriert." }, + "error": { + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/flunearyou/translations/pt.json b/homeassistant/components/flunearyou/translations/pt.json index c7081cd694..219446a038 100644 --- a/homeassistant/components/flunearyou/translations/pt.json +++ b/homeassistant/components/flunearyou/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/forked_daapd/translations/pt.json b/homeassistant/components/forked_daapd/translations/pt.json index 8d3dfe38d4..e9b298e14e 100644 --- a/homeassistant/components/forked_daapd/translations/pt.json +++ b/homeassistant/components/forked_daapd/translations/pt.json @@ -1,14 +1,21 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "unknown_error": "Erro inesperado", "wrong_password": "Senha incorreta." }, "step": { "user": { "data": { "host": "Servidor", - "password": "Palavra-passe da API (deixar em branco se sem palavra-passe)" - } + "name": "Nome amig\u00e1vel", + "password": "Palavra-passe da API (deixar em branco se sem palavra-passe)", + "port": "Porta da API" + }, + "title": "Configurar dispositivo forked-daapd" } } } diff --git a/homeassistant/components/forked_daapd/translations/zh-Hant.json b/homeassistant/components/forked_daapd/translations/zh-Hant.json index 88e8628848..0ac0bac013 100644 --- a/homeassistant/components/forked_daapd/translations/zh-Hant.json +++ b/homeassistant/components/forked_daapd/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "not_forked_daapd": "\u8a2d\u5099\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_forked_daapd": "\u88dd\u7f6e\u4e26\u975e forked-daapd \u4f3a\u670d\u5668\u3002" }, "error": { "forbidden": "\u7121\u6cd5\u9023\u7dda\uff0c\u8acb\u78ba\u8a8d forked-daapd \u7db2\u8def\u6b0a\u9650\u3002", @@ -21,7 +21,7 @@ "password": "API \u5bc6\u78bc\uff08\u5047\u5982\u7121\u5bc6\u78bc\uff0c\u8acb\u7559\u7a7a\uff09", "port": "API \u901a\u8a0a\u57e0" }, - "title": "\u8a2d\u5b9a forked-daapd \u8a2d\u5099" + "title": "\u8a2d\u5b9a forked-daapd \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/freebox/translations/pt.json b/homeassistant/components/freebox/translations/pt.json index 09e13bc200..7eacd09c9d 100644 --- a/homeassistant/components/freebox/translations/pt.json +++ b/homeassistant/components/freebox/translations/pt.json @@ -3,6 +3,11 @@ "abort": { "already_configured": "Servidor j\u00e1 configurado" }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "register_failed": "Falha no registo, por favor tente novamente", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/freebox/translations/zh-Hant.json b/homeassistant/components/freebox/translations/zh-Hant.json index 608c5bbcba..734498585f 100644 --- a/homeassistant/components/freebox/translations/zh-Hant.json +++ b/homeassistant/components/freebox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index a1924296f7..45b73cf58e 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -2,6 +2,6 @@ "domain": "fritz", "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": ["fritzconnection==1.3.4"], + "requirements": ["fritzconnection==1.4.0"], "codeowners": [] } diff --git a/homeassistant/components/fritzbox/translations/pt.json b/homeassistant/components/fritzbox/translations/pt.json index a5b5cd26dc..9d2eadee61 100644 --- a/homeassistant/components/fritzbox/translations/pt.json +++ b/homeassistant/components/fritzbox/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "no_devices_found": "Nenhum dispositivo encontrado na rede" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "confirm": { "data": { diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index b09eac7761..7b85df577e 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index e06d3b881f..4879842ee2 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "fritzbox_callmonitor", "name": "AVM FRITZ!Box Call Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": ["fritzconnection==1.3.4"], + "requirements": ["fritzconnection==1.4.0"], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index 3eeac8bd8d..d2fe23a811 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "fritzbox_netmonitor", "name": "AVM FRITZ!Box Net Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", - "requirements": ["fritzconnection==1.3.4"], + "requirements": ["fritzconnection==1.4.0"], "codeowners": [] } diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index caf309e671..241b07fd59 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==20201212.0"], + "requirements": ["home-assistant-frontend==20201229.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/garmin_connect/translations/pt.json b/homeassistant/components/garmin_connect/translations/pt.json index b3b468ff3e..2d9b2f9e9c 100644 --- a/homeassistant/components/garmin_connect/translations/pt.json +++ b/homeassistant/components/garmin_connect/translations/pt.json @@ -4,6 +4,8 @@ "already_configured": "Conta j\u00e1 configurada" }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { @@ -11,7 +13,9 @@ "data": { "password": "Palavra-passe", "username": "Nome de Utilizador" - } + }, + "description": "Introduza as suas credenciais.", + "title": "Garmin Connect" } } } diff --git a/homeassistant/components/gdacs/translations/pt.json b/homeassistant/components/gdacs/translations/pt.json index 98180e1124..250400b6e2 100644 --- a/homeassistant/components/gdacs/translations/pt.json +++ b/homeassistant/components/gdacs/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geofency/translations/pt.json b/homeassistant/components/geofency/translations/pt.json index 4e20283462..11e5023bae 100644 --- a/homeassistant/components/geofency/translations/pt.json +++ b/homeassistant/components/geofency/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no Geofency. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." }, diff --git a/homeassistant/components/geofency/translations/zh-Hant.json b/homeassistant/components/geofency/translations/zh-Hant.json index 0bef632c5a..4ffe673045 100644 --- a/homeassistant/components/geofency/translations/zh-Hant.json +++ b/homeassistant/components/geofency/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/geonetnz_quakes/translations/pt.json b/homeassistant/components/geonetnz_quakes/translations/pt.json new file mode 100644 index 0000000000..d252c078a2 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/pt.json b/homeassistant/components/geonetnz_volcano/translations/pt.json index 98180e1124..88f4021b4a 100644 --- a/homeassistant/components/geonetnz_volcano/translations/pt.json +++ b/homeassistant/components/geonetnz_volcano/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json index ce80aa8786..ef2fec9f84 100644 --- a/homeassistant/components/gios/strings.json +++ b/homeassistant/components/gios/strings.json @@ -18,5 +18,10 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_location%]" } + }, + "system_health": { + "info": { + "can_reach_server": "Reach GIO\u015a server" + } } } diff --git a/homeassistant/components/gios/system_health.py b/homeassistant/components/gios/system_health.py new file mode 100644 index 0000000000..391a8c1aff --- /dev/null +++ b/homeassistant/components/gios/system_health.py @@ -0,0 +1,20 @@ +"""Provide info to system health.""" +from homeassistant.components import system_health +from homeassistant.core import HomeAssistant, callback + +API_ENDPOINT = "http://api.gios.gov.pl/" + + +@callback +def async_register( + hass: HomeAssistant, register: system_health.SystemHealthRegistration +) -> None: + """Register system health callbacks.""" + register.async_register_info(system_health_info) + + +async def system_health_info(hass): + """Get info for the info page.""" + return { + "can_reach_server": system_health.async_check_can_reach_url(hass, API_ENDPOINT) + } diff --git a/homeassistant/components/gios/translations/ca.json b/homeassistant/components/gios/translations/ca.json index 0f1e0b522e..8150e309b2 100644 --- a/homeassistant/components/gios/translations/ca.json +++ b/homeassistant/components/gios/translations/ca.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Servidor de GIO\u015a accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/cs.json b/homeassistant/components/gios/translations/cs.json index 9a552502af..8dea1f5e01 100644 --- a/homeassistant/components/gios/translations/cs.json +++ b/homeassistant/components/gios/translations/cs.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (polsk\u00fd hlavn\u00ed inspektor\u00e1t ochrany \u017eivotn\u00edho prost\u0159ed\u00ed)" } } + }, + "system_health": { + "info": { + "can_reach_server": "GIOS server dosa\u017een" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/en.json b/homeassistant/components/gios/translations/en.json index abc49b1f5a..86f05b8987 100644 --- a/homeassistant/components/gios/translations/en.json +++ b/homeassistant/components/gios/translations/en.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Reach GIO\u015a server" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/es.json b/homeassistant/components/gios/translations/es.json index 6888266716..011ddd0b6f 100644 --- a/homeassistant/components/gios/translations/es.json +++ b/homeassistant/components/gios/translations/es.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n del Medio Ambiente de Polonia)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Alcanzar el servidor GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/et.json b/homeassistant/components/gios/translations/et.json index 163407ffce..2d0906f73a 100644 --- a/homeassistant/components/gios/translations/et.json +++ b/homeassistant/components/gios/translations/et.json @@ -18,5 +18,10 @@ "title": "" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u00dchendus GIO\u015a serveriga" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json index f1d7d60315..26bf8386d6 100644 --- a/homeassistant/components/gios/translations/it.json +++ b/homeassistant/components/gios/translations/it.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Ispettorato capo polacco di protezione ambientale)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Raggiungi il server GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/no.json b/homeassistant/components/gios/translations/no.json index d80e3bcae1..038cbdc20a 100644 --- a/homeassistant/components/gios/translations/no.json +++ b/homeassistant/components/gios/translations/no.json @@ -18,5 +18,10 @@ "title": "" } } + }, + "system_health": { + "info": { + "can_reach_server": "N\u00e5 GIO\u015a-server" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/pl.json b/homeassistant/components/gios/translations/pl.json index 1b35ab9899..8bc909e2ba 100644 --- a/homeassistant/components/gios/translations/pl.json +++ b/homeassistant/components/gios/translations/pl.json @@ -18,5 +18,10 @@ "title": "G\u0142\u00f3wny Inspektorat Ochrony \u015arodowiska (GIO\u015a)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dost\u0119p do serwera GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/pt.json b/homeassistant/components/gios/translations/pt.json new file mode 100644 index 0000000000..47e36006ad --- /dev/null +++ b/homeassistant/components/gios/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/ru.json b/homeassistant/components/gios/translations/ru.json index 826cfc22d4..68d6ee44b0 100644 --- a/homeassistant/components/gios/translations/ru.json +++ b/homeassistant/components/gios/translations/ru.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u043a\u0430\u044f \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u044f \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b)" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/sl.json b/homeassistant/components/gios/translations/sl.json index f01728783c..4bbc28bfed 100644 --- a/homeassistant/components/gios/translations/sl.json +++ b/homeassistant/components/gios/translations/sl.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (glavni poljski in\u0161pektorat za varstvo okolja)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Dostop do GIOS stre\u017enika." + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/zh-Hans.json b/homeassistant/components/gios/translations/zh-Hans.json new file mode 100644 index 0000000000..72430b5e15 --- /dev/null +++ b/homeassistant/components/gios/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "can_reach_server": "\u53ef\u8bbf\u95ee GIO\u015a \u670d\u52a1\u5668" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/zh-Hant.json b/homeassistant/components/gios/translations/zh-Hant.json index 4b8668e08a..d72bc9bc01 100644 --- a/homeassistant/components/gios/translations/zh-Hant.json +++ b/homeassistant/components/gios/translations/zh-Hant.json @@ -18,5 +18,10 @@ "title": "GIO\u015a\uff08\u6ce2\u862d\u7e3d\u74b0\u5883\u4fdd\u8b77\u7763\u5bdf\u8655\uff09" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u9023\u7dda GIO\u015a \u4f3a\u670d\u5668" + } } } \ No newline at end of file diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py index 491d400411..69e4ce0c01 100644 --- a/homeassistant/components/glances/const.py +++ b/homeassistant/components/glances/const.py @@ -36,7 +36,9 @@ SENSOR_TYPES = { "process_thread": ["processcount", "Thread", "Count", CPU_ICON], "process_sleeping": ["processcount", "Sleeping", "Count", CPU_ICON], "cpu_use_percent": ["cpu", "CPU used", PERCENTAGE, CPU_ICON], - "sensor_temp": ["sensors", "Temp", TEMP_CELSIUS, "mdi:thermometer"], + "temperature_core": ["sensors", "temperature", TEMP_CELSIUS, "mdi:thermometer"], + "fan_speed": ["sensors", "fan speed", "RPM", "mdi:fan"], + "battery": ["sensors", "charge", PERCENTAGE, "mdi:battery"], "docker_active": ["docker", "Containers active", "", "mdi:docker"], "docker_cpu_use": ["docker", "Containers CPU used", PERCENTAGE, "mdi:docker"], "docker_memory_use": [ diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index fb36312cf1..4c534a90ae 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -34,16 +34,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): elif sensor_details[0] == "sensors": # sensors will provide temp for different devices for sensor in client.api.data[sensor_details[0]]: - dev.append( - GlancesSensor( - client, - name, - sensor["label"], - SENSOR_TYPES[sensor_type][1], - sensor_type, - SENSOR_TYPES[sensor_type], + if sensor["type"] == sensor_type: + dev.append( + GlancesSensor( + client, + name, + sensor["label"], + SENSOR_TYPES[sensor_type][1], + sensor_type, + SENSOR_TYPES[sensor_type], + ) ) - ) elif client.api.data[sensor_details[0]]: dev.append( GlancesSensor( @@ -156,11 +157,21 @@ class GlancesSensor(Entity): (disk["size"] - disk["used"]) / 1024 ** 3, 1, ) - elif self.type == "sensor_temp": + elif self.type == "battery": for sensor in value["sensors"]: - if sensor["label"] == self._sensor_name_prefix: - self._state = sensor["value"] - break + if sensor["type"] == "battery": + if sensor["label"] == self._sensor_name_prefix: + self._state = sensor["value"] + elif self.type == "fan_speed": + for sensor in value["sensors"]: + if sensor["type"] == "fan_speed": + if sensor["label"] == self._sensor_name_prefix: + self._state = sensor["value"] + elif self.type == "temperature_core": + for sensor in value["sensors"]: + if sensor["type"] == "temperature_core": + if sensor["label"] == self._sensor_name_prefix: + self._state = sensor["value"] elif self.type == "memory_use_percent": self._state = value["mem"]["percent"] elif self.type == "memory_use": diff --git a/homeassistant/components/glances/translations/pt.json b/homeassistant/components/glances/translations/pt.json index f7195cd0bf..0d8cc552dd 100644 --- a/homeassistant/components/glances/translations/pt.json +++ b/homeassistant/components/glances/translations/pt.json @@ -10,9 +10,21 @@ "user": { "data": { "host": "Servidor", + "name": "Nome", "password": "Palavra-passe", "port": "Porta", - "username": "Nome de Utilizador" + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" } } } diff --git a/homeassistant/components/glances/translations/zh-Hant.json b/homeassistant/components/glances/translations/zh-Hant.json index 0054edbdb0..d81ca02f6b 100644 --- a/homeassistant/components/glances/translations/zh-Hant.json +++ b/homeassistant/components/glances/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index 80db678f27..d79c03f017 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -1,7 +1,9 @@ { "config": { "error": { - "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse" + "cannot_connect": "Verbindungsfehler", + "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", + "unknown": "Unerwarteter Fehler" } } } \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/pt.json b/homeassistant/components/goalzero/translations/pt.json new file mode 100644 index 0000000000..ce945ba68d --- /dev/null +++ b/homeassistant/components/goalzero/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido.", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/zh-Hant.json b/homeassistant/components/goalzero/translations/zh-Hant.json index b033c16afb..5c25a8cb98 100644 --- a/homeassistant/components/goalzero/translations/zh-Hant.json +++ b/homeassistant/components/goalzero/translations/zh-Hant.json @@ -14,7 +14,7 @@ "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", + "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\u88dd\u7f6e\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" } } diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 48664bff39..0063342293 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) from homeassistant.core import Context, HomeAssistant, State, callback +from homeassistant.helpers.area_registry import AreaEntry from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import get_url from homeassistant.helpers.storage import Store @@ -39,6 +40,29 @@ SYNC_DELAY = 15 _LOGGER = logging.getLogger(__name__) +async def _get_area(hass, entity_id) -> Optional[AreaEntry]: + """Calculate the area for a entity_id.""" + dev_reg, ent_reg, area_reg = await gather( + hass.helpers.device_registry.async_get_registry(), + hass.helpers.entity_registry.async_get_registry(), + hass.helpers.area_registry.async_get_registry(), + ) + + entity_entry = ent_reg.async_get(entity_id) + if not entity_entry: + return None + + if entity_entry.area_id: + area_id = entity_entry.area_id + else: + device_entry = dev_reg.devices.get(entity_entry.device_id) + if not (device_entry and device_entry.area_id): + return None + area_id = device_entry.area_id + + return area_reg.areas.get(area_id) + + class AbstractConfig(ABC): """Hold the configuration for Google Assistant.""" @@ -450,25 +474,10 @@ class GoogleEntity: room = entity_config.get(CONF_ROOM_HINT) if room: device["roomHint"] = room - return device - - dev_reg, ent_reg, area_reg = await gather( - self.hass.helpers.device_registry.async_get_registry(), - self.hass.helpers.entity_registry.async_get_registry(), - self.hass.helpers.area_registry.async_get_registry(), - ) - - entity_entry = ent_reg.async_get(state.entity_id) - if not (entity_entry and entity_entry.device_id): - return device - - device_entry = dev_reg.devices.get(entity_entry.device_id) - if not (device_entry and device_entry.area_id): - return device - - area_entry = area_reg.areas.get(device_entry.area_id) - if area_entry and area_entry.name: - device["roomHint"] = area_entry.name + else: + area = await _get_area(self.hass, state.entity_id) + if area and area.name: + device["roomHint"] = area.name return device diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 1658fcec1f..69b276fc75 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -20,6 +20,7 @@ CONF_SPEED = "speed" CONF_PITCH = "pitch" CONF_GAIN = "gain" CONF_PROFILES = "profiles" +CONF_TEXT_TYPE = "text_type" SUPPORTED_LANGUAGES = [ "ar-XA", @@ -84,6 +85,9 @@ MIN_GAIN = -96.0 MAX_GAIN = 16.0 DEFAULT_GAIN = 0 +SUPPORTED_TEXT_TYPES = ["text", "ssml"] +DEFAULT_TEXT_TYPE = "text" + SUPPORTED_PROFILES = [ "wearable-class-device", "handset-class-device", @@ -103,6 +107,7 @@ SUPPORTED_OPTIONS = [ CONF_PITCH, CONF_GAIN, CONF_PROFILES, + CONF_TEXT_TYPE, ] GENDER_SCHEMA = vol.All( @@ -116,6 +121,7 @@ SPEED_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_SPEED, max=MAX_SPEED PITCH_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_PITCH, max=MAX_PITCH)) GAIN_SCHEMA = vol.All(vol.Coerce(float), vol.Clamp(min=MIN_GAIN, max=MAX_GAIN)) PROFILES_SCHEMA = vol.All(cv.ensure_list, [vol.In(SUPPORTED_PROFILES)]) +TEXT_TYPE_SCHEMA = vol.All(vol.Lower, vol.In(SUPPORTED_TEXT_TYPES)) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -128,6 +134,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): PITCH_SCHEMA, vol.Optional(CONF_GAIN, default=DEFAULT_GAIN): GAIN_SCHEMA, vol.Optional(CONF_PROFILES, default=[]): PROFILES_SCHEMA, + vol.Optional(CONF_TEXT_TYPE, default=DEFAULT_TEXT_TYPE): TEXT_TYPE_SCHEMA, } ) @@ -144,14 +151,15 @@ async def async_get_engine(hass, config, discovery_info=None): return GoogleCloudTTSProvider( hass, key_file, - config.get(CONF_LANG), - config.get(CONF_GENDER), - config.get(CONF_VOICE), - config.get(CONF_ENCODING), - config.get(CONF_SPEED), - config.get(CONF_PITCH), - config.get(CONF_GAIN), - config.get(CONF_PROFILES), + config[CONF_LANG], + config[CONF_GENDER], + config[CONF_VOICE], + config[CONF_ENCODING], + config[CONF_SPEED], + config[CONF_PITCH], + config[CONF_GAIN], + config[CONF_PROFILES], + config[CONF_TEXT_TYPE], ) @@ -170,6 +178,7 @@ class GoogleCloudTTSProvider(Provider): pitch=0, gain=0, profiles=None, + text_type=DEFAULT_TEXT_TYPE, ): """Init Google Cloud TTS service.""" self.hass = hass @@ -182,6 +191,7 @@ class GoogleCloudTTSProvider(Provider): self._pitch = pitch self._gain = gain self._profiles = profiles + self._text_type = text_type if key_file: self._client = texttospeech.TextToSpeechClient.from_service_account_json( @@ -216,6 +226,7 @@ class GoogleCloudTTSProvider(Provider): CONF_PITCH: self._pitch, CONF_GAIN: self._gain, CONF_PROFILES: self._profiles, + CONF_TEXT_TYPE: self._text_type, } async def async_get_tts_audio(self, message, language, options=None): @@ -224,11 +235,12 @@ class GoogleCloudTTSProvider(Provider): { vol.Optional(CONF_GENDER, default=self._gender): GENDER_SCHEMA, vol.Optional(CONF_VOICE, default=self._voice): VOICE_SCHEMA, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): SCHEMA_ENCODING, + vol.Optional(CONF_ENCODING, default=self._encoding): SCHEMA_ENCODING, vol.Optional(CONF_SPEED, default=self._speed): SPEED_SCHEMA, - vol.Optional(CONF_PITCH, default=self._speed): SPEED_SCHEMA, - vol.Optional(CONF_GAIN, default=DEFAULT_GAIN): GAIN_SCHEMA, - vol.Optional(CONF_PROFILES, default=[]): PROFILES_SCHEMA, + vol.Optional(CONF_PITCH, default=self._pitch): PITCH_SCHEMA, + vol.Optional(CONF_GAIN, default=self._gain): GAIN_SCHEMA, + vol.Optional(CONF_PROFILES, default=self._profiles): PROFILES_SCHEMA, + vol.Optional(CONF_TEXT_TYPE, default=self._text_type): TEXT_TYPE_SCHEMA, } ) options = options_schema(options) @@ -239,8 +251,9 @@ class GoogleCloudTTSProvider(Provider): language = _voice[:5] try: + params = {options[CONF_TEXT_TYPE]: message} # pylint: disable=no-member - synthesis_input = texttospeech.types.SynthesisInput(text=message) + synthesis_input = texttospeech.types.SynthesisInput(**params) voice = texttospeech.types.VoiceSelectionParams( language_code=language, @@ -250,10 +263,10 @@ class GoogleCloudTTSProvider(Provider): audio_config = texttospeech.types.AudioConfig( audio_encoding=texttospeech.enums.AudioEncoding[_encoding], - speaking_rate=options.get(CONF_SPEED), - pitch=options.get(CONF_PITCH), - volume_gain_db=options.get(CONF_GAIN), - effects_profile_id=options.get(CONF_PROFILES), + speaking_rate=options[CONF_SPEED], + pitch=options[CONF_PITCH], + volume_gain_db=options[CONF_GAIN], + effects_profile_id=options[CONF_PROFILES], ) # pylint: enable=no-member diff --git a/homeassistant/components/gpslogger/translations/pt.json b/homeassistant/components/gpslogger/translations/pt.json index 8602afcabf..47e4e6e383 100644 --- a/homeassistant/components/gpslogger/translations/pt.json +++ b/homeassistant/components/gpslogger/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no GPslogger. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." }, diff --git a/homeassistant/components/gpslogger/translations/zh-Hant.json b/homeassistant/components/gpslogger/translations/zh-Hant.json index 324a92cd8b..9c5448266e 100644 --- a/homeassistant/components/gpslogger/translations/zh-Hant.json +++ b/homeassistant/components/gpslogger/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 96f2401e8d..92b56a4804 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,12 +1,14 @@ """The Gree Climate integration.""" +import asyncio import logging from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .bridge import CannotConnect, DeviceHelper -from .const import DOMAIN +from .bridge import CannotConnect, DeviceDataUpdateCoordinator, DeviceHelper +from .const import COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -40,23 +42,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) devices.append(device) - hass.data[DOMAIN]["devices"] = devices - hass.data[DOMAIN]["pending"] = devices + coordinators = [DeviceDataUpdateCoordinator(hass, d) for d in devices] + await asyncio.gather(*[x.async_refresh() for x in coordinators]) + + hass.data[DOMAIN][COORDINATOR] = coordinators hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, SWITCH_DOMAIN) + ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - entry, CLIMATE_DOMAIN + results = asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN), + hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), ) + unload_ok = all(await results) if unload_ok: hass.data[DOMAIN].pop("devices", None) - hass.data[DOMAIN].pop("pending", None) + hass.data[DOMAIN].pop(CLIMATE_DOMAIN, None) + hass.data[DOMAIN].pop(SWITCH_DOMAIN, None) return unload_ok diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index 44adaf970b..3fbf4a21fb 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -1,11 +1,71 @@ """Helper and wrapper classes for Gree module.""" +from datetime import timedelta +import logging from typing import List from greeclimate.device import Device, DeviceInfo from greeclimate.discovery import Discovery -from greeclimate.exceptions import DeviceNotBoundError +from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError from homeassistant import exceptions +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, MAX_ERRORS + +_LOGGER = logging.getLogger(__name__) + + +class DeviceDataUpdateCoordinator(DataUpdateCoordinator): + """Manages polling for state changes from the device.""" + + def __init__(self, hass: HomeAssistant, device: Device): + """Initialize the data update coordinator.""" + DataUpdateCoordinator.__init__( + self, + hass, + _LOGGER, + name=f"{DOMAIN}-{device.device_info.name}", + update_interval=timedelta(seconds=60), + ) + self.device = device + self._error_count = 0 + + async def _async_update_data(self): + """Update the state of the device.""" + try: + await self.device.update_state() + except DeviceTimeoutError as error: + self._error_count += 1 + + # Under normal conditions GREE units timeout every once in a while + if self.last_update_success and self._error_count >= MAX_ERRORS: + _LOGGER.warning( + "Device is unavailable: %s (%s)", + self.name, + self.device.device_info, + ) + raise UpdateFailed(error) from error + else: + if not self.last_update_success and self._error_count: + _LOGGER.warning( + "Device is available: %s (%s)", + self.name, + str(self.device.device_info), + ) + + self._error_count = 0 + + async def push_state_update(self): + """Send state updates to the physical device.""" + try: + return await self.device.push_state_update() + except DeviceTimeoutError: + _LOGGER.warning( + "Timeout send state update to: %s (%s)", + self.name, + self.device.device_info, + ) class DeviceHelper: diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 724903ef36..6a33e3341b 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -1,5 +1,4 @@ """Support for interface with a Gree climate systems.""" -from datetime import timedelta import logging from typing import List @@ -10,7 +9,6 @@ from greeclimate.device import ( TemperatureUnits, VerticalSwing, ) -from greeclimate.exceptions import DeviceTimeoutError from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -45,12 +43,13 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( + COORDINATOR, DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, - MAX_ERRORS, MAX_TEMP, MIN_TEMP, TARGET_TEMPERATURE_STEP, @@ -58,9 +57,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) -PARALLEL_UPDATES = 0 - HVAC_MODES = { Mode.Auto: HVAC_MODE_AUTO, Mode.Cool: HVAC_MODE_COOL, @@ -101,85 +97,21 @@ SUPPORTED_FEATURES = ( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Gree HVAC device from a config entry.""" async_add_entities( - GreeClimateEntity(device) for device in hass.data[DOMAIN].pop("pending") + [ + GreeClimateEntity(coordinator) + for coordinator in hass.data[DOMAIN][COORDINATOR] + ] ) -class GreeClimateEntity(ClimateEntity): +class GreeClimateEntity(CoordinatorEntity, ClimateEntity): """Representation of a Gree HVAC device.""" - def __init__(self, device): + def __init__(self, coordinator): """Initialize the Gree device.""" - self._device = device - self._name = device.device_info.name - self._mac = device.device_info.mac - self._available = False - self._error_count = 0 - - async def async_update(self): - """Update the state of the device.""" - try: - await self._device.update_state() - - if not self._available and self._error_count: - _LOGGER.warning( - "Device is available: %s (%s)", - self._name, - str(self._device.device_info), - ) - - self._available = True - self._error_count = 0 - except DeviceTimeoutError: - self._error_count += 1 - - # Under normal conditions GREE units timeout every once in a while - if self._available and self._error_count >= MAX_ERRORS: - self._available = False - _LOGGER.warning( - "Device is unavailable: %s (%s)", - self._name, - self._device.device_info, - ) - except Exception: # pylint: disable=broad-except - # Under normal conditions GREE units timeout every once in a while - if self._available: - self._available = False - _LOGGER.exception( - "Unknown exception caught during update by gree device: %s (%s)", - self._name, - self._device.device_info, - ) - - async def _push_state_update(self): - """Send state updates to the physical device.""" - try: - return await self._device.push_state_update() - except DeviceTimeoutError: - self._error_count += 1 - - # Under normal conditions GREE units timeout every once in a while - if self._available and self._error_count >= MAX_ERRORS: - self._available = False - _LOGGER.warning( - "Device timedout while sending state update: %s (%s)", - self._name, - self._device.device_info, - ) - except Exception: # pylint: disable=broad-except - # Under normal conditions GREE units timeout every once in a while - if self._available: - self._available = False - _LOGGER.exception( - "Unknown exception caught while sending state update to: %s (%s)", - self._name, - self._device.device_info, - ) - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self._available + super().__init__(coordinator) + self._name = coordinator.device.device_info.name + self._mac = coordinator.device.device_info.mac @property def name(self) -> str: @@ -204,7 +136,7 @@ class GreeClimateEntity(ClimateEntity): @property def temperature_unit(self) -> str: """Return the temperature units for the device.""" - units = self._device.temperature_units + units = self.coordinator.device.temperature_units return TEMP_CELSIUS if units == TemperatureUnits.C else TEMP_FAHRENHEIT @property @@ -220,7 +152,7 @@ class GreeClimateEntity(ClimateEntity): @property def target_temperature(self) -> float: """Return the target temperature for the device.""" - return self._device.target_temperature + return self.coordinator.device.target_temperature async def async_set_temperature(self, **kwargs): """Set new target temperature.""" @@ -234,8 +166,9 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.target_temperature = round(temperature) - await self._push_state_update() + self.coordinator.device.target_temperature = round(temperature) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def min_temp(self) -> float: @@ -255,10 +188,10 @@ class GreeClimateEntity(ClimateEntity): @property def hvac_mode(self) -> str: """Return the current HVAC mode for the device.""" - if not self._device.power: + if not self.coordinator.device.power: return HVAC_MODE_OFF - return HVAC_MODES.get(self._device.mode) + return HVAC_MODES.get(self.coordinator.device.mode) async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" @@ -272,15 +205,17 @@ class GreeClimateEntity(ClimateEntity): ) if hvac_mode == HVAC_MODE_OFF: - self._device.power = False - await self._push_state_update() + self.coordinator.device.power = False + await self.coordinator.push_state_update() + self.async_write_ha_state() return - if not self._device.power: - self._device.power = True + if not self.coordinator.device.power: + self.coordinator.device.power = True - self._device.mode = HVAC_MODES_REVERSE.get(hvac_mode) - await self._push_state_update() + self.coordinator.device.mode = HVAC_MODES_REVERSE.get(hvac_mode) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def hvac_modes(self) -> List[str]: @@ -292,13 +227,13 @@ class GreeClimateEntity(ClimateEntity): @property def preset_mode(self) -> str: """Return the current preset mode for the device.""" - if self._device.steady_heat: + if self.coordinator.device.steady_heat: return PRESET_AWAY - if self._device.power_save: + if self.coordinator.device.power_save: return PRESET_ECO - if self._device.sleep: + if self.coordinator.device.sleep: return PRESET_SLEEP - if self._device.turbo: + if self.coordinator.device.turbo: return PRESET_BOOST return PRESET_NONE @@ -313,21 +248,22 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.steady_heat = False - self._device.power_save = False - self._device.turbo = False - self._device.sleep = False + self.coordinator.device.steady_heat = False + self.coordinator.device.power_save = False + self.coordinator.device.turbo = False + self.coordinator.device.sleep = False if preset_mode == PRESET_AWAY: - self._device.steady_heat = True + self.coordinator.device.steady_heat = True elif preset_mode == PRESET_ECO: - self._device.power_save = True + self.coordinator.device.power_save = True elif preset_mode == PRESET_BOOST: - self._device.turbo = True + self.coordinator.device.turbo = True elif preset_mode == PRESET_SLEEP: - self._device.sleep = True + self.coordinator.device.sleep = True - await self._push_state_update() + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def preset_modes(self) -> List[str]: @@ -337,7 +273,7 @@ class GreeClimateEntity(ClimateEntity): @property def fan_mode(self) -> str: """Return the current fan mode for the device.""" - speed = self._device.fan_speed + speed = self.coordinator.device.fan_speed return FAN_MODES.get(speed) async def async_set_fan_mode(self, fan_mode): @@ -345,8 +281,9 @@ class GreeClimateEntity(ClimateEntity): if fan_mode not in FAN_MODES_REVERSE: raise ValueError(f"Invalid fan mode: {fan_mode}") - self._device.fan_speed = FAN_MODES_REVERSE.get(fan_mode) - await self._push_state_update() + self.coordinator.device.fan_speed = FAN_MODES_REVERSE.get(fan_mode) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def fan_modes(self) -> List[str]: @@ -356,8 +293,8 @@ class GreeClimateEntity(ClimateEntity): @property def swing_mode(self) -> str: """Return the current swing mode for the device.""" - h_swing = self._device.horizontal_swing == HorizontalSwing.FullSwing - v_swing = self._device.vertical_swing == VerticalSwing.FullSwing + h_swing = self.coordinator.device.horizontal_swing == HorizontalSwing.FullSwing + v_swing = self.coordinator.device.vertical_swing == VerticalSwing.FullSwing if h_swing and v_swing: return SWING_BOTH @@ -378,14 +315,15 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.horizontal_swing = HorizontalSwing.Center - self._device.vertical_swing = VerticalSwing.FixedMiddle + self.coordinator.device.horizontal_swing = HorizontalSwing.Center + self.coordinator.device.vertical_swing = VerticalSwing.FixedMiddle if swing_mode in (SWING_BOTH, SWING_HORIZONTAL): - self._device.horizontal_swing = HorizontalSwing.FullSwing + self.coordinator.device.horizontal_swing = HorizontalSwing.FullSwing if swing_mode in (SWING_BOTH, SWING_VERTICAL): - self._device.vertical_swing = VerticalSwing.FullSwing + self.coordinator.device.vertical_swing = VerticalSwing.FullSwing - await self._push_state_update() + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def swing_modes(self) -> List[str]: diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 95435bb3bd..9c64506225 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -1,6 +1,7 @@ """Constants for the Gree Climate integration.""" DOMAIN = "gree" +COORDINATOR = "coordinator" FAN_MEDIUM_LOW = "medium low" FAN_MEDIUM_HIGH = "medium high" diff --git a/homeassistant/components/gree/strings.json b/homeassistant/components/gree/strings.json index ad8f0f41ae..9f3518bcf8 100644 --- a/homeassistant/components/gree/strings.json +++ b/homeassistant/components/gree/strings.json @@ -10,4 +10,4 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py new file mode 100644 index 0000000000..f4e9792a58 --- /dev/null +++ b/homeassistant/components/gree/switch.py @@ -0,0 +1,78 @@ +"""Support for interface with a Gree climate systems.""" +import logging +from typing import Optional + +from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import COORDINATOR, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Gree HVAC device from a config entry.""" + async_add_entities( + [ + GreeSwitchEntity(coordinator) + for coordinator in hass.data[DOMAIN][COORDINATOR] + ] + ) + + +class GreeSwitchEntity(CoordinatorEntity, SwitchEntity): + """Representation of a Gree HVAC device.""" + + def __init__(self, coordinator): + """Initialize the Gree device.""" + super().__init__(coordinator) + self._name = coordinator.device.device_info.name + " Panel Light" + self._mac = coordinator.device.device_info.mac + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._name + + @property + def unique_id(self) -> str: + """Return a unique id for the device.""" + return f"{self._mac}-panel-light" + + @property + def icon(self) -> Optional[str]: + """Return the icon for the device.""" + return "mdi:lightbulb" + + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self._name, + "identifiers": {(DOMAIN, self._mac)}, + "manufacturer": "Gree", + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, + } + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_SWITCH + + @property + def is_on(self) -> bool: + """Return if the light is turned on.""" + return self.coordinator.device.light + + async def async_turn_on(self, **kwargs): + """Turn the entity on.""" + self.coordinator.device.light = True + await self.coordinator.push_state_update() + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + self.coordinator.device.light = False + await self.coordinator.push_state_update() + self.async_write_ha_state() diff --git a/homeassistant/components/gree/translations/pt.json b/homeassistant/components/gree/translations/pt.json new file mode 100644 index 0000000000..e25888655a --- /dev/null +++ b/homeassistant/components/gree/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/zh-Hant.json b/homeassistant/components/gree/translations/zh-Hant.json index 91a0dc60be..90c98e491d 100644 --- a/homeassistant/components/gree/translations/zh-Hant.json +++ b/homeassistant/components/gree/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/griddy/translations/pt.json b/homeassistant/components/griddy/translations/pt.json index 0c5c776056..9b067d35f8 100644 --- a/homeassistant/components/griddy/translations/pt.json +++ b/homeassistant/components/griddy/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" } } diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 7407782bc3..27770d690f 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindungsfehler" }, "step": { "user": { diff --git a/homeassistant/components/guardian/translations/pt.json b/homeassistant/components/guardian/translations/pt.json index 0077ceddd4..91def9afb9 100644 --- a/homeassistant/components/guardian/translations/pt.json +++ b/homeassistant/components/guardian/translations/pt.json @@ -1,8 +1,14 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { + "ip_address": "Endere\u00e7o IP", "port": "Porta" } } diff --git a/homeassistant/components/guardian/translations/zh-Hant.json b/homeassistant/components/guardian/translations/zh-Hant.json index e40cef4f94..bf3a1606e6 100644 --- a/homeassistant/components/guardian/translations/zh-Hant.json +++ b/homeassistant/components/guardian/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, @@ -11,10 +11,10 @@ "ip_address": "IP \u4f4d\u5740", "port": "\u901a\u8a0a\u57e0" }, - "description": "\u8a2d\u5b9a\u5340\u57df Elexa Guardian \u8a2d\u5099\u3002" + "description": "\u8a2d\u5b9a\u5340\u57df Elexa Guardian \u88dd\u7f6e\u3002" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Guardian \u8a2d\u5099\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Guardian \u88dd\u7f6e\uff1f" } } } diff --git a/homeassistant/components/hangouts/translations/no.json b/homeassistant/components/hangouts/translations/no.json index 1ea5c9020e..fa34150963 100644 --- a/homeassistant/components/hangouts/translations/no.json +++ b/homeassistant/components/hangouts/translations/no.json @@ -6,13 +6,13 @@ }, "error": { "invalid_2fa": "Ugyldig totrinnsbekreftelse, vennligst pr\u00f8v igjen.", - "invalid_2fa_method": "Ugyldig 2FA-metode (Bekreft p\u00e5 telefon).", + "invalid_2fa_method": "Ugyldig totrinnsbekreftelse-metode (Bekreft p\u00e5 telefon)", "invalid_login": "Ugyldig innlogging, vennligst pr\u00f8v igjen." }, "step": { "2fa": { "data": { - "2fa": "2FA Pin" + "2fa": "Totrinnsbekreftelse Pin" }, "description": "", "title": "Totrinnsbekreftelse" diff --git a/homeassistant/components/harmony/translations/pt.json b/homeassistant/components/harmony/translations/pt.json index 2a9c91681b..04374af8e8 100644 --- a/homeassistant/components/harmony/translations/pt.json +++ b/homeassistant/components/harmony/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/harmony/translations/zh-Hant.json b/homeassistant/components/harmony/translations/zh-Hant.json index 4ab79dd603..608a2150c6 100644 --- a/homeassistant/components/harmony/translations/zh-Hant.json +++ b/homeassistant/components/harmony/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index ac804794b4..0cdb931842 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -3,7 +3,7 @@ "info": { "board": "Placa", "disk_total": "Total disc", - "disk_used": "Disc utilitzat", + "disk_used": "Emmagatzematge utilitzat", "docker_version": "Versi\u00f3 de Docker", "healthy": "Saludable", "host_os": "Sistema operatiu amfitri\u00f3", diff --git a/homeassistant/components/hassio/translations/hu.json b/homeassistant/components/hassio/translations/hu.json index 4119802eb7..216e8d391b 100644 --- a/homeassistant/components/hassio/translations/hu.json +++ b/homeassistant/components/hassio/translations/hu.json @@ -4,6 +4,7 @@ "disk_total": "\u00d6sszes hely", "disk_used": "Felhaszn\u00e1lt hely", "docker_version": "Docker verzi\u00f3", + "healthy": "Eg\u00e9szs\u00e9ges", "host_os": "Gazdag\u00e9p oper\u00e1ci\u00f3s rendszer", "installed_addons": "Telep\u00edtett kieg\u00e9sz\u00edt\u0151k", "supervisor_api": "Adminisztr\u00e1tor API", diff --git a/homeassistant/components/hassio/translations/it.json b/homeassistant/components/hassio/translations/it.json index 937b6099bd..385a0eedff 100644 --- a/homeassistant/components/hassio/translations/it.json +++ b/homeassistant/components/hassio/translations/it.json @@ -5,7 +5,7 @@ "disk_total": "Disco totale", "disk_used": "Disco utilizzato", "docker_version": "Versione Docker", - "healthy": "Sano", + "healthy": "Integrit\u00e0", "host_os": "Sistema Operativo Host", "installed_addons": "Componenti aggiuntivi installati", "supervisor_api": "API Supervisore", diff --git a/homeassistant/components/hassio/translations/nl.json b/homeassistant/components/hassio/translations/nl.json index 981cb51c83..fca08d49d7 100644 --- a/homeassistant/components/hassio/translations/nl.json +++ b/homeassistant/components/hassio/translations/nl.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "disk_total": "Totale schijfruimte", + "disk_used": "Gebruikte schijfruimte", + "docker_version": "Docker versie", + "healthy": "Gezond", + "host_os": "Host-besturingssysteem", + "installed_addons": "Ge\u00efnstalleerde add-ons", + "supervisor_api": "Supervisor API", + "supervisor_version": "Supervisor versie", + "supported": "Ondersteund", + "update_channel": "Update kanaal", + "version_api": "API Versie" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/pt.json b/homeassistant/components/hassio/translations/pt.json index 973601e744..06083bae75 100644 --- a/homeassistant/components/hassio/translations/pt.json +++ b/homeassistant/components/hassio/translations/pt.json @@ -1,10 +1,13 @@ { "system_health": { "info": { + "board": "Tabela", "disk_total": "Disco Total", - "disk_used": "Disco Usado", + "disk_used": "Disco Utilizado", "docker_version": "Vers\u00e3o Docker", + "healthy": "Saud\u00e1vel", "host_os": "Sistema operativo anfitri\u00e3o", + "installed_addons": "Add-ons instalados", "supervisor_api": "API do Supervisor", "supervisor_version": "Vers\u00e3o do Supervisor", "supported": "Suportado" diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index 981cb51c83..d368ac0fb3 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -1,3 +1,13 @@ { + "system_health": { + "info": { + "board": "Panel", + "disk_total": "Disk Toplam\u0131", + "disk_used": "Kullan\u0131lan Disk", + "docker_version": "Docker S\u00fcr\u00fcm\u00fc", + "healthy": "Sa\u011fl\u0131kl\u0131", + "host_os": "Ana Bilgisayar \u0130\u015fletim Sistemi" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/zh-Hans.json b/homeassistant/components/hassio/translations/zh-Hans.json index 95b4a8a8a6..0d74360b8f 100644 --- a/homeassistant/components/hassio/translations/zh-Hans.json +++ b/homeassistant/components/hassio/translations/zh-Hans.json @@ -12,7 +12,7 @@ "supervisor_version": "Supervisor \u7248\u672c", "supported": "\u53d7\u652f\u6301", "update_channel": "\u66f4\u65b0\u901a\u9053", - "version_api": "API\u7248\u672c" + "version_api": "API \u7248\u672c" } }, "title": "Hass.io" diff --git a/homeassistant/components/heos/translations/de.json b/homeassistant/components/heos/translations/de.json index 7c5e1d87c9..92ab6c1c8f 100644 --- a/homeassistant/components/heos/translations/de.json +++ b/homeassistant/components/heos/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/heos/translations/pt.json b/homeassistant/components/heos/translations/pt.json index ce7cbc3f54..a893104829 100644 --- a/homeassistant/components/heos/translations/pt.json +++ b/homeassistant/components/heos/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/heos/translations/zh-Hant.json b/homeassistant/components/heos/translations/zh-Hant.json index 95ddf7e51a..fe3e8fb7b4 100644 --- a/homeassistant/components/heos/translations/zh-Hant.json +++ b/homeassistant/components/heos/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -11,7 +11,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u8a2d\u5099 IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", + "description": "\u8acb\u8f38\u5165\u4e3b\u6a5f\u6bb5\u540d\u7a31\u6216 Heos \u88dd\u7f6e IP \u4f4d\u5740\uff08\u5df2\u900f\u904e\u6709\u7dda\u7db2\u8def\u9023\u7dda\uff09\u3002", "title": "\u9023\u7dda\u81f3 Heos" } } diff --git a/homeassistant/components/hisense_aehw4a1/translations/pt.json b/homeassistant/components/hisense_aehw4a1/translations/pt.json new file mode 100644 index 0000000000..7a4274b008 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json b/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json index 56ad5128d9..e08a2c5f6d 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json +++ b/homeassistant/components/hisense_aehw4a1/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/hlk_sw16/translations/de.json b/homeassistant/components/hlk_sw16/translations/de.json index 6f39806287..94b8d6526d 100644 --- a/homeassistant/components/hlk_sw16/translations/de.json +++ b/homeassistant/components/hlk_sw16/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hlk_sw16/translations/zh-Hant.json b/homeassistant/components/hlk_sw16/translations/zh-Hant.json index 8bf65ef6ee..cad7d736a9 100644 --- a/homeassistant/components/hlk_sw16/translations/zh-Hant.json +++ b/homeassistant/components/hlk_sw16/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py index 38f487a98a..301bd1976e 100644 --- a/homeassistant/components/home_connect/__init__.py +++ b/homeassistant/components/home_connect/__init__.py @@ -32,7 +32,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -PLATFORMS = ["binary_sensor", "sensor", "switch"] +PLATFORMS = ["binary_sensor", "light", "sensor", "switch"] async def async_setup(hass: HomeAssistant, config: dict) -> bool: diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index e1ee75297f..8db8afa3a6 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -168,6 +168,30 @@ class DeviceWithDoor(HomeConnectDevice): } +class DeviceWithLight(HomeConnectDevice): + """Device that has lighting.""" + + def get_light_entity(self): + """Get a dictionary with info about the lighting.""" + return { + "device": self, + "desc": "Light", + "ambient": None, + } + + +class DeviceWithAmbientLight(HomeConnectDevice): + """Device that has ambient lighting.""" + + def get_ambientlight_entity(self): + """Get a dictionary with info about the ambient lighting.""" + return { + "device": self, + "desc": "AmbientLight", + "ambient": True, + } + + class Dryer(DeviceWithDoor, DeviceWithPrograms): """Dryer class.""" @@ -202,7 +226,7 @@ class Dryer(DeviceWithDoor, DeviceWithPrograms): } -class Dishwasher(DeviceWithDoor, DeviceWithPrograms): +class Dishwasher(DeviceWithDoor, DeviceWithAmbientLight, DeviceWithPrograms): """Dishwasher class.""" PROGRAMS = [ @@ -335,7 +359,7 @@ class CoffeeMaker(DeviceWithPrograms): return {"switch": program_switches, "sensor": program_sensors} -class Hood(DeviceWithPrograms): +class Hood(DeviceWithLight, DeviceWithAmbientLight, DeviceWithPrograms): """Hood class.""" PROGRAMS = [ @@ -346,9 +370,15 @@ class Hood(DeviceWithPrograms): def get_entity_info(self): """Get a dictionary with infos about the associated entities.""" + light_entity = self.get_light_entity() + ambientlight_entity = self.get_ambientlight_entity() program_sensors = self.get_program_sensors() program_switches = self.get_program_switches() - return {"switch": program_switches, "sensor": program_sensors} + return { + "switch": program_switches, + "sensor": program_sensors, + "light": [light_entity, ambientlight_entity], + } class FridgeFreezer(DeviceWithDoor): diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py index 10eb5dfd1e..22ce4dba67 100644 --- a/homeassistant/components/home_connect/const.py +++ b/homeassistant/components/home_connect/const.py @@ -11,6 +11,18 @@ BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off" BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby" BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram" BSH_OPERATION_STATE = "BSH.Common.Status.OperationState" + +COOKING_LIGHTING = "Cooking.Common.Setting.Lighting" +COOKING_LIGHTING_BRIGHTNESS = "Cooking.Common.Setting.LightingBrightness" + +BSH_AMBIENT_LIGHT_ENABLED = "BSH.Common.Setting.AmbientLightEnabled" +BSH_AMBIENT_LIGHT_BRIGHTNESS = "BSH.Common.Setting.AmbientLightBrightness" +BSH_AMBIENT_LIGHT_COLOR = "BSH.Common.Setting.AmbientLightColor" +BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR = ( + "BSH.Common.EnumType.AmbientLightColor.CustomColor" +) +BSH_AMBIENT_LIGHT_CUSTOM_COLOR = "BSH.Common.Setting.AmbientLightCustomColor" + BSH_DOOR_STATE = "BSH.Common.Status.DoorState" SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities" diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py new file mode 100644 index 0000000000..a8d0d7ffbd --- /dev/null +++ b/homeassistant/components/home_connect/light.py @@ -0,0 +1,203 @@ +"""Provides a light for Home Connect.""" +import logging +from math import ceil + +from homeconnect.api import HomeConnectError + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_HS_COLOR, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + LightEntity, +) +import homeassistant.util.color as color_util + +from .const import ( + BSH_AMBIENT_LIGHT_BRIGHTNESS, + BSH_AMBIENT_LIGHT_COLOR, + BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR, + BSH_AMBIENT_LIGHT_CUSTOM_COLOR, + BSH_AMBIENT_LIGHT_ENABLED, + COOKING_LIGHTING, + COOKING_LIGHTING_BRIGHTNESS, + DOMAIN, +) +from .entity import HomeConnectEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Home Connect light.""" + + def get_entities(): + """Get a list of entities.""" + entities = [] + hc_api = hass.data[DOMAIN][config_entry.entry_id] + for device_dict in hc_api.devices: + entity_dicts = device_dict.get("entities", {}).get("light", []) + entity_list = [HomeConnectLight(**d) for d in entity_dicts] + entities += entity_list + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +class HomeConnectLight(HomeConnectEntity, LightEntity): + """Light for Home Connect.""" + + def __init__(self, device, desc, ambient): + """Initialize the entity.""" + super().__init__(device, desc) + self._state = None + self._brightness = None + self._hs_color = None + self._ambient = ambient + if self._ambient: + self._brightness_key = BSH_AMBIENT_LIGHT_BRIGHTNESS + self._key = BSH_AMBIENT_LIGHT_ENABLED + self._custom_color_key = BSH_AMBIENT_LIGHT_CUSTOM_COLOR + self._color_key = BSH_AMBIENT_LIGHT_COLOR + else: + self._brightness_key = COOKING_LIGHTING_BRIGHTNESS + self._key = COOKING_LIGHTING + self._custom_color_key = None + self._color_key = None + + @property + def is_on(self): + """Return true if the light is on.""" + return bool(self._state) + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._brightness + + @property + def hs_color(self): + """Return the color property.""" + return self._hs_color + + @property + def supported_features(self): + """Flag supported features.""" + if self._ambient: + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_BRIGHTNESS + + async def async_turn_on(self, **kwargs): + """Switch the light on, change brightness, change color.""" + if self._ambient: + if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs: + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._color_key, + BSH_AMBIENT_LIGHT_COLOR_CUSTOM_COLOR, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying selecting customcolor: %s", err) + if self._brightness is not None: + brightness = 10 + ceil(self._brightness / 255 * 90) + if ATTR_BRIGHTNESS in kwargs: + brightness = 10 + ceil(kwargs[ATTR_BRIGHTNESS] / 255 * 90) + + hs_color = kwargs.get(ATTR_HS_COLOR, default=self._hs_color) + + if hs_color is not None: + rgb = color_util.color_hsv_to_RGB(*hs_color, brightness) + hex_val = color_util.color_rgb_to_hex(rgb[0], rgb[1], rgb[2]) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._custom_color_key, + f"#{hex_val}", + ) + except HomeConnectError as err: + _LOGGER.error( + "Error while trying setting the color: %s", err + ) + else: + _LOGGER.debug("Switching ambient light on for: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._key, + True, + ) + except HomeConnectError as err: + _LOGGER.error( + "Error while trying to turn on ambient light: %s", err + ) + + elif ATTR_BRIGHTNESS in kwargs: + _LOGGER.debug("Changing brightness for: %s", self.name) + brightness = 10 + ceil(kwargs[ATTR_BRIGHTNESS] / 255 * 90) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._brightness_key, + brightness, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying set the brightness: %s", err) + else: + _LOGGER.debug("Switching light on for: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._key, + True, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn on light: %s", err) + + self.async_entity_update() + + async def async_turn_off(self, **kwargs): + """Switch the light off.""" + _LOGGER.debug("Switching light off for: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._key, + False, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn off light: %s", err) + self.async_entity_update() + + async def async_update(self): + """Update the light's status.""" + if self.device.appliance.status.get(self._key, {}).get("value") is True: + self._state = True + elif self.device.appliance.status.get(self._key, {}).get("value") is False: + self._state = False + else: + self._state = None + + _LOGGER.debug("Updated, new light state: %s", self._state) + + if self._ambient: + color = self.device.appliance.status.get(self._custom_color_key, {}) + + if not color: + self._hs_color = None + self._brightness = None + else: + colorvalue = color.get("value")[1:] + rgb = color_util.rgb_hex_to_rgb_list(colorvalue) + hsv = color_util.color_RGB_to_hsv(rgb[0], rgb[1], rgb[2]) + self._hs_color = [hsv[0], hsv[1]] + self._brightness = ceil((hsv[2] - 10) * 255 / 90) + _LOGGER.debug("Updated, new brightness: %s", self._brightness) + + else: + brightness = self.device.appliance.status.get(self._brightness_key, {}) + if brightness is None: + self._brightness = None + else: + self._brightness = ceil((brightness.get("value") - 10) * 255 / 90) + _LOGGER.debug("Updated, new brightness: %s", self._brightness) diff --git a/homeassistant/components/home_connect/translations/no.json b/homeassistant/components/home_connect/translations/no.json index 3d95664d0a..e929ea2f91 100644 --- a/homeassistant/components/home_connect/translations/no.json +++ b/homeassistant/components/home_connect/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "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 hjelpseksjonen]({docs_url})" }, "create_entry": { diff --git a/homeassistant/components/home_connect/translations/pt.json b/homeassistant/components/home_connect/translations/pt.json new file mode 100644 index 0000000000..eb27f25953 --- /dev/null +++ b/homeassistant/components/home_connect/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/ca.json b/homeassistant/components/homeassistant/translations/ca.json index 8f9931c682..97e3d088af 100644 --- a/homeassistant/components/homeassistant/translations/ca.json +++ b/homeassistant/components/homeassistant/translations/ca.json @@ -3,7 +3,7 @@ "info": { "arch": "Arquitectura de la CPU", "chassis": "Xass\u00eds", - "dev": "Desenvolupament", + "dev": "Desenvolupador", "docker": "Docker", "docker_version": "Docker", "hassio": "Supervisor", diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json new file mode 100644 index 0000000000..45768a9f12 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/de.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/nl.json b/homeassistant/components/homeassistant/translations/nl.json new file mode 100644 index 0000000000..338a019019 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/nl.json @@ -0,0 +1,18 @@ +{ + "system_health": { + "info": { + "dev": "Ontwikkeling", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "Type installatie", + "os_version": "Versie van het besturingssysteem", + "python_version": "Python versie", + "supervisor": "Supervisor", + "timezone": "Tijdzone", + "version": "Versie", + "virtualenv": "Virtuele omgeving" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/pt.json b/homeassistant/components/homeassistant/translations/pt.json index 7bf340567f..c16c2c3baa 100644 --- a/homeassistant/components/homeassistant/translations/pt.json +++ b/homeassistant/components/homeassistant/translations/pt.json @@ -2,9 +2,10 @@ "system_health": { "info": { "arch": "Arquitetura do Processador", + "chassis": "Chassis", "dev": "Desenvolvimento", - "docker": "Docker", - "docker_version": "Docker", + "docker": "", + "docker_version": "", "hassio": "Supervisor", "host_os": "Sistema Operativo do Home Assistant", "installation_type": "Tipo de Instala\u00e7\u00e3o", diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json new file mode 100644 index 0000000000..1ff8ea1b3a --- /dev/null +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -0,0 +1,19 @@ +{ + "system_health": { + "info": { + "arch": "CPU Mimarisi", + "dev": "Geli\u015ftirme", + "docker": "Konteyner", + "docker_version": "Konteyner", + "host_os": "Home Assistant OS", + "installation_type": "Kurulum T\u00fcr\u00fc", + "os_name": "\u0130\u015fletim Sistemi Ailesi", + "os_version": "\u0130\u015fletim Sistemi S\u00fcr\u00fcm\u00fc", + "python_version": "Python S\u00fcr\u00fcm\u00fc", + "supervisor": "S\u00fcperviz\u00f6r", + "timezone": "Saat dilimi", + "version": "S\u00fcr\u00fcm", + "virtualenv": "Sanal Ortam" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 65b70a8463..9d50e62fcd 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -128,7 +128,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): default_domains = [] if self._async_current_names() else DEFAULT_DOMAINS setup_schema = vol.Schema( { - vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): bool, vol.Required( CONF_INCLUDE_DOMAINS, default=default_domains ): cv.multi_select(SUPPORTED_DOMAINS), diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 5270ac6970..3f12eca0f5 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -30,7 +30,7 @@ }, "advanced": { "data": { - "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]", + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", "safe_mode": "Safe Mode (enable only if pairing fails)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", @@ -42,7 +42,6 @@ "step": { "user": { "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", "include_domains": "Domains to include" }, "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", diff --git a/homeassistant/components/homekit/translations/nl.json b/homeassistant/components/homekit/translations/nl.json index ca41ff6758..2733d6bd12 100644 --- a/homeassistant/components/homekit/translations/nl.json +++ b/homeassistant/components/homekit/translations/nl.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "port_name_in_use": "Er is al een bridge met dezelfde naam of poort geconfigureerd." + "port_name_in_use": "Er is al een bridge of apparaat met dezelfde naam of poort geconfigureerd." }, "step": { "pairing": { @@ -35,6 +35,11 @@ "description": "Controleer alle camera's die native H.264-streams ondersteunen. Als de camera geen H.264-stream uitvoert, transcodeert het systeem de video naar H.264 voor HomeKit. Transcodering vereist een performante CPU en het is onwaarschijnlijk dat dit werkt op computers met \u00e9\u00e9n bord.", "title": "Selecteer de videocodec van de camera." }, + "include_exclude": { + "data": { + "entities": "Entiteiten" + } + }, "init": { "data": { "include_domains": "Op te nemen domeinen", diff --git a/homeassistant/components/homekit/translations/pt.json b/homeassistant/components/homekit/translations/pt.json new file mode 100644 index 0000000000..b5da3fdfc9 --- /dev/null +++ b/homeassistant/components/homekit/translations/pt.json @@ -0,0 +1,43 @@ +{ + "config": { + "step": { + "pairing": { + "title": "Emparelhar HomeKit" + }, + "user": { + "data": { + "include_domains": "Dom\u00ednios a incluir" + }, + "title": "Activar o HomeKit" + } + } + }, + "options": { + "step": { + "advanced": { + "title": "Configura\u00e7\u00e3o avan\u00e7ada" + }, + "cameras": { + "title": "Selecione o codec de v\u00eddeo da c\u00e2mera." + }, + "include_exclude": { + "data": { + "entities": "Entidades", + "mode": "Modo" + }, + "title": "Selecione as entidades a serem expostas" + }, + "init": { + "data": { + "include_domains": "Dom\u00ednios a incluir", + "mode": "Modo" + }, + "title": "Selecione os dom\u00ednios a serem expostos." + }, + "yaml": { + "description": "Esta entrada \u00e9 controlada via YAML", + "title": "Ajustar as op\u00e7\u00f5es do HomeKit" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index e3e9247e7c..0f1093f5b5 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -48,7 +48,7 @@ "include_domains": "\u5305\u542b Domain", "mode": "\u6a21\u5f0f" }, - "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002\u5a92\u9ad4\u64ad\u653e\u5668\u9700\u8981\u4ee5\u96fb\u8996\u8a2d\u5099\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u4f7f\u7528\u3002\"\u5305\u542b Domains\"\u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002", + "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002\u5a92\u9ad4\u64ad\u653e\u5668\u9700\u8981\u4ee5\u96fb\u8996\u88dd\u7f6e\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u4f7f\u7528\u3002\"\u5305\u542b Domains\"\u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002", "title": "\u9078\u64c7\u6240\u8981\u63a5\u901a\u7684 Domain\u3002" }, "yaml": { diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index d6231efe7a..1142a476bc 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -49,7 +49,7 @@ class Fan(HomeAccessory): """ def __init__(self, *args): - """Initialize a new Light accessory object.""" + """Initialize a new Fan accessory object.""" super().__init__(*args, category=CATEGORY_FAN) chars = [] state = self.hass.states.get(self.entity_id) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 6d0f5f22d7..54e2e9f92a 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -89,6 +89,20 @@ HC_HEAT_COOL_HEAT = 1 HC_HEAT_COOL_COOL = 2 HC_HEAT_COOL_AUTO = 3 +HC_HEAT_COOL_PREFER_HEAT = [ + HC_HEAT_COOL_AUTO, + HC_HEAT_COOL_HEAT, + HC_HEAT_COOL_COOL, + HC_HEAT_COOL_OFF, +] + +HC_HEAT_COOL_PREFER_COOL = [ + HC_HEAT_COOL_AUTO, + HC_HEAT_COOL_COOL, + HC_HEAT_COOL_HEAT, + HC_HEAT_COOL_OFF, +] + HC_MIN_TEMP = 10 HC_MAX_TEMP = 38 @@ -236,7 +250,7 @@ class Thermostat(HomeAccessory): state = self.hass.states.get(self.entity_id) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - hvac_mode = self.hass.states.get(self.entity_id).state + hvac_mode = state.state homekit_hvac_mode = HC_HASS_TO_HOMEKIT[hvac_mode] if CHAR_TARGET_HEATING_COOLING in char_values: @@ -244,19 +258,37 @@ class Thermostat(HomeAccessory): # Ignore it if its the same mode if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode: target_hc = char_values[CHAR_TARGET_HEATING_COOLING] - if target_hc in self.hc_homekit_to_hass: - service = SERVICE_SET_HVAC_MODE_THERMOSTAT - hass_value = self.hc_homekit_to_hass[target_hc] - params = {ATTR_HVAC_MODE: hass_value} - events.append( - f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" - ) - else: - _LOGGER.warning( - "The entity: %s does not have a %s mode", - self.entity_id, - target_hc, - ) + if target_hc not in self.hc_homekit_to_hass: + # If the target heating cooling state we want does not + # exist on the device, we have to sort it out + # based on the the current and target temperature since + # siri will always send HC_HEAT_COOL_AUTO in this case + # and hope for the best. + hc_target_temp = char_values.get(CHAR_TARGET_TEMPERATURE) + hc_current_temp = _get_current_temperature(state, self._unit) + hc_fallback_order = HC_HEAT_COOL_PREFER_HEAT + if ( + hc_target_temp is not None + and hc_current_temp is not None + and hc_target_temp < hc_current_temp + ): + hc_fallback_order = HC_HEAT_COOL_PREFER_COOL + for hc_fallback in hc_fallback_order: + if hc_fallback in self.hc_homekit_to_hass: + _LOGGER.debug( + "Siri requested target mode: %s and the device does not support, falling back to %s", + target_hc, + hc_fallback, + ) + target_hc = hc_fallback + break + + service = SERVICE_SET_HVAC_MODE_THERMOSTAT + hass_value = self.hc_homekit_to_hass[target_hc] + params = {ATTR_HVAC_MODE: hass_value} + events.append( + f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" + ) if CHAR_TARGET_TEMPERATURE in char_values: hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE] @@ -429,9 +461,8 @@ class Thermostat(HomeAccessory): self.char_current_heat_cool.set_value(homekit_hvac_action) # Update current temperature - current_temp = new_state.attributes.get(ATTR_CURRENT_TEMPERATURE) - if isinstance(current_temp, (int, float)): - current_temp = self._temperature_to_homekit(current_temp) + current_temp = _get_current_temperature(new_state, self._unit) + if current_temp is not None: if self.char_current_temp.value != current_temp: self.char_current_temp.set_value(current_temp) @@ -466,10 +497,8 @@ class Thermostat(HomeAccessory): self.char_heating_thresh_temp.set_value(heating_thresh) # Update target temperature - target_temp = new_state.attributes.get(ATTR_TEMPERATURE) - if isinstance(target_temp, (int, float)): - target_temp = self._temperature_to_homekit(target_temp) - elif features & SUPPORT_TARGET_TEMPERATURE_RANGE: + target_temp = _get_target_temperature(new_state, self._unit) + if target_temp is None and features & SUPPORT_TARGET_TEMPERATURE_RANGE: # Homekit expects a target temperature # even if the device does not support it hc_hvac_mode = self.char_target_heat_cool.value @@ -566,9 +595,8 @@ class WaterHeater(HomeAccessory): def async_update_state(self, new_state): """Update water_heater state after state change.""" # Update current and target temperature - temperature = new_state.attributes.get(ATTR_TEMPERATURE) - if isinstance(temperature, (int, float)): - temperature = temperature_to_homekit(temperature, self._unit) + temperature = _get_target_temperature(new_state, self._unit) + if temperature is not None: if temperature != self.char_current_temp.value: self.char_target_temp.set_value(temperature) @@ -606,3 +634,19 @@ def _get_temperature_range_from_state(state, unit, default_min, default_max): max_temp = min_temp return min_temp, max_temp + + +def _get_target_temperature(state, unit): + """Calculate the target temperature from a state.""" + target_temp = state.attributes.get(ATTR_TEMPERATURE) + if isinstance(target_temp, (int, float)): + return temperature_to_homekit(target_temp, unit) + return None + + +def _get_current_temperature(state, unit): + """Calculate the current temperature from a state.""" + target_temp = state.attributes.get(ATTR_CURRENT_TEMPERATURE) + if isinstance(target_temp, (int, float)): + return temperature_to_homekit(target_temp, unit) + return None diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index ed2d3c74d7..042bc4771c 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -19,6 +19,8 @@ from homeassistant.components.climate import ( ClimateEntity, ) from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -30,6 +32,7 @@ from homeassistant.components.climate.const import ( SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, SWING_OFF, SWING_VERTICAL, ) @@ -329,7 +332,9 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): return [ CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_CURRENT, + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD, CharacteristicsTypes.TEMPERATURE_TARGET, CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT, CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET, @@ -338,10 +343,23 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) - - await self.async_put_characteristics( - {CharacteristicsTypes.TEMPERATURE_TARGET: temp} - ) + heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) + cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + if temp is None: + temp = (cool_temp + heat_temp) / 2 + await self.async_put_characteristics( + { + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD: heat_temp, + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD: cool_temp, + CharacteristicsTypes.TEMPERATURE_TARGET: temp, + } + ) + else: + await self.async_put_characteristics( + {CharacteristicsTypes.TEMPERATURE_TARGET: temp} + ) async def async_set_humidity(self, humidity): """Set new target humidity.""" @@ -367,22 +385,57 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): @property def target_temperature(self): """Return the temperature we try to reach.""" + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + return None return self.service.value(CharacteristicsTypes.TEMPERATURE_TARGET) + @property + def target_temperature_high(self): + """Return the highbound target temperature we try to reach.""" + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT_COOL}: + return None + return self.service.value(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) + + @property + def target_temperature_low(self): + """Return the lowbound target temperature we try to reach.""" + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) not in {HVAC_MODE_HEAT_COOL}: + return None + return self.service.value(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD) + @property def min_temp(self): """Return the minimum target temp.""" - if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): - char = self.service[CharacteristicsTypes.TEMPERATURE_TARGET] - return char.minValue + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + min_temp = self.service[ + CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD + ].minValue + if min_temp is not None: + return min_temp + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + min_temp = self.service[CharacteristicsTypes.TEMPERATURE_TARGET].minValue + if min_temp is not None: + return min_temp return super().min_temp @property def max_temp(self): """Return the maximum target temp.""" - if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): - char = self.service[CharacteristicsTypes.TEMPERATURE_TARGET] - return char.maxValue + value = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET) + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT_COOL}: + max_temp = self.service[ + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ].maxValue + if max_temp is not None: + return max_temp + if MODE_HOMEKIT_TO_HASS.get(value) in {HVAC_MODE_HEAT, HVAC_MODE_COOL}: + max_temp = self.service[CharacteristicsTypes.TEMPERATURE_TARGET].maxValue + if max_temp is not None: + return max_temp return super().max_temp @property @@ -443,6 +496,11 @@ class HomeKitClimateEntity(HomeKitEntity, ClimateEntity): if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): features |= SUPPORT_TARGET_TEMPERATURE + if self.service.has( + CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD + ) and self.service.has(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD): + features |= SUPPORT_TARGET_TEMPERATURE_RANGE + if self.service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET): features |= SUPPORT_TARGET_HUMIDITY diff --git a/homeassistant/components/homekit_controller/translations/pt.json b/homeassistant/components/homekit_controller/translations/pt.json index deeb376e5e..c4ab5c1e63 100644 --- a/homeassistant/components/homekit_controller/translations/pt.json +++ b/homeassistant/components/homekit_controller/translations/pt.json @@ -18,6 +18,9 @@ }, "flow_title": "Acess\u00f3rio HomeKit: {name}", "step": { + "busy_error": { + "title": "O dispositivo j\u00e1 est\u00e1 a emparelhar com outro controlador" + }, "pair": { "data": { "pairing_code": "C\u00f3digo de emparelhamento" @@ -34,5 +37,22 @@ } } }, + "device_automation": { + "trigger_subtype": { + "button1": "Bot\u00e3o 1", + "button10": "Bot\u00e3o 10", + "button2": "Bot\u00e3o 2", + "button3": "Bot\u00e3o 3", + "button4": "Bot\u00e3o 4", + "button5": "Bot\u00e3o 5", + "button6": "Bot\u00e3o 6", + "button7": "Bot\u00e3o 7", + "button8": "Bot\u00e3o 8", + "button9": "Bot\u00e3o 9" + }, + "trigger_type": { + "single_press": "\"{subtype}\" pressionado" + } + }, "title": "Acess\u00f3rio HomeKit" } \ 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 75c46125c1..6490904a32 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hant.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hant.json @@ -1,49 +1,49 @@ { "config": { "abort": { - "accessory_not_found_error": "\u627e\u4e0d\u5230\u8a2d\u5099\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", + "accessory_not_found_error": "\u627e\u4e0d\u5230\u88dd\u7f6e\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002", "already_configured": "\u914d\u4ef6\u5df2\u7d93\u7531\u6b64\u63a7\u5236\u5668\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u8a2d\u5099\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", + "already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u88dd\u7f6e\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" + "invalid_config_entry": "\u6b64\u88dd\u7f6e\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": "\u88dd\u7f6e\u5ba3\u544a\u5c6c\u6027\u7121\u6548\u3002", + "no_devices": "\u627e\u4e0d\u5230\u4efb\u4f55\u672a\u914d\u5c0d\u88dd\u7f6e" }, "error": { "authentication_error": "Homekit \u4ee3\u78bc\u932f\u8aa4\uff0c\u8acb\u78ba\u5b9a\u5f8c\u518d\u8a66\u4e00\u6b21\u3002", - "max_peers_error": "\u8a2d\u5099\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", - "pairing_failed": "\u7576\u8a66\u5716\u8207\u8a2d\u5099\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u8a2d\u5099\u76ee\u524d\u4e0d\u652f\u63f4\u3002", + "max_peers_error": "\u88dd\u7f6e\u5df2\u7121\u5269\u9918\u914d\u5c0d\u7a7a\u9593\uff0c\u62d2\u7d55\u9032\u884c\u914d\u5c0d\u3002", + "pairing_failed": "\u7576\u8a66\u5716\u8207\u88dd\u7f6e\u914d\u5c0d\u6642\u767c\u751f\u7121\u6cd5\u8655\u7406\u932f\u8aa4\uff0c\u53ef\u80fd\u50c5\u70ba\u66ab\u6642\u5931\u6548\u3001\u6216\u8005\u88dd\u7f6e\u76ee\u524d\u4e0d\u652f\u63f4\u3002", "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" + "unknown_error": "\u88dd\u7f6e\u56de\u5831\u672a\u77e5\u932f\u8aa4\u3002\u914d\u5c0d\u5931\u6557\u3002" }, "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", - "title": "\u8a2d\u5099\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d" + "description": "\u53d6\u6d88\u6240\u6709\u63a7\u5236\u5668\u914d\u5c0d\uff0c\u6216\u8005\u91cd\u555f\u88dd\u7f6e\u3001\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", + "title": "\u88dd\u7f6e\u5df2\u7d93\u8207\u5176\u4ed6\u63a7\u5236\u5668\u914d\u5c0d" }, "max_tries_error": { - "description": "\u8a2d\u5099\u5df2\u8d85\u904e 100 \u6b21\u8a8d\u8b49\u5617\u8a66\u6b21\u6578\uff0c\u8acb\u5617\u8a66\u91cd\u65b0\u555f\u52d5\u8a2d\u5099\u3001\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", + "description": "\u88dd\u7f6e\u5df2\u8d85\u904e 100 \u6b21\u8a8d\u8b49\u5617\u8a66\u6b21\u6578\uff0c\u8acb\u5617\u8a66\u91cd\u65b0\u555f\u52d5\u88dd\u7f6e\u3001\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", "title": "\u5df2\u8d85\u904e\u6700\u5927\u9a57\u8b49\u5617\u8a66\u6b21\u6578" }, "pair": { "data": { "pairing_code": "\u8a2d\u5b9a\u4ee3\u78bc" }, - "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" + "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\u88dd\u7f6e\u6216\u8005\u5305\u88dd\u4e0a\u627e\u5230\u3002", + "title": "\u900f\u904e HomeKit \u914d\u4ef6\u901a\u8a0a\u5354\u5b9a\u6240\u914d\u5c0d\u88dd\u7f6e" }, "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", + "description": "\u88dd\u7f6e\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\u88dd\u7f6e\u5df2\u7d93\u8655\u65bc\u914d\u5c0d\u6a21\u5f0f\u3001\u6216\u91cd\u555f\u88dd\u7f6e\uff0c\u7136\u5f8c\u518d\u7e7c\u7e8c\u914d\u5c0d\u3002", "title": "\u8207\u914d\u4ef6\u901a\u8a0a\u932f\u8aa4" }, "user": { "data": { - "device": "\u8a2d\u5099" + "device": "\u88dd\u7f6e" }, - "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" + "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\u88dd\u7f6e\uff1a", + "title": "\u88dd\u7f6e\u9078\u64c7" } } }, diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 12bca8378c..57aaa2b0b0 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -6,6 +6,7 @@ from homematicip.aio.device import ( AsyncContactInterface, AsyncDevice, AsyncFullFlushContactInterface, + AsyncFullFlushContactInterface6, AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, @@ -91,6 +92,11 @@ async def async_setup_entry( entities.append( HomematicipMultiContactInterface(hap, device, channel=channel) ) + elif isinstance(device, AsyncFullFlushContactInterface6): + for channel in range(1, 7): + entities.append( + HomematicipMultiContactInterface(hap, device, channel=channel) + ) elif isinstance( device, (AsyncContactInterface, AsyncFullFlushContactInterface) ): @@ -224,9 +230,17 @@ class HomematicipTiltVibrationSensor(HomematicipBaseActionSensor): class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEntity): """Representation of the HomematicIP multi room/area contact interface.""" - def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: """Initialize the multi contact entity.""" - super().__init__(hap, device, channel=channel) + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) @property def device_class(self) -> str: @@ -244,30 +258,22 @@ class HomematicipMultiContactInterface(HomematicipGenericEntity, BinarySensorEnt ) -class HomematicipContactInterface(HomematicipGenericEntity, BinarySensorEntity): +class HomematicipContactInterface(HomematicipMultiContactInterface, BinarySensorEntity): """Representation of the HomematicIP contact interface.""" - @property - def device_class(self) -> str: - """Return the class of this sensor.""" - return DEVICE_CLASS_OPENING - - @property - def is_on(self) -> bool: - """Return true if the contact interface is on/open.""" - if self._device.windowState is None: - return None - return self._device.windowState != WindowState.CLOSED + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the multi contact entity.""" + super().__init__(hap, device, is_multi_channel=False) -class HomematicipShutterContact(HomematicipGenericEntity, BinarySensorEntity): +class HomematicipShutterContact(HomematicipMultiContactInterface, BinarySensorEntity): """Representation of the HomematicIP shutter contact.""" def __init__( self, hap: HomematicipHAP, device, has_additional_state: bool = False ) -> None: """Initialize the shutter contact.""" - super().__init__(hap, device) + super().__init__(hap, device, is_multi_channel=False) self.has_additional_state = has_additional_state @property @@ -275,13 +281,6 @@ class HomematicipShutterContact(HomematicipGenericEntity, BinarySensorEntity): """Return the class of this sensor.""" return DEVICE_CLASS_DOOR - @property - def is_on(self) -> bool: - """Return true if the shutter contact is on/open.""" - if self._device.windowState is None: - return None - return self._device.windowState != WindowState.CLOSED - @property def device_state_attributes(self) -> Dict[str, Any]: """Return the state attributes of the Shutter Contact.""" diff --git a/homeassistant/components/homematicip_cloud/const.py b/homeassistant/components/homematicip_cloud/const.py index 5c48de975f..4fb21febb4 100644 --- a/homeassistant/components/homematicip_cloud/const.py +++ b/homeassistant/components/homematicip_cloud/const.py @@ -1,19 +1,30 @@ """Constants for the HomematicIP Cloud component.""" import logging +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN + _LOGGER = logging.getLogger(".") DOMAIN = "homematicip_cloud" COMPONENTS = [ - "alarm_control_panel", - "binary_sensor", - "climate", - "cover", - "light", - "sensor", - "switch", - "weather", + ALARM_CONTROL_PANEL_DOMAIN, + BINARY_SENSOR_DOMAIN, + CLIMATE_DOMAIN, + COVER_DOMAIN, + LIGHT_DOMAIN, + SENSOR_DOMAIN, + SWITCH_DOMAIN, + WEATHER_DOMAIN, ] CONF_ACCESSPOINT = "accesspoint" diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 60d3867d05..29a06c558f 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -2,6 +2,8 @@ from typing import Optional from homematicip.aio.device import ( + AsyncBlindModule, + AsyncDinRailBlind4, AsyncFullFlushBlind, AsyncFullFlushShutter, AsyncGarageDoorModuleTormatic, @@ -34,7 +36,14 @@ async def async_setup_entry( hap = hass.data[HMIPC_DOMAIN][config_entry.unique_id] entities = [] for device in hap.home.devices: - if isinstance(device, AsyncFullFlushBlind): + if isinstance(device, AsyncBlindModule): + entities.append(HomematicipBlindModule(hap, device)) + elif isinstance(device, AsyncDinRailBlind4): + for channel in range(1, 5): + entities.append( + HomematicipMultiCoverSlats(hap, device, channel=channel) + ) + elif isinstance(device, AsyncFullFlushBlind): entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): entities.append(HomematicipCoverShutter(hap, device)) @@ -51,14 +60,21 @@ async def async_setup_entry( async_add_entities(entities) -class HomematicipCoverShutter(HomematicipGenericEntity, CoverEntity): - """Representation of the HomematicIP cover shutter.""" +class HomematicipBlindModule(HomematicipGenericEntity, CoverEntity): + """Representation of the HomematicIP blind module.""" @property def current_cover_position(self) -> int: """Return current position of cover.""" - if self._device.shutterLevel is not None: - return int((1 - self._device.shutterLevel) * 100) + if self._device.primaryShadingLevel is not None: + return int((1 - self._device.primaryShadingLevel) * 100) + return None + + @property + def current_cover_tilt_position(self) -> int: + """Return current tilt position of cover.""" + if self._device.secondaryShadingLevel is not None: + return int((1 - self._device.secondaryShadingLevel) * 100) return None async def async_set_cover_position(self, **kwargs) -> None: @@ -66,36 +82,144 @@ class HomematicipCoverShutter(HomematicipGenericEntity, CoverEntity): position = kwargs[ATTR_POSITION] # HmIP cover is closed:1 -> open:0 level = 1 - position / 100.0 - await self._device.set_shutter_level(level) + await self._device.set_primary_shading_level(primaryShadingLevel=level) + + async def async_set_cover_tilt_position(self, **kwargs) -> None: + """Move the cover to a specific tilt position.""" + position = kwargs[ATTR_TILT_POSITION] + # HmIP slats is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=level, + ) @property def is_closed(self) -> Optional[bool]: """Return if the cover is closed.""" - if self._device.shutterLevel is not None: - return self._device.shutterLevel == HMIP_COVER_CLOSED + if self._device.primaryShadingLevel is not None: + return self._device.primaryShadingLevel == HMIP_COVER_CLOSED return None async def async_open_cover(self, **kwargs) -> None: """Open the cover.""" - await self._device.set_shutter_level(HMIP_COVER_OPEN) + await self._device.set_primary_shading_level( + primaryShadingLevel=HMIP_COVER_OPEN + ) async def async_close_cover(self, **kwargs) -> None: """Close the cover.""" - await self._device.set_shutter_level(HMIP_COVER_CLOSED) + await self._device.set_primary_shading_level( + primaryShadingLevel=HMIP_COVER_CLOSED + ) async def async_stop_cover(self, **kwargs) -> None: """Stop the device if in motion.""" - await self._device.set_shutter_stop() + await self._device.stop() + + async def async_open_cover_tilt(self, **kwargs) -> None: + """Open the slats.""" + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=HMIP_SLATS_OPEN, + ) + + async def async_close_cover_tilt(self, **kwargs) -> None: + """Close the slats.""" + await self._device.set_secondary_shading_level( + primaryShadingLevel=self._device.primaryShadingLevel, + secondaryShadingLevel=HMIP_SLATS_CLOSED, + ) + + async def async_stop_cover_tilt(self, **kwargs) -> None: + """Stop the device if in motion.""" + await self._device.stop() -class HomematicipCoverSlats(HomematicipCoverShutter, CoverEntity): - """Representation of the HomematicIP cover slats.""" +class HomematicipMultiCoverShutter(HomematicipGenericEntity, CoverEntity): + """Representation of the HomematicIP cover shutter.""" + + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: + """Initialize the multi cover entity.""" + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) + + @property + def current_cover_position(self) -> int: + """Return current position of cover.""" + if self._device.functionalChannels[self._channel].shutterLevel is not None: + return int( + (1 - self._device.functionalChannels[self._channel].shutterLevel) * 100 + ) + return None + + async def async_set_cover_position(self, **kwargs) -> None: + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + # HmIP cover is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_shutter_level(level, self._channel) + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed.""" + if self._device.functionalChannels[self._channel].shutterLevel is not None: + return ( + self._device.functionalChannels[self._channel].shutterLevel + == HMIP_COVER_CLOSED + ) + return None + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.set_shutter_level(HMIP_COVER_OPEN, self._channel) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.set_shutter_level(HMIP_COVER_CLOSED, self._channel) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the device if in motion.""" + await self._device.set_shutter_stop(self._channel) + + +class HomematicipCoverShutter(HomematicipMultiCoverShutter, CoverEntity): + """Representation of the HomematicIP cover shutter.""" + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the multi cover entity.""" + super().__init__(hap, device, is_multi_channel=False) + + +class HomematicipMultiCoverSlats(HomematicipMultiCoverShutter, CoverEntity): + """Representation of the HomematicIP multi cover slats.""" + + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: + """Initialize the multi slats entity.""" + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) @property def current_cover_tilt_position(self) -> int: """Return current tilt position of cover.""" - if self._device.slatsLevel is not None: - return int((1 - self._device.slatsLevel) * 100) + if self._device.functionalChannels[self._channel].slatsLevel is not None: + return int( + (1 - self._device.functionalChannels[self._channel].slatsLevel) * 100 + ) return None async def async_set_cover_tilt_position(self, **kwargs) -> None: @@ -103,19 +227,27 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverEntity): position = kwargs[ATTR_TILT_POSITION] # HmIP slats is closed:1 -> open:0 level = 1 - position / 100.0 - await self._device.set_slats_level(level) + await self._device.set_slats_level(level, self._channel) async def async_open_cover_tilt(self, **kwargs) -> None: """Open the slats.""" - await self._device.set_slats_level(HMIP_SLATS_OPEN) + await self._device.set_slats_level(HMIP_SLATS_OPEN, self._channel) async def async_close_cover_tilt(self, **kwargs) -> None: """Close the slats.""" - await self._device.set_slats_level(HMIP_SLATS_CLOSED) + await self._device.set_slats_level(HMIP_SLATS_CLOSED, self._channel) async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" - await self._device.set_shutter_stop() + await self._device.set_shutter_stop(self._channel) + + +class HomematicipCoverSlats(HomematicipMultiCoverSlats, CoverEntity): + """Representation of the HomematicIP cover slats.""" + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the multi slats entity.""" + super().__init__(hap, device, is_multi_channel=False) class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): @@ -150,10 +282,69 @@ class HomematicipGarageDoorModule(HomematicipGenericEntity, CoverEntity): await self._device.send_door_command(DoorCommand.STOP) -class HomematicipCoverShutterGroup(HomematicipCoverSlats, CoverEntity): +class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity): """Representation of the HomematicIP cover shutter group.""" def __init__(self, hap: HomematicipHAP, device, post: str = "ShutterGroup") -> None: """Initialize switching group.""" device.modelType = f"HmIP-{post}" - super().__init__(hap, device, post) + super().__init__(hap, device, post, is_multi_channel=False) + + @property + def current_cover_position(self) -> int: + """Return current position of cover.""" + if self._device.shutterLevel is not None: + return int((1 - self._device.shutterLevel) * 100) + return None + + @property + def current_cover_tilt_position(self) -> int: + """Return current tilt position of cover.""" + if self._device.slatsLevel is not None: + return int((1 - self._device.slatsLevel) * 100) + return None + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed.""" + if self._device.shutterLevel is not None: + return self._device.shutterLevel == HMIP_COVER_CLOSED + return None + + async def async_set_cover_position(self, **kwargs) -> None: + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] + # HmIP cover is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_shutter_level(level) + + async def async_set_cover_tilt_position(self, **kwargs) -> None: + """Move the cover to a specific tilt position.""" + position = kwargs[ATTR_TILT_POSITION] + # HmIP slats is closed:1 -> open:0 + level = 1 - position / 100.0 + await self._device.set_slats_level(level) + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.set_shutter_level(HMIP_COVER_OPEN) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.set_shutter_level(HMIP_COVER_CLOSED) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the group if in motion.""" + await self._device.set_shutter_stop() + + async def async_open_cover_tilt(self, **kwargs) -> None: + """Open the slats.""" + await self._device.set_slats_level(HMIP_SLATS_OPEN) + + async def async_close_cover_tilt(self, **kwargs) -> None: + """Close the slats.""" + await self._device.set_slats_level(HMIP_SLATS_CLOSED) + + async def async_stop_cover_tilt(self, **kwargs) -> None: + """Stop the group if in motion.""" + await self._device.set_shutter_stop() diff --git a/homeassistant/components/homematicip_cloud/generic_entity.py b/homeassistant/components/homematicip_cloud/generic_entity.py index ce8b44f570..a8df0107ee 100644 --- a/homeassistant/components/homematicip_cloud/generic_entity.py +++ b/homeassistant/components/homematicip_cloud/generic_entity.py @@ -76,6 +76,7 @@ class HomematicipGenericEntity(Entity): device, post: Optional[str] = None, channel: Optional[int] = None, + is_multi_channel: Optional[bool] = False, ) -> None: """Initialize the generic entity.""" self._hap = hap @@ -83,6 +84,7 @@ class HomematicipGenericEntity(Entity): self._device = device self._post = post self._channel = channel + self._is_multi_channel = is_multi_channel # Marker showing that the HmIP device hase been removed. self.hmip_device_removed = False _LOGGER.info("Setting up %s (%s)", self.name, self._device.modelType) @@ -179,7 +181,7 @@ class HomematicipGenericEntity(Entity): name = None # Try to get a label from a channel. if hasattr(self._device, "functionalChannels"): - if self._channel: + if self._is_multi_channel: name = self._device.functionalChannels[self._channel].label else: if len(self._device.functionalChannels) > 1: @@ -190,7 +192,7 @@ class HomematicipGenericEntity(Entity): name = self._device.label if self._post: name = f"{name} {self._post}" - elif self._channel: + elif self._is_multi_channel: name = f"{name} Channel{self._channel}" # Add a prefix to the name if the homematic ip home has a name. @@ -213,7 +215,7 @@ class HomematicipGenericEntity(Entity): def unique_id(self) -> str: """Return a unique ID.""" unique_id = f"{self.__class__.__name__}_{self._device.id}" - if self._channel: + if self._is_multi_channel: unique_id = ( f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}" ) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index f387e7bfda..1909ff818b 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -8,6 +8,7 @@ from homematicip.aio.device import ( AsyncDimmer, AsyncFullFlushDimmer, AsyncPluggableDimmer, + AsyncWiredDimmer3, ) from homematicip.base.enums import RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel @@ -51,6 +52,9 @@ async def async_setup_entry( hap, device, device.bottomLightChannelIndex ) ) + elif isinstance(device, AsyncWiredDimmer3): + for channel in range(1, 4): + entities.append(HomematicipMultiDimmer(hap, device, channel=channel)) elif isinstance( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), @@ -99,22 +103,33 @@ class HomematicipLightMeasuring(HomematicipLight): return state_attr -class HomematicipDimmer(HomematicipGenericEntity, LightEntity): +class HomematicipMultiDimmer(HomematicipGenericEntity, LightEntity): """Representation of HomematicIP Cloud dimmer.""" - def __init__(self, hap: HomematicipHAP, device) -> None: + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: """Initialize the dimmer light entity.""" - super().__init__(hap, device) + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) @property def is_on(self) -> bool: """Return true if dimmer is on.""" - return self._device.dimLevel is not None and self._device.dimLevel > 0.0 + func_channel = self._device.functionalChannels[self._channel] + return func_channel.dimLevel is not None and func_channel.dimLevel > 0.0 @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - return int((self._device.dimLevel or 0.0) * 255) + return int( + (self._device.functionalChannels[self._channel].dimLevel or 0.0) * 255 + ) @property def supported_features(self) -> int: @@ -124,13 +139,23 @@ class HomematicipDimmer(HomematicipGenericEntity, LightEntity): async def async_turn_on(self, **kwargs) -> None: """Turn the dimmer on.""" if ATTR_BRIGHTNESS in kwargs: - await self._device.set_dim_level(kwargs[ATTR_BRIGHTNESS] / 255.0) + await self._device.set_dim_level( + kwargs[ATTR_BRIGHTNESS] / 255.0, self._channel + ) else: - await self._device.set_dim_level(1) + await self._device.set_dim_level(1, self._channel) async def async_turn_off(self, **kwargs) -> None: """Turn the dimmer off.""" - await self._device.set_dim_level(0) + await self._device.set_dim_level(0, self._channel) + + +class HomematicipDimmer(HomematicipMultiDimmer, LightEntity): + """Representation of HomematicIP Cloud dimmer.""" + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize the dimmer light entity.""" + super().__init__(hap, device, is_multi_channel=False) class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): @@ -139,9 +164,13 @@ class HomematicipNotificationLight(HomematicipGenericEntity, LightEntity): def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the notification light entity.""" if channel == 2: - super().__init__(hap, device, post="Top", channel=channel) + super().__init__( + hap, device, post="Top", channel=channel, is_multi_channel=True + ) else: - super().__init__(hap, device, post="Bottom", channel=channel) + super().__init__( + hap, device, post="Bottom", channel=channel, is_multi_channel=True + ) self._color_switcher = { RGBColorState.WHITE: [0.0, 0.0], diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 30ca5165c8..9f04569446 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==0.12.1"], + "requirements": ["homematicip==0.13.0"], "codeowners": ["@SukramJ"], "quality_scale": "platinum" } diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 72f9f94c21..9047ed9095 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -3,6 +3,7 @@ from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, + AsyncDinRailSwitch4, AsyncFullFlushInputSwitch, AsyncFullFlushSwitchMeasuring, AsyncHeatingSwitch2, @@ -44,6 +45,9 @@ async def async_setup_entry( elif isinstance(device, AsyncWiredSwitch8): for channel in range(1, 9): entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) + elif isinstance(device, AsyncDinRailSwitch4): + for channel in range(1, 5): + entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) elif isinstance( device, ( @@ -77,9 +81,17 @@ async def async_setup_entry( class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity): """Representation of the HomematicIP multi switch.""" - def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: + def __init__( + self, + hap: HomematicipHAP, + device, + channel=1, + is_multi_channel=True, + ) -> None: """Initialize the multi switch device.""" - super().__init__(hap, device, channel=channel) + super().__init__( + hap, device, channel=channel, is_multi_channel=is_multi_channel + ) @property def is_on(self) -> bool: @@ -95,25 +107,12 @@ class HomematicipMultiSwitch(HomematicipGenericEntity, SwitchEntity): await self._device.turn_off(self._channel) -class HomematicipSwitch(HomematicipGenericEntity, SwitchEntity): +class HomematicipSwitch(HomematicipMultiSwitch, SwitchEntity): """Representation of the HomematicIP switch.""" def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the switch device.""" - super().__init__(hap, device) - - @property - def is_on(self) -> bool: - """Return true if device is on.""" - return self._device.on - - async def async_turn_on(self, **kwargs) -> None: - """Turn the device on.""" - await self._device.turn_on() - - async def async_turn_off(self, **kwargs) -> None: - """Turn the device off.""" - await self._device.turn_off() + super().__init__(hap, device, is_multi_channel=False) class HomematicipGroupSwitch(HomematicipGenericEntity, SwitchEntity): diff --git a/homeassistant/components/homematicip_cloud/translations/no.json b/homeassistant/components/homematicip_cloud/translations/no.json index d28fe17a69..7d24da73e7 100644 --- a/homeassistant/components/homematicip_cloud/translations/no.json +++ b/homeassistant/components/homematicip_cloud/translations/no.json @@ -6,7 +6,7 @@ "unknown": "Uventet feil" }, "error": { - "invalid_sgtin_or_pin": "Ugyldig SGTIN eller PIN-kode , pr\u00f8v igjen.", + "invalid_sgtin_or_pin": "Ugyldig SGTIN eller PIN kode , pr\u00f8v igjen.", "press_the_button": "Vennligst trykk p\u00e5 den bl\u00e5 knappen.", "register_failed": "Kunne ikke registrere, vennligst pr\u00f8v igjen.", "timeout_button": "Bl\u00e5 knapp-trykk tok for lang tid, vennligst pr\u00f8v igjen." @@ -16,7 +16,7 @@ "data": { "hapid": "Tilgangspunkt ID (SGTIN)", "name": "Navn (valgfritt, brukes som navneprefiks for alle enheter)", - "pin": "PIN-kode" + "pin": "PIN kode" }, "title": "Velg HomematicIP tilgangspunkt" }, diff --git a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json index 21c896182f..066ce89c2b 100644 --- a/homeassistant/components/homematicip_cloud/translations/zh-Hant.json +++ b/homeassistant/components/homematicip_cloud/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "connection_aborted": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "Access point ID (SGTIN)", - "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u8a2d\u5099\u7684\u5b57\u9996\u7528\uff09", + "name": "\u540d\u7a31\uff08\u9078\u9805\uff0c\u7528\u4ee5\u4f5c\u70ba\u6240\u6709\u88dd\u7f6e\u7684\u5b57\u9996\u7528\uff09", "pin": "PIN \u78bc" }, "title": "\u9078\u64c7 HomematicIP Access point" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index fd57478483..b0cd7bb8b8 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.2", - "huawei-lte-api==1.4.12", + "huawei-lte-api==1.4.17", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 451720ffb1..7da997f12d 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -11,7 +11,8 @@ "incorrect_username": "Ung\u00fcltiger Benutzername", "invalid_url": "Ung\u00fcltige URL", "login_attempts_exceeded": "Maximale Anzahl von Anmeldeversuchen \u00fcberschritten. Bitte versuche es sp\u00e4ter erneut", - "response_error": "Unbekannter Fehler vom Ger\u00e4t" + "response_error": "Unbekannter Fehler vom Ger\u00e4t", + "unknown": "Unerwarteter Fehler" }, "flow_title": "Huawei LTE: {name}", "step": { diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index 81a8804ee4..34d057a9ab 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -7,7 +7,9 @@ "error": { "connection_timeout": "Liga\u00e7\u00e3o expirou", "incorrect_password": "Palavra-passe incorreta", - "incorrect_username": "Nome de Utilizador incorreto" + "incorrect_username": "Nome de Utilizador incorreto", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" }, "flow_title": "Huawei LTE: {name}", "step": { diff --git a/homeassistant/components/huawei_lte/translations/zh-Hant.json b/homeassistant/components/huawei_lte/translations/zh-Hant.json index f0ea5d28ff..c8b067c887 100644 --- a/homeassistant/components/huawei_lte/translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u8a2d\u5099" + "not_huawei_lte": "\u4e26\u975e\u83ef\u70ba LTE \u88dd\u7f6e" }, "error": { "connection_timeout": "\u9023\u7dda\u903e\u6642", @@ -12,7 +12,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "invalid_url": "\u7db2\u5740\u7121\u6548", "login_attempts_exceeded": "\u5df2\u9054\u5617\u8a66\u767b\u5165\u6700\u5927\u6b21\u6578\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66", - "response_error": "\u4f86\u81ea\u8a2d\u5099\u672a\u77e5\u932f\u8aa4", + "response_error": "\u4f86\u81ea\u88dd\u7f6e\u672a\u77e5\u932f\u8aa4", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "\u83ef\u70ba LTE\uff1a{name}", @@ -23,7 +23,7 @@ "url": "\u7db2\u5740", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8f38\u5165\u8a2d\u5099\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u8a2d\u5099 Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", + "description": "\u8f38\u5165\u88dd\u7f6e\u5b58\u53d6\u8a73\u7d30\u8cc7\u6599\u3002\u6307\u5b9a\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u70ba\u9078\u9805\u8f38\u5165\uff0c\u4f46\u958b\u555f\u5c07\u652f\u63f4\u66f4\u591a\u6574\u5408\u529f\u80fd\u3002\u6b64\u5916\uff0c\u4f7f\u7528\u6388\u6b0a\u9023\u7dda\uff0c\u53ef\u80fd\u5c0e\u81f4\u6574\u5408\u555f\u7528\u5f8c\uff0c\u7531\u5916\u90e8\u9023\u7dda\u81f3 Home Assistant \u88dd\u7f6e Web \u4ecb\u9762\u51fa\u73fe\u67d0\u4e9b\u554f\u984c\uff0c\u53cd\u4e4b\u4ea6\u7136\u3002", "title": "\u8a2d\u5b9a\u83ef\u70ba LTE" } } @@ -34,7 +34,7 @@ "data": { "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", - "track_new_devices": "\u8ffd\u8e64\u65b0\u8a2d\u5099" + "track_new_devices": "\u8ffd\u8e64\u65b0\u88dd\u7f6e" } } } diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index eef0cc82c1..8eabbbb08c 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -3,9 +3,11 @@ "abort": { "all_configured": "Todos os hubs Philips Hue j\u00e1 est\u00e3o configurados", "already_configured": "Hue j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "cannot_connect": "N\u00e3o foi poss\u00edvel conectar-se ao hub", "discover_timeout": "Nenhum hub Hue descoberto", "no_bridges": "Nenhum hub Philips Hue descoberto", + "not_hue_bridge": "N\u00e3o \u00e9 uma bridge Hue", "unknown": "Ocorreu um erro desconhecido" }, "error": { diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index 9a5c0b2b54..ffb2b3a0e5 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -2,12 +2,12 @@ "config": { "abort": { "all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210", - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "cannot_connect": "\u9023\u7dda\u5931\u6557", "discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge", "no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge", - "not_hue_bridge": "\u975e Hue Bridge \u8a2d\u5099", + "not_hue_bridge": "\u975e Hue Bridge \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/humidifier/translations/bg.json b/homeassistant/components/humidifier/translations/bg.json new file mode 100644 index 0000000000..21aa58a9e6 --- /dev/null +++ b/homeassistant/components/humidifier/translations/bg.json @@ -0,0 +1,7 @@ +{ + "state": { + "_": { + "off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0435\u043d" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/sv.json b/homeassistant/components/humidifier/translations/sv.json new file mode 100644 index 0000000000..325e9f2e6a --- /dev/null +++ b/homeassistant/components/humidifier/translations/sv.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} st\u00e4ngdes av" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/pt.json b/homeassistant/components/hunterdouglas_powerview/translations/pt.json index 8b7889f0d1..ef5279e090 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/pt.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json index 85d167fb04..e78e05855c 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hvv_departures/translations/pt.json b/homeassistant/components/hvv_departures/translations/pt.json index 45e45ab85f..cbd43a04cf 100644 --- a/homeassistant/components/hvv_departures/translations/pt.json +++ b/homeassistant/components/hvv_departures/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hvv_departures/translations/zh-Hant.json b/homeassistant/components/hvv_departures/translations/zh-Hant.json index a965fa3881..df1eb910d2 100644 --- a/homeassistant/components/hvv_departures/translations/zh-Hant.json +++ b/homeassistant/components/hvv_departures/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 13dd977b7d..05494d1486 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -8,8 +8,8 @@ from hyperion import client, const as hyperion_const from pkg_resources import parse_version from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONF_TOKEN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -85,6 +85,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True +async def _create_reauth_flow( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_REAUTH}, data=config_entry.data + ) + ) + + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Hyperion from a config entry.""" host = config_entry.data[CONF_HOST] @@ -92,8 +103,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b token = config_entry.data.get(CONF_TOKEN) hyperion_client = await async_create_connect_hyperion_client( - host, port, token=token + host, port, token=token, raw_connection=True ) + + # Client won't connect? => Not ready. if not hyperion_client: raise ConfigEntryNotReady version = await hyperion_client.async_sysinfo_version() @@ -110,6 +123,31 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b except ValueError: pass + # Client needs authentication, but no token provided? => Reauth. + auth_resp = await hyperion_client.async_is_auth_required() + if ( + auth_resp is not None + and client.ResponseOK(auth_resp) + and auth_resp.get(hyperion_const.KEY_INFO, {}).get( + hyperion_const.KEY_REQUIRED, False + ) + and token is None + ): + await _create_reauth_flow(hass, config_entry) + return False + + # Client login doesn't work? => Reauth. + if not await hyperion_client.async_client_login(): + await _create_reauth_flow(hass, config_entry) + return False + + # Cannot switch instance or cannot load state? => Not ready. + if ( + not await hyperion_client.async_client_switch_instance() + or not client.ServerInfoResponseOK(await hyperion_client.async_get_serverinfo()) + ): + raise ConfigEntryNotReady + hyperion_client.set_callbacks( { f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": lambda json: ( @@ -139,17 +177,17 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ] ) hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append( - config_entry.add_update_listener(_async_options_updated) + config_entry.add_update_listener(_async_entry_updated) ) hass.async_create_task(setup_then_listen()) return True -async def _async_options_updated( +async def _async_entry_updated( hass: HomeAssistantType, config_entry: ConfigEntry ) -> None: - """Handle options update.""" + """Handle entry updates.""" await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/hyperion/config_flow.py b/homeassistant/components/hyperion/config_flow.py index aef74e530b..11ab3289d1 100644 --- a/homeassistant/components/hyperion/config_flow.py +++ b/homeassistant/components/hyperion/config_flow.py @@ -12,11 +12,19 @@ import voluptuous as vol from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import ( CONN_CLASS_LOCAL_PUSH, + SOURCE_REAUTH, ConfigEntry, ConfigFlow, OptionsFlow, ) -from homeassistant.const import CONF_BASE, CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN +from homeassistant.const import ( + CONF_BASE, + CONF_HOST, + CONF_ID, + CONF_PORT, + CONF_SOURCE, + CONF_TOKEN, +) from homeassistant.core import callback from homeassistant.helpers.typing import ConfigType @@ -35,13 +43,13 @@ from .const import ( _LOGGER = logging.getLogger(__name__) _LOGGER.setLevel(logging.DEBUG) -# +------------------+ +------------------+ +--------------------+ -# |Step: SSDP | |Step: user | |Step: import | -# | | | | | | -# |Input: | |Input: | |Input: | -# +------------------+ +------------------+ +--------------------+ -# v v v -# +----------------------+-----------------------+ +# +------------------+ +------------------+ +--------------------+ +--------------------+ +# |Step: SSDP | |Step: user | |Step: import | |Step: reauth | +# | | | | | | | | +# |Input: | |Input: | |Input: | |Input: | +# +------------------+ +------------------+ +--------------------+ +--------------------+ +# v v v v +# +-------------------+-----------------------+--------------------+ # Auth not | Auth | # required? | required? | # | v @@ -82,7 +90,7 @@ _LOGGER.setLevel(logging.DEBUG) # | # v # +----------------+ -# | Create! | +# | Create/Update! | # +----------------+ # A note on choice of discovery mechanisms: Hyperion supports both Zeroconf and SSDP out @@ -140,6 +148,17 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="cannot_connect") return await self._advance_to_auth_step_if_necessary(hyperion_client) + async def async_step_reauth( + self, + config_data: ConfigType, + ) -> Dict[str, Any]: + """Handle a reauthentication flow.""" + self._data = dict(config_data) + async with self._create_client(raw_connection=True) as hyperion_client: + if not hyperion_client: + return self.async_abort(reason="cannot_connect") + return await self._advance_to_auth_step_if_necessary(hyperion_client) + async def async_step_ssdp( # type: ignore[override] self, discovery_info: Dict[str, Any] ) -> Dict[str, Any]: @@ -401,7 +420,18 @@ class HyperionConfigFlow(ConfigFlow, domain=DOMAIN): if not hyperion_id: return self.async_abort(reason="no_id") - await self.async_set_unique_id(hyperion_id, raise_on_progress=False) + entry = await self.async_set_unique_id(hyperion_id, raise_on_progress=False) + + # pylint: disable=no-member + if self.context.get(CONF_SOURCE) == SOURCE_REAUTH and entry is not None: + assert self.hass + self.hass.config_entries.async_update_entry(entry, data=self._data) + # Need to manually reload, as the listener won't have been installed because + # the initial load did not succeed (the reauth flow will not be initiated if + # the load succeeds) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + self._abort_if_unique_id_configured() # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 9875f3bd91..2bb9ec241e 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -1,24 +1,22 @@ """Constants for Hyperion integration.""" -DOMAIN = "hyperion" + +CONF_AUTH_ID = "auth_id" +CONF_CREATE_TOKEN = "create_token" +CONF_INSTANCE = "instance" +CONF_ON_UNLOAD = "ON_UNLOAD" +CONF_PRIORITY = "priority" +CONF_ROOT_CLIENT = "ROOT_CLIENT" DEFAULT_NAME = "Hyperion" DEFAULT_ORIGIN = "Home Assistant" DEFAULT_PRIORITY = 128 -CONF_AUTH_ID = "auth_id" -CONF_CREATE_TOKEN = "create_token" -CONF_INSTANCE = "instance" -CONF_PRIORITY = "priority" +DOMAIN = "hyperion" -CONF_ROOT_CLIENT = "ROOT_CLIENT" -CONF_ON_UNLOAD = "ON_UNLOAD" +HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" +HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" SIGNAL_INSTANCES_UPDATED = f"{DOMAIN}_instances_updated_signal." "{}" SIGNAL_INSTANCE_REMOVED = f"{DOMAIN}_instance_removed_signal." "{}" -SOURCE_IMPORT = "import" - -HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" -HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" - TYPE_HYPERION_LIGHT = "hyperion_light" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index 5aa087c051..e2989cb973 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -21,7 +21,7 @@ from homeassistant.components.light import ( SUPPORT_EFFECT, LightEntity, ) -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -47,7 +47,6 @@ from .const import ( DOMAIN, SIGNAL_INSTANCE_REMOVED, SIGNAL_INSTANCES_UPDATED, - SOURCE_IMPORT, TYPE_HYPERION_LIGHT, ) diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index d8c6a2c352..5f5e8ea622 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -6,8 +6,9 @@ "documentation": "https://www.home-assistant.io/integrations/hyperion", "domain": "hyperion", "name": "Hyperion", + "quality_scale": "platinum", "requirements": [ - "hyperion-py==0.6.0" + "hyperion-py==0.6.1" ], "ssdp": [ { diff --git a/homeassistant/components/hyperion/strings.json b/homeassistant/components/hyperion/strings.json index 180f266f1a..ca7ed238f0 100644 --- a/homeassistant/components/hyperion/strings.json +++ b/homeassistant/components/hyperion/strings.json @@ -37,7 +37,8 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "auth_new_token_not_granted_error": "Newly created token was not approved on Hyperion UI", "auth_new_token_not_work_error": "Failed to authenticate using newly created token", - "no_id": "The Hyperion Ambilight instance did not report its id" + "no_id": "The Hyperion Ambilight instance did not report its id", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/hyperion/translations/ca.json b/homeassistant/components/hyperion/translations/ca.json new file mode 100644 index 0000000000..50ac384d8c --- /dev/null +++ b/homeassistant/components/hyperion/translations/ca.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "El servei ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "auth_new_token_not_granted_error": "El nou token creat no est\u00e0 aprovat a Hyperion UI", + "auth_new_token_not_work_error": "No s'ha pogut autenticar amb el nou token creat", + "auth_required_error": "No s'ha pogut determinar si cal autoritzaci\u00f3", + "cannot_connect": "Ha fallat la connexi\u00f3", + "no_id": "La inst\u00e0ncia d'Hyperion Ambilight no ha retornat el seu ID", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_access_token": "Token d'acc\u00e9s no v\u00e0lid" + }, + "step": { + "auth": { + "data": { + "create_token": "Crea un nou token autom\u00e0ticament", + "token": "O proporciona un token ja existent" + }, + "description": "Configura l'autoritzaci\u00f3 amb el teu servidor Hyperion Ambilight" + }, + "confirm": { + "description": "Vols afegir el seg\u00fcent Hyperion Ambilight a Home Assistant?\n\n**Host:** {host}\n**Port:** {port}\n**ID**: {id}", + "title": "Confirma l'addici\u00f3 del servei Hyperion Ambilight" + }, + "create_token": { + "description": "Selecciona **Envia** a continuaci\u00f3 per sol\u00b7licitar un token d'autenticaci\u00f3 nou. Ser\u00e0s redirigit a la interf\u00edcie d'usuari d'Hyperion perqu\u00e8 puguis aprovar la sol\u00b7licitud. Verifica que l'identificador que es mostra \u00e9s \"{auth_id}\"", + "title": "Crea un nou token d'autenticaci\u00f3 autom\u00e0ticament" + }, + "create_token_external": { + "title": "Accepta el nou token a la IU d'Hyperion" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Prioritat Hyperion a utilitzar per als colors i efectes" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/cs.json b/homeassistant/components/hyperion/translations/cs.json index c5358988ba..52e3f0beb5 100644 --- a/homeassistant/components/hyperion/translations/cs.json +++ b/homeassistant/components/hyperion/translations/cs.json @@ -3,13 +3,22 @@ "abort": { "already_configured": "Slu\u017eba je ji\u017e nastavena", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_access_token": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token" }, "step": { + "auth": { + "data": { + "create_token": "Automaticky vytvo\u0159it nov\u00fd token" + } + }, + "create_token_external": { + "title": "P\u0159ijmout nov\u00fd token v u\u017eivatelsk\u00e9m rozhran\u00ed Hyperion" + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/hyperion/translations/de.json b/homeassistant/components/hyperion/translations/de.json new file mode 100644 index 0000000000..95c0f1734c --- /dev/null +++ b/homeassistant/components/hyperion/translations/de.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "auth_new_token_not_granted_error": "Neu erstellter Token wurde auf der Hyperion-Benutzeroberfl\u00e4che nicht genehmigt", + "auth_new_token_not_work_error": "Authentifizierung mit neu erstelltem Token fehlgeschlagen", + "auth_required_error": "Es konnte nicht festgestellt werden, ob eine Autorisierung erforderlich ist", + "cannot_connect": "Verbindung fehlgeschlagen", + "no_id": "Die Hyperion Ambilight-Instanz hat ihre ID nicht gemeldet", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token" + }, + "step": { + "auth": { + "data": { + "create_token": "Automatisch neuen Authentifizierungs-Token erstellen", + "token": "Oder stelle einen bereits vorhandenen Token bereit" + }, + "description": "Konfiguriere die Autorisierung f\u00fcr den Hyperion-Ambilight-Server" + }, + "confirm": { + "description": "Soll dieses Hyperion Ambilight zu Home Assistant hinzugef\u00fcgt werden? \n\n ** Host: ** {host}\n ** Port: ** {port}\n ** ID **: {id}", + "title": "Best\u00e4tige das Hinzuf\u00fcgen des Hyperion-Ambilight-Dienstes" + }, + "create_token": { + "description": "W\u00e4hle **Submit**, um einen neuen Authentifizierungs-Token anzufordern. Du wirst zur Hyperion-Benutzeroberfl\u00e4che weitergeleitet, um die Anforderung zu best\u00e4tigen. Bitte \u00fcberpr\u00fcfe, ob die angezeigte ID \"{auth_id}\" lautet.", + "title": "Automatisch neuen Authentifizierungs-Token erstellen" + }, + "create_token_external": { + "title": "Neuen Token in Hyperion UI akzeptieren" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Hyperion-Priorit\u00e4t f\u00fcr Farben und Effekte" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/en.json b/homeassistant/components/hyperion/translations/en.json index c4c4f512d6..d1277b411e 100644 --- a/homeassistant/components/hyperion/translations/en.json +++ b/homeassistant/components/hyperion/translations/en.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Failed to authenticate using newly created token", "auth_required_error": "Failed to determine if authorization is required", "cannot_connect": "Failed to connect", - "no_id": "The Hyperion Ambilight instance did not report its id" + "no_id": "The Hyperion Ambilight instance did not report its id", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", diff --git a/homeassistant/components/hyperion/translations/es.json b/homeassistant/components/hyperion/translations/es.json index bb1ef3e2c0..db3aa75462 100644 --- a/homeassistant/components/hyperion/translations/es.json +++ b/homeassistant/components/hyperion/translations/es.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Error al autenticarse con el token reci\u00e9n creado", "auth_required_error": "No se pudo determinar si se requiere autorizaci\u00f3n", "cannot_connect": "No se pudo conectar", - "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su identificaci\u00f3n" + "no_id": "La instancia de Hyperion Ambilight no inform\u00f3 su identificaci\u00f3n", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "cannot_connect": "No se pudo conectar", diff --git a/homeassistant/components/hyperion/translations/et.json b/homeassistant/components/hyperion/translations/et.json index e8d6232236..a225b7f2c4 100644 --- a/homeassistant/components/hyperion/translations/et.json +++ b/homeassistant/components/hyperion/translations/et.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Loodud juurdep\u00e4\u00e4sut\u00f5endiga autentimine nurjus", "auth_required_error": "Autoriseerimise vajalikkuse tuvastamine nurjus", "cannot_connect": "\u00dchendamine nurjus", - "no_id": "Hyperion Ambilighti eksemplar ei teatanud oma ID-d" + "no_id": "Hyperion Ambilighti eksemplar ei teatanud oma ID-d", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "cannot_connect": "\u00dchendamine nurjus", diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json new file mode 100644 index 0000000000..8c1cb919d1 --- /dev/null +++ b/homeassistant/components/hyperion/translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/hu.json b/homeassistant/components/hyperion/translations/hu.json new file mode 100644 index 0000000000..50ccd9f3b6 --- /dev/null +++ b/homeassistant/components/hyperion/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" + }, + "step": { + "auth": { + "data": { + "create_token": "\u00daj token automatikus l\u00e9trehoz\u00e1sa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json new file mode 100644 index 0000000000..ff3170ffb9 --- /dev/null +++ b/homeassistant/components/hyperion/translations/it.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Il servizio \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "auth_new_token_not_granted_error": "Il token appena creato non \u00e8 stato approvato sull'interfaccia utente di Hyperion", + "auth_new_token_not_work_error": "Autenticazione utilizzando il token appena creato non riuscita", + "auth_required_error": "Impossibile determinare se \u00e8 necessaria l'autorizzazione", + "cannot_connect": "Impossibile connettersi", + "no_id": "L'istanza Hyperion Ambilight non ha segnalato il suo ID", + "reauth_successful": "Ri-autenticazione completata con successo" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_access_token": "Token di accesso non valido" + }, + "step": { + "auth": { + "data": { + "create_token": "Crea automaticamente un nuovo token", + "token": "Oppure fornisci un token preesistente" + }, + "description": "Configura l'autorizzazione per il tuo server Hyperion Ambilight" + }, + "confirm": { + "description": "Vuoi aggiungere questo Hyperion Ambilight a Home Assistant? \n\n ** Host:** {host}\n ** Porta:** {port}\n ** ID:** {id}", + "title": "Conferma l'aggiunta del servizio Hyperion Ambilight" + }, + "create_token": { + "description": "Scegli **Invia** di seguito per richiedere un nuovo token di autenticazione. Verrai reindirizzato all'interfaccia utente di Hyperion per approvare la richiesta. Verifica che l'ID visualizzato sia \"{auth_id}\"", + "title": "Crea automaticamente un nuovo token di autenticazione" + }, + "create_token_external": { + "title": "Accetta il nuovo token nell'interfaccia utente di Hyperion" + }, + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Priorit\u00e0 Hyperion da usare per colori ed effetti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/nl.json b/homeassistant/components/hyperion/translations/nl.json new file mode 100644 index 0000000000..d93018f8a3 --- /dev/null +++ b/homeassistant/components/hyperion/translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth": { + "data": { + "create_token": "Maak automatisch een nieuw token aan" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/no.json b/homeassistant/components/hyperion/translations/no.json index 79c90379f1..e411982b58 100644 --- a/homeassistant/components/hyperion/translations/no.json +++ b/homeassistant/components/hyperion/translations/no.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Kunne ikke godkjenne ved hjelp av nylig opprettet token", "auth_required_error": "Kan ikke fastsl\u00e5 om autorisasjon er n\u00f8dvendig", "cannot_connect": "Tilkobling mislyktes", - "no_id": "Hyperion Ambilight-forekomsten rapporterte ikke ID-en" + "no_id": "Hyperion Ambilight-forekomsten rapporterte ikke ID-en", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "cannot_connect": "Tilkobling mislyktes", diff --git a/homeassistant/components/hyperion/translations/pl.json b/homeassistant/components/hyperion/translations/pl.json index e705d115c8..33b7c92752 100644 --- a/homeassistant/components/hyperion/translations/pl.json +++ b/homeassistant/components/hyperion/translations/pl.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "Nie uda\u0142o si\u0119 uwierzytelni\u0107 przy u\u017cyciu nowo utworzonego tokena", "auth_required_error": "Nie uda\u0142o si\u0119 okre\u015bli\u0107, czy wymagana jest autoryzacja", "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", - "no_id": "Instancja Hyperion Ambilight nie zg\u0142osi\u0142a swojego identyfikatora" + "no_id": "Instancja Hyperion Ambilight nie zg\u0142osi\u0142a swojego identyfikatora", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", diff --git a/homeassistant/components/hyperion/translations/pt.json b/homeassistant/components/hyperion/translations/pt.json new file mode 100644 index 0000000000..ac9710c6b9 --- /dev/null +++ b/homeassistant/components/hyperion/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_access_token": "Token de acesso inv\u00e1lido" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/ru.json b/homeassistant/components/hyperion/translations/ru.json index fda9ef4bb5..9e74680a95 100644 --- a/homeassistant/components/hyperion/translations/ru.json +++ b/homeassistant/components/hyperion/translations/ru.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0433\u043e \u0442\u043e\u043a\u0435\u043d\u0430.", "auth_required_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c, \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043b\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", - "no_id": "Hyperion Ambilight \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0438\u043b \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440." + "no_id": "Hyperion Ambilight \u043d\u0435 \u0441\u043e\u043e\u0431\u0449\u0438\u043b \u0441\u0432\u043e\u0439 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440.", + "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.", diff --git a/homeassistant/components/hyperion/translations/sl.json b/homeassistant/components/hyperion/translations/sl.json new file mode 100644 index 0000000000..6829f43011 --- /dev/null +++ b/homeassistant/components/hyperion/translations/sl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Storitev je \u017ee name\u0161\u010dena", + "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", + "auth_new_token_not_granted_error": "Uporabni\u0161ki vmesnik Hyperion UI ni potrdil novo ustvarjenega \u017eetona", + "auth_new_token_not_work_error": "Overjanje s pomo\u010djo novo ustvarjenega \u017eetona ni uspelo", + "auth_required_error": "Ni mogo\u010de dolo\u010diti ali je overjanje potrebno", + "cannot_connect": "Povezovanje ni bilo uspe\u0161no", + "no_id": "Hyperion Ambilight instanca ni prijavila tega id", + "reauth_successful": "Ponovno overjanje je uspelo." + }, + "error": { + "cannot_connect": "Neuspelo povezovanje", + "invalid_access_token": "Neveljaven \u017eeton za dostop" + }, + "step": { + "auth": { + "data": { + "create_token": "Samodejno ustvari nov \u017eeton", + "token": "ali zagotovite \u017ee obstoje\u010di \u017eeton" + }, + "description": "Nastavite overitev za Hyperion Ambilight stre\u017enik" + }, + "confirm": { + "description": "\u017delite dodati ta Hyperion Ambilight v Home Assistant?\n\n**Gostitelj:** {host}\n**Vrata:** {port}\n**ID**: {id}", + "title": "Potrdi dodajanje storitve Hyperion Ambilight" + }, + "create_token": { + "description": "Izberite **Posreduj*, \u010de \u017eelite zahtevati nov overitveni \u017eeton. Preusmerjeni boste na uporabni\u0161ki vmesnik Hyperion, da potrdite zahtevek. Prepri\u010dajte se, da je prikazani id \"{auth_id}\"", + "title": "Samodejno ustvari nov overitveni \u017eeton" + }, + "create_token_external": { + "title": "Sprejmi nov \u017eeton v uporabni\u0161kem vmesniku Hyperion" + }, + "user": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Prednostna raba Hyperiona za barve in u\u010dinke" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json new file mode 100644 index 0000000000..6f46000e3e --- /dev/null +++ b/homeassistant/components/hyperion/translations/tr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "auth_new_token_not_granted_error": "Hyperion UI'de yeni olu\u015fturulan belirte\u00e7 onaylanmad\u0131", + "auth_new_token_not_work_error": "Yeni olu\u015fturulan belirte\u00e7 kullan\u0131larak kimlik do\u011frulamas\u0131 ba\u015far\u0131s\u0131z oldu", + "auth_required_error": "Yetkilendirmenin gerekli olup olmad\u0131\u011f\u0131 belirlenemedi", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_id": "Hyperion Ambilight \u00f6rne\u011fi kimli\u011fini bildirmedi" + }, + "step": { + "auth": { + "data": { + "create_token": "Otomatik olarak yeni belirte\u00e7 olu\u015fturma", + "token": "Veya \u00f6nceden varolan belirte\u00e7 leri sa\u011flay\u0131n" + } + }, + "create_token": { + "title": "Otomatik olarak yeni kimlik do\u011frulama belirteci olu\u015fturun" + }, + "create_token_external": { + "title": "Hyperion kullan\u0131c\u0131 aray\u00fcz\u00fcnde yeni belirteci kabul edin" + }, + "user": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "Renkler ve efektler i\u00e7in kullan\u0131lacak hyperion \u00f6nceli\u011fi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/translations/zh-Hant.json b/homeassistant/components/hyperion/translations/zh-Hant.json index fb9cbe3b7a..ed003131bf 100644 --- a/homeassistant/components/hyperion/translations/zh-Hant.json +++ b/homeassistant/components/hyperion/translations/zh-Hant.json @@ -7,7 +7,8 @@ "auth_new_token_not_work_error": "\u4f7f\u7528\u65b0\u5275\u5bc6\u9470\u8a8d\u8b49\u5931\u6557", "auth_required_error": "\u7121\u6cd5\u5224\u5b9a\u662f\u5426\u9700\u8981\u9a57\u8b49", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "no_id": "Hyperion Ambilight \u5be6\u9ad4\u672a\u56de\u5831\u5176 ID" + "no_id": "Hyperion Ambilight \u5be6\u9ad4\u672a\u56de\u5831\u5176 ID", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/iaqualink/translations/pt.json b/homeassistant/components/iaqualink/translations/pt.json index 24825307e7..3b46686633 100644 --- a/homeassistant/components/iaqualink/translations/pt.json +++ b/homeassistant/components/iaqualink/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/iaqualink/translations/zh-Hant.json b/homeassistant/components/iaqualink/translations/zh-Hant.json index 3923f95f71..aaf1800f74 100644 --- a/homeassistant/components/iaqualink/translations/zh-Hant.json +++ b/homeassistant/components/iaqualink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/icloud/translations/no.json b/homeassistant/components/icloud/translations/no.json index 2f9571b68f..62e123eb84 100644 --- a/homeassistant/components/icloud/translations/no.json +++ b/homeassistant/components/icloud/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "no_device": "Ingen av enhetene dine har \"Finn min iPhone\" aktivert", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning", @@ -16,7 +16,7 @@ "password": "Passord" }, "description": "Ditt tidligere angitte passord for {username} fungerer ikke lenger. Oppdater passordet ditt for \u00e5 fortsette \u00e5 bruke denne integrasjonen.", - "title": "Bekreft integrering p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" }, "trusted_device": { "data": { diff --git a/homeassistant/components/icloud/translations/pt.json b/homeassistant/components/icloud/translations/pt.json index 420196bb05..3e8e4cce2b 100644 --- a/homeassistant/components/icloud/translations/pt.json +++ b/homeassistant/components/icloud/translations/pt.json @@ -1,6 +1,20 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { + "reauth": { + "data": { + "password": "Palavra-passe" + }, + "description": "A sua palavra-passe anteriormente introduzida para {username} j\u00e1 n\u00e3o \u00e9 v\u00e1lida. Atualize sua palavra-passe para continuar a utilizar esta integra\u00e7\u00e3o.", + "title": "Reautenticar integra\u00e7\u00e3o" + }, "trusted_device": { "data": { "trusted_device": "Dispositivo confi\u00e1vel" @@ -9,7 +23,8 @@ "user": { "data": { "password": "Palavra-passe", - "username": "Email" + "username": "Email", + "with_family": "Com a fam\u00edlia" } } } diff --git a/homeassistant/components/icloud/translations/zh-Hant.json b/homeassistant/components/icloud/translations/zh-Hant.json index 3a6f1b64fa..1c16db77fa 100644 --- a/homeassistant/components/icloud/translations/zh-Hant.json +++ b/homeassistant/components/icloud/translations/zh-Hant.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_device": "\u8a2d\u5099\u7686\u672a\u958b\u555f\u300c\u5c0b\u627e\u6211\u7684 iPhone\u300d\u529f\u80fd\u3002", + "no_device": "\u88dd\u7f6e\u7686\u672a\u958b\u555f\u300c\u5c0b\u627e\u6211\u7684 iPhone\u300d\u529f\u80fd\u3002", "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "send_verification_code": "\u50b3\u9001\u9a57\u8b49\u78bc\u5931\u6557", - "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u8a2d\u5099\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" + "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u88dd\u7f6e\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" }, "step": { "reauth": { @@ -20,10 +20,10 @@ }, "trusted_device": { "data": { - "trusted_device": "\u4fe1\u4efb\u8a2d\u5099" + "trusted_device": "\u4fe1\u4efb\u88dd\u7f6e" }, - "description": "\u9078\u64c7\u4fe1\u4efb\u8a2d\u5099", - "title": "iCloud \u4fe1\u4efb\u8a2d\u5099" + "description": "\u9078\u64c7\u4fe1\u4efb\u88dd\u7f6e", + "title": "iCloud \u4fe1\u4efb\u88dd\u7f6e" }, "user": { "data": { diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 783cd16fef..519c2e4276 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -52,10 +52,13 @@ DEFAULT_EVENT_HOME = "alarm_arm_home" DEFAULT_EVENT_NIGHT = "alarm_arm_night" DEFAULT_EVENT_DISARM = "alarm_disarm" +CONF_CODE_ARM_REQUIRED = "code_arm_required" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_CODE): cv.string, + vol.Optional(CONF_CODE_ARM_REQUIRED, default=True): cv.boolean, vol.Optional(CONF_EVENT_AWAY, default=DEFAULT_EVENT_AWAY): cv.string, vol.Optional(CONF_EVENT_HOME, default=DEFAULT_EVENT_HOME): cv.string, vol.Optional(CONF_EVENT_NIGHT, default=DEFAULT_EVENT_NIGHT): cv.string, @@ -76,6 +79,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = config.get(CONF_NAME) code = config.get(CONF_CODE) + code_arm_required = config.get(CONF_CODE_ARM_REQUIRED) event_away = config.get(CONF_EVENT_AWAY) event_home = config.get(CONF_EVENT_HOME) event_night = config.get(CONF_EVENT_NIGHT) @@ -83,7 +87,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): optimistic = config.get(CONF_OPTIMISTIC) alarmpanel = IFTTTAlarmPanel( - name, code, event_away, event_home, event_night, event_disarm, optimistic + name, + code, + code_arm_required, + event_away, + event_home, + event_night, + event_disarm, + optimistic, ) hass.data[DATA_IFTTT_ALARM].append(alarmpanel) add_entities([alarmpanel]) @@ -112,11 +123,20 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity): """Representation of an alarm control panel controlled through IFTTT.""" def __init__( - self, name, code, event_away, event_home, event_night, event_disarm, optimistic + self, + name, + code, + code_arm_required, + event_away, + event_home, + event_night, + event_disarm, + optimistic, ): """Initialize the alarm control panel.""" self._name = name self._code = code + self._code_arm_required = code_arm_required self._event_away = event_away self._event_home = event_home self._event_night = event_night @@ -161,19 +181,19 @@ class IFTTTAlarmPanel(AlarmControlPanelEntity): def alarm_arm_away(self, code=None): """Send arm away command.""" - if not self._check_code(code): + if self._code_arm_required and not self._check_code(code): return self.set_alarm_state(self._event_away, STATE_ALARM_ARMED_AWAY) def alarm_arm_home(self, code=None): """Send arm home command.""" - if not self._check_code(code): + if self._code_arm_required and not self._check_code(code): return self.set_alarm_state(self._event_home, STATE_ALARM_ARMED_HOME) def alarm_arm_night(self, code=None): """Send arm night command.""" - if not self._check_code(code): + if self._code_arm_required and not self._check_code(code): return self.set_alarm_state(self._event_night, STATE_ALARM_ARMED_NIGHT) diff --git a/homeassistant/components/ifttt/translations/et.json b/homeassistant/components/ifttt/translations/et.json index cecd2aea7e..e4d2c8f148 100644 --- a/homeassistant/components/ifttt/translations/et.json +++ b/homeassistant/components/ifttt/translations/et.json @@ -5,7 +5,7 @@ "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, "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." + "default": "S\u00fcndmuste saatmiseks Home Assistantile peate kasutama toimingut \"Make a web request\" [IFTTT Webhooki apletilt] ({applet_url}).\n\nSisesta 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." }, "step": { "user": { diff --git a/homeassistant/components/ifttt/translations/pt.json b/homeassistant/components/ifttt/translations/pt.json index eaed455b71..030af8e090 100644 --- a/homeassistant/components/ifttt/translations/pt.json +++ b/homeassistant/components/ifttt/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, precisa de utilizar a a\u00e7\u00e3o \"Make a web request\" no [IFTTT Webhook applet]({applet_url}).\n\nPreencha com a seguinte informa\u00e7\u00e3o:\n\n- URL: `{webhook_url}`\n- Method: POST \n- Content Type: application/json \n\nConsulte [a documenta\u00e7\u00e3o]({docs_url}) sobre como configurar automa\u00e7\u00f5es para lidar com dados de entrada." }, diff --git a/homeassistant/components/ifttt/translations/zh-Hant.json b/homeassistant/components/ifttt/translations/zh-Hant.json index beef1c7070..fe5b80f72f 100644 --- a/homeassistant/components/ifttt/translations/zh-Hant.json +++ b/homeassistant/components/ifttt/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/insteon/translations/nl.json b/homeassistant/components/insteon/translations/nl.json index 923fdbfb44..d2f73fca37 100644 --- a/homeassistant/components/insteon/translations/nl.json +++ b/homeassistant/components/insteon/translations/nl.json @@ -5,14 +5,17 @@ "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." }, "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kon niet verbinden", + "select_single": "Selecteer een optie." }, "step": { "hubv1": { "data": { "host": "IP-adres", "port": "Poort" - } + }, + "description": "Configureer de Insteon Hub versie 1 (pre-2014).", + "title": "Insteon Hub versie 1" }, "hubv2": { "data": { @@ -20,7 +23,13 @@ "password": "Wachtwoord", "port": "Poort", "username": "Gebruikersnaam" - } + }, + "description": "Configureer de Insteon Hub versie 2.", + "title": "Insteon Hub versie 2" + }, + "plm": { + "description": "Configureer de Insteon PowerLink Modem (PLM).", + "title": "Insteon PLM" }, "user": { "data": { @@ -33,11 +42,29 @@ }, "options": { "error": { - "cannot_connect": "Kon niet verbinden" + "cannot_connect": "Kon niet verbinden", + "input_error": "Ongeldige invoer, controleer uw waarden.", + "select_single": "Selecteer \u00e9\u00e9n optie." }, "step": { + "add_override": { + "data": { + "address": "Apparaatadres (bijv. 1a2b3c)", + "cat": "Apparaatcategorie (bijv. 0x10)", + "subcat": "Apparaatsubcategorie (bijv. 0x0a)" + }, + "description": "Voeg een apparaat overschrijven toe.", + "title": "Insteon" + }, "add_x10": { - "description": "Wijzig het wachtwoord van de Insteon Hub." + "data": { + "housecode": "Huiscode (a - p)", + "platform": "Platform", + "steps": "Dimmerstappen (alleen voor verlichtingsapparaten, standaard 22)", + "unitcode": "Unitcode (1 - 16)" + }, + "description": "Wijzig het wachtwoord van de Insteon Hub.", + "title": "Insteon" }, "change_hub_config": { "data": { @@ -46,7 +73,17 @@ "port": "Poort", "username": "Gebruikersnaam" }, - "description": "Wijzig de verbindingsgegevens van de Insteon Hub. Je moet Home Assistant opnieuw opstarten nadat je deze wijziging hebt aangebracht. Dit verandert niets aan de configuratie van de Hub zelf. Gebruik de Hub-app om de configuratie in de Hub te wijzigen." + "description": "Wijzig de verbindingsgegevens van de Insteon Hub. Je moet Home Assistant opnieuw opstarten nadat je deze wijziging hebt aangebracht. Dit verandert niets aan de configuratie van de Hub zelf. Gebruik de Hub-app om de configuratie in de Hub te wijzigen.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Voeg een apparaat overschrijven toe.", + "add_x10": "Voeg een X10-apparaat toe.", + "change_hub_config": "Wijzig de Hub-configuratie.", + "remove_override": "Verwijder een apparaatoverschrijving.", + "remove_x10": "Verwijder een X10-apparaat." + } } } } diff --git a/homeassistant/components/insteon/translations/zh-Hant.json b/homeassistant/components/insteon/translations/zh-Hant.json index 5358cccb85..dd69e0ec7c 100644 --- a/homeassistant/components/insteon/translations/zh-Hant.json +++ b/homeassistant/components/insteon/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -29,7 +29,7 @@ }, "plm": { "data": { - "device": "USB \u8a2d\u5099\u8def\u5f91" + "device": "USB \u88dd\u7f6e\u8def\u5f91" }, "description": "\u8a2d\u5b9a PowerLink Modem (PLM)\u3002", "title": "Insteon PLM" @@ -52,18 +52,18 @@ "step": { "add_override": { "data": { - "address": "\u8a2d\u5099\u4f4d\u5740\uff08\u4f8b\u5982 1a2b3c\uff09", - "cat": "\u8a2d\u5099\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x10\uff09", - "subcat": "\u8a2d\u5099\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x0a\uff09" + "address": "\u88dd\u7f6e\u4f4d\u5740\uff08\u4f8b\u5982 1a2b3c\uff09", + "cat": "\u88dd\u7f6e\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x10\uff09", + "subcat": "\u88dd\u7f6e\u5b50\u985e\u5225\uff08\u4f8b\u5982 0x0a\uff09" }, - "description": "\u65b0\u589e\u8a2d\u5099\u8986\u5beb\u3002", + "description": "\u65b0\u589e\u88dd\u7f6e\u8986\u5beb\u3002", "title": "Insteon" }, "add_x10": { "data": { "housecode": "Housecode (a - p)", "platform": "\u5e73\u53f0", - "steps": "\u8abf\u5149\u968e\u6bb5\uff08\u50c5\u9069\u7528\u7167\u660e\u8a2d\u5099\u3001\u9810\u8a2d\u503c\u70ba 22\uff09", + "steps": "\u8abf\u5149\u968e\u6bb5\uff08\u50c5\u9069\u7528\u7167\u660e\u88dd\u7f6e\u3001\u9810\u8a2d\u503c\u70ba 22\uff09", "unitcode": "Unitcode (1 - 16)" }, "description": "\u8b8a\u66f4 Insteon Hub \u5bc6\u78bc\u3002", @@ -76,32 +76,32 @@ "port": "\u901a\u8a0a\u57e0", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u8a2d\u5099\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002", + "description": "\u8b8a\u66f4 Insteon Hub \u9023\u7dda\u8cc7\u8a0a\u3002\u65bc\u8b8a\u66f4\u4e4b\u5f8c\u3001\u5fc5\u9808\u91cd\u555f Home Assistant\u3002\u6b64\u4e9b\u8a2d\u5b9a\u4e0d\u6703\u8b8a\u66f4 Hub \u88dd\u7f6e\u672c\u8eab\u7684\u8a2d\u5b9a\uff0c\u5982\u6b32\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3001\u5247\u8acb\u4f7f\u7528 Hub app\u3002", "title": "Insteon" }, "init": { "data": { - "add_override": "\u65b0\u589e\u8a2d\u5099\u8986\u5beb\u3002", - "add_x10": "\u65b0\u589e X10 \u8a2d\u5099\u3002", + "add_override": "\u65b0\u589e\u88dd\u7f6e\u8986\u5beb\u3002", + "add_x10": "\u65b0\u589e X10 \u88dd\u7f6e\u3002", "change_hub_config": "\u8b8a\u66f4 Hub \u8a2d\u5b9a\u3002", - "remove_override": "\u79fb\u9664\u8a2d\u5099\u8986\u5beb", - "remove_x10": "\u79fb\u9664 X10 \u8a2d\u5099\u3002" + "remove_override": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb", + "remove_x10": "\u79fb\u9664 X10 \u88dd\u7f6e\u3002" }, "description": "\u9078\u64c7\u9078\u9805\u4ee5\u8a2d\u5b9a", "title": "Insteon" }, "remove_override": { "data": { - "address": "\u9078\u64c7\u8a2d\u5099\u4f4d\u5740\u4ee5\u79fb\u9664" + "address": "\u9078\u64c7\u88dd\u7f6e\u4f4d\u5740\u4ee5\u79fb\u9664" }, - "description": "\u79fb\u9664\u8a2d\u5099\u8986\u5beb", + "description": "\u79fb\u9664\u88dd\u7f6e\u8986\u5beb", "title": "Insteon" }, "remove_x10": { "data": { - "address": "\u9078\u64c7\u8a2d\u5099\u4f4d\u5740\u4ee5\u79fb\u9664" + "address": "\u9078\u64c7\u88dd\u7f6e\u4f4d\u5740\u4ee5\u79fb\u9664" }, - "description": "\u79fb\u9664 X10 \u8a2d\u5099", + "description": "\u79fb\u9664 X10 \u88dd\u7f6e", "title": "Insteon" } } diff --git a/homeassistant/components/ios/translations/zh-Hant.json b/homeassistant/components/ios/translations/zh-Hant.json index ea5e5afce2..aceb4ea78d 100644 --- a/homeassistant/components/ios/translations/zh-Hant.json +++ b/homeassistant/components/ios/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/ipma/translations/ca.json b/homeassistant/components/ipma/translations/ca.json index 2318a5eba0..806b5aebc6 100644 --- a/homeassistant/components/ipma/translations/ca.json +++ b/homeassistant/components/ipma/translations/ca.json @@ -15,5 +15,10 @@ "title": "Ubicaci\u00f3" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint de l'API d'IPMA accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/de.json b/homeassistant/components/ipma/translations/de.json index 62e2e1e59c..9fa766c190 100644 --- a/homeassistant/components/ipma/translations/de.json +++ b/homeassistant/components/ipma/translations/de.json @@ -15,5 +15,10 @@ "title": "Standort" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA-API-Endpunkt erreichbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/it.json b/homeassistant/components/ipma/translations/it.json index 467cc64f56..4dd8ddda76 100644 --- a/homeassistant/components/ipma/translations/it.json +++ b/homeassistant/components/ipma/translations/it.json @@ -15,5 +15,10 @@ "title": "Posizione" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint API IPMA raggiungibile" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/pt.json b/homeassistant/components/ipma/translations/pt.json index 3f25486c6a..a9ebd3c23e 100644 --- a/homeassistant/components/ipma/translations/pt.json +++ b/homeassistant/components/ipma/translations/pt.json @@ -15,5 +15,10 @@ "title": "Localiza\u00e7\u00e3o" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Servidor API do IPMA dispon\u00edvel" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/sl.json b/homeassistant/components/ipma/translations/sl.json index d42b858157..8c0c5441a9 100644 --- a/homeassistant/components/ipma/translations/sl.json +++ b/homeassistant/components/ipma/translations/sl.json @@ -15,5 +15,10 @@ "title": "Lokacija" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API kon\u010dna to\u010dka je dosegljiva" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/tr.json b/homeassistant/components/ipma/translations/tr.json new file mode 100644 index 0000000000..488ad37994 --- /dev/null +++ b/homeassistant/components/ipma/translations/tr.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "api_endpoint_reachable": "Ula\u015f\u0131labilir IPMA API u\u00e7 noktas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/zh-Hans.json b/homeassistant/components/ipma/translations/zh-Hans.json index 7e0da1fb84..cd5d576d0a 100644 --- a/homeassistant/components/ipma/translations/zh-Hans.json +++ b/homeassistant/components/ipma/translations/zh-Hans.json @@ -15,5 +15,10 @@ "title": "\u4f4d\u7f6e" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "\u53ef\u8bbf\u95ee IPMA API" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py index 9f522b086f..7a18da03dd 100644 --- a/homeassistant/components/ipp/__init__.py +++ b/homeassistant/components/ipp/__init__.py @@ -48,22 +48,24 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up IPP from a config entry.""" - # Create IPP instance for this entry - coordinator = IPPDataUpdateCoordinator( - hass, - host=entry.data[CONF_HOST], - port=entry.data[CONF_PORT], - base_path=entry.data[CONF_BASE_PATH], - tls=entry.data[CONF_SSL], - verify_ssl=entry.data[CONF_VERIFY_SSL], - ) + coordinator = hass.data[DOMAIN].get(entry.entry_id) + if not coordinator: + # Create IPP instance for this entry + coordinator = IPPDataUpdateCoordinator( + hass, + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + base_path=entry.data[CONF_BASE_PATH], + tls=entry.data[CONF_SSL], + verify_ssl=entry.data[CONF_VERIFY_SSL], + ) + hass.data[DOMAIN][entry.entry_id] = coordinator + await coordinator.async_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = coordinator - for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/ipp/translations/pt.json b/homeassistant/components/ipp/translations/pt.json index 02353e5fca..1f312c187c 100644 --- a/homeassistant/components/ipp/translations/pt.json +++ b/homeassistant/components/ipp/translations/pt.json @@ -1,15 +1,25 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "ipp_error": "Erro IPP encontrado.", "ipp_version_error": "Vers\u00e3o IPP n\u00e3o suportada pela impressora." }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { "host": "Servidor", - "port": "Porta" + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "verify_ssl": "Verificar o certificado SSL" } + }, + "zeroconf_confirm": { + "title": "Impressora encontrada" } } } diff --git a/homeassistant/components/ipp/translations/zh-Hant.json b/homeassistant/components/ipp/translations/zh-Hant.json index 9fcb91c462..f5d4446def 100644 --- a/homeassistant/components/ipp/translations/zh-Hant.json +++ b/homeassistant/components/ipp/translations/zh-Hant.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "connection_upgrade": "\u7531\u65bc\u9700\u8981\u5148\u5347\u7d1a\u9023\u7dda\u3001\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002", "ipp_error": "\u767c\u751f IPP \u932f\u8aa4\u3002", "ipp_version_error": "\u4e0d\u652f\u63f4\u5370\u8868\u6a5f\u7684 IPP \u7248\u672c\u3002", "parse_error": "\u7372\u5f97\u5370\u8868\u6a5f\u56de\u61c9\u5931\u6557\u3002", - "unique_id_required": "\u8a2d\u5099\u7f3a\u5c11\u641c\u5c0b\u6240\u9700\u7368\u4e00\u8b58\u5225\u3002" + "unique_id_required": "\u88dd\u7f6e\u7f3a\u5c11\u641c\u5c0b\u6240\u9700\u7368\u4e00\u8b58\u5225\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/iqvia/translations/pt.json b/homeassistant/components/iqvia/translations/pt.json new file mode 100644 index 0000000000..d252c078a2 --- /dev/null +++ b/homeassistant/components/iqvia/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/pt.json b/homeassistant/components/islamic_prayer_times/translations/pt.json new file mode 100644 index 0000000000..25538aa003 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json b/homeassistant/components/islamic_prayer_times/translations/zh-Hant.json index b94faab25c..ea7a2c4f9b 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": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index d14dfa6c65..99d11e5d6c 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -5,6 +5,8 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_host": "Der Hosteintrag hatte nicht das vollst\u00e4ndige URL-Format, z. B. http://192.168.10.100:80", "unknown": "Unerwarteter Fehler" }, "step": { @@ -12,8 +14,11 @@ "data": { "host": "URL", "password": "Passwort", + "tls": "Die TLS-Version des ISY-Controllers.", "username": "Benutzername" - } + }, + "description": "Der Hosteintrag muss im vollst\u00e4ndigen URL-Format vorliegen, z. B. http://192.168.10.100:80", + "title": "Stellen Sie eine Verbindung zu Ihrem ISY994 her" } } }, @@ -21,8 +26,10 @@ "step": { "init": { "data": { - "ignore_string": "Zeichenfolge ignorieren" - } + "ignore_string": "Zeichenfolge ignorieren", + "restore_light_state": "Lichthelligkeit wiederherstellen" + }, + "title": "ISY994 Optionen" } } } diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json index b8a454fbab..3696210051 100644 --- a/homeassistant/components/isy994/translations/pt.json +++ b/homeassistant/components/isy994/translations/pt.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { - "password": "Palavra-passe" + "host": "", + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/isy994/translations/zh-Hant.json b/homeassistant/components/isy994/translations/zh-Hant.json index 43b43661b6..9ab55c19a7 100644 --- a/homeassistant/components/isy994/translations/zh-Hant.json +++ b/homeassistant/components/isy994/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -32,7 +32,7 @@ "sensor_string": "\u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32", "variable_sensor_string": "\u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32" }, - "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u8a2d\u5099\u90fd\u6703\u88ab\u8996\u70ba\u50b3\u611f\u5668\u6216\u4e8c\u9032\u4f4d\u50b3\u611f\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u8a2d\u5099\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u50b3\u611f\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u8a2d\u5099\u9810\u8a2d\u4eae\u5ea6\u3002", + "description": "ISY \u6574\u5408\u8a2d\u5b9a\u9078\u9805\uff1a \n \u2022 \u7bc0\u9ede\u50b3\u611f\u5668\u5b57\u4e32\uff08Node Sensor String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u6216\u8cc7\u6599\u593e\u5305\u542b\u300cNode Sensor String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u8996\u70ba\u50b3\u611f\u5668\u6216\u4e8c\u9032\u4f4d\u50b3\u611f\u5668\u3002\n \u2022 \u5ffd\u7565\u5b57\u4e32\uff08Ignore String\uff09\uff1a\u4efb\u4f55\u540d\u7a31\u5305\u542b\u300cIgnore String\u300d\u7684\u88dd\u7f6e\u90fd\u6703\u88ab\u5ffd\u7565\u3002\n \u2022 \u53ef\u8b8a\u50b3\u611f\u5668\u5b57\u4e32\uff08Variable Sensor String\uff09\uff1a\u4efb\u4f55\u5305\u542b\u300cVariable Sensor String\u300d\u7684\u8b8a\u6578\u90fd\u5c07\u65b0\u589e\u70ba\u50b3\u611f\u5668\u3002 \n \u2022 \u56de\u5fa9\u4eae\u5ea6\uff08Restore Light Brightness\uff09\uff1a\u958b\u5553\u5f8c\u3001\u7576\u71c8\u5149\u958b\u555f\u6642\u6703\u56de\u5fa9\u5148\u524d\u7684\u4eae\u5ea6\uff0c\u800c\u4e0d\u662f\u4f7f\u7528\u88dd\u7f6e\u9810\u8a2d\u4eae\u5ea6\u3002", "title": "ISY994 \u9078\u9805" } } diff --git a/homeassistant/components/izone/translations/pt.json b/homeassistant/components/izone/translations/pt.json new file mode 100644 index 0000000000..7a4274b008 --- /dev/null +++ b/homeassistant/components/izone/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/zh-Hant.json b/homeassistant/components/izone/translations/zh-Hant.json index f49de8669d..363e62a1b5 100644 --- a/homeassistant/components/izone/translations/zh-Hant.json +++ b/homeassistant/components/izone/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 70d55a74b9..d1474c3cf5 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -26,8 +26,8 @@ SENSOR_TYPES = { "talit": ["Talit and Tefillin", "mdi:calendar-clock"], "gra_end_shma": ['Latest time for Shma Gr"a', "mdi:calendar-clock"], "mga_end_shma": ['Latest time for Shma MG"A', "mdi:calendar-clock"], - "gra_end_tfila": ['Latest time for Tefilla MG"A', "mdi:calendar-clock"], - "mga_end_tfila": ['Latest time for Tefilla Gr"a', "mdi:calendar-clock"], + "gra_end_tfila": ['Latest time for Tefilla Gr"a', "mdi:calendar-clock"], + "mga_end_tfila": ['Latest time for Tefilla MG"A', "mdi:calendar-clock"], "big_mincha": ["Mincha Gedola", "mdi:calendar-clock"], "small_mincha": ["Mincha Ketana", "mdi:calendar-clock"], "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"], diff --git a/homeassistant/components/juicenet/translations/pt.json b/homeassistant/components/juicenet/translations/pt.json index 0c5c776056..db82206819 100644 --- a/homeassistant/components/juicenet/translations/pt.json +++ b/homeassistant/components/juicenet/translations/pt.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_token": "API Token" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 1d547e895b..3123031595 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -131,14 +131,23 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_KNX_SEND_SCHEMA = vol.Schema( - { - vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, - vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( - cv.positive_int, [cv.positive_int] - ), - vol.Optional(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), - } +SERVICE_KNX_SEND_SCHEMA = vol.Any( + vol.Schema( + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(SERVICE_KNX_ATTR_PAYLOAD): cv.match_all, + vol.Required(SERVICE_KNX_ATTR_TYPE): vol.Any(int, float, str), + } + ), + vol.Schema( + # without type given payload is treated as raw bytes + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Required(SERVICE_KNX_ATTR_PAYLOAD): vol.Any( + cv.positive_int, [cv.positive_int] + ), + } + ), ) diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index d9f0f9c0d3..50d067bf29 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -86,7 +86,8 @@ class KNXLight(KnxEntity, LightEntity): """Return the color temperature in mireds.""" if self._device.supports_color_temperature: kelvin = self._device.current_color_temperature - if kelvin is not None: + # Avoid division by zero if actuator reported 0 Kelvin (e.g., uninitialized DALI-Gateway) + if kelvin is not None and kelvin > 0: return color_util.color_temperature_kelvin_to_mired(kelvin) if self._device.supports_tunable_white: relative_ct = self._device.current_tunable_white diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index c17667cbed..b1d791e328 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -112,7 +112,7 @@ class BinarySensorSchema: ), vol.Optional(CONF_DEVICE_CLASS): cv.string, vol.Optional(CONF_INVERT): cv.boolean, - vol.Optional(CONF_RESET_AFTER): cv.positive_int, + vol.Optional(CONF_RESET_AFTER): cv.positive_float, } ), ) diff --git a/homeassistant/components/kodi/translations/cs.json b/homeassistant/components/kodi/translations/cs.json index ccfb08328f..157c61cf24 100644 --- a/homeassistant/components/kodi/translations/cs.json +++ b/homeassistant/components/kodi/translations/cs.json @@ -36,7 +36,8 @@ "ws_port": { "data": { "ws_port": "Port" - } + }, + "description": "Port WebSocket (n\u011bkdy se v Kodi naz\u00fdv\u00e1 port TCP). Abyste se mohli p\u0159ipojit p\u0159es WebSocket, mus\u00edte povolit \"Povolit programy ... ovl\u00e1dat Kodi\" v Syst\u00e9m / Nastaven\u00ed / S\u00ed\u0165 / Slu\u017eby. Pokud WebSocket nen\u00ed povolen, odeberte port a nechte pr\u00e1zdn\u00e9." } } }, diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index f50a90c8a8..a0bf05cb5e 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "unknown": "Unerwarteter Fehler" }, "flow_title": "Kodi: {name}", "step": { diff --git a/homeassistant/components/kodi/translations/zh-Hant.json b/homeassistant/components/kodi/translations/zh-Hant.json index a4aaf90934..11d962f9d1 100644 --- a/homeassistant/components/kodi/translations/zh-Hant.json +++ b/homeassistant/components/kodi/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "no_uuid": "Kodi \u5be6\u4f8b\u6c92\u6709\u552f\u4e00 ID\u3002\u901a\u5e38\u662f\u56e0\u70ba Kodi \u7248\u672c\u904e\u820a\uff08\u4f4e\u65bc 17.x\uff09\u3002\u53ef\u4ee5\u624b\u52d5\u8a2d\u5b9a\u6574\u5408\u6216\u66f4\u65b0\u81f3\u6700\u65b0\u7248\u672c Kodi\u3002", diff --git a/homeassistant/components/konnected/translations/pt.json b/homeassistant/components/konnected/translations/pt.json index 972aed55cc..64aaf6cbf4 100644 --- a/homeassistant/components/konnected/translations/pt.json +++ b/homeassistant/components/konnected/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { @@ -8,5 +16,48 @@ } } } + }, + "options": { + "error": { + "one": "Vazio", + "other": "Vazios" + }, + "step": { + "options_binary": { + "data": { + "name": "Nome (opcional)" + } + }, + "options_digital": { + "data": { + "name": "Nome (opcional)" + } + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7" + } + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9" + } + }, + "options_switch": { + "data": { + "name": "Nome (opcional)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/zh-Hant.json b/homeassistant/components/konnected/translations/zh-Hant.json index e4b38a6d10..604dc28b57 100644 --- a/homeassistant/components/konnected/translations/zh-Hant.json +++ b/homeassistant/components/konnected/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099", + "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u88dd\u7f6e", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { @@ -12,11 +12,11 @@ "step": { "confirm": { "description": "\u578b\u865f\uff1a{model}\nID\uff1a{id}\n\u4e3b\u6a5f\u7aef\uff1a{host}\n\u901a\u8a0a\u57e0\uff1a{port}\n\n\u53ef\u4ee5\u65bc Konncected \u8b66\u5831\u9762\u677f\u8a2d\u5b9a\u4e2d\u8a2d\u5b9a IO \u8207\u9762\u677f\u884c\u70ba\u3002", - "title": "Konnected \u8a2d\u5099\u5df2\u5099\u59a5" + "title": "Konnected \u88dd\u7f6e\u5df2\u5099\u59a5" }, "import_confirm": { "description": "\u65bc configuration.yaml \u4e2d\u767c\u73fe Konnected \u8b66\u5831 ID {id}\u3002\u6b64\u6d41\u7a0b\u5c07\u5141\u8a31\u532f\u5165\u81f3\u8a2d\u5b9a\u4e2d\u3002", - "title": "\u532f\u5165 Konnected \u8a2d\u5099" + "title": "\u532f\u5165 Konnected \u88dd\u7f6e" }, "user": { "data": { @@ -29,7 +29,7 @@ }, "options": { "abort": { - "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099" + "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u88dd\u7f6e" }, "error": { "bad_host": "\u7121\u6548\u7684\u8986\u5beb API \u4e3b\u6a5f\u7aef URL" diff --git a/homeassistant/components/kulersky/translations/ca.json b/homeassistant/components/kulersky/translations/ca.json new file mode 100644 index 0000000000..dc21c371e6 --- /dev/null +++ b/homeassistant/components/kulersky/translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No s'han trobat dispositius a la xarxa", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "step": { + "confirm": { + "description": "Vols comen\u00e7ar la configuraci\u00f3?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/cs.json b/homeassistant/components/kulersky/translations/cs.json new file mode 100644 index 0000000000..d3f0e37a13 --- /dev/null +++ b/homeassistant/components/kulersky/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "step": { + "confirm": { + "description": "Chcete za\u010d\u00edt nastavovat?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/de.json b/homeassistant/components/kulersky/translations/de.json new file mode 100644 index 0000000000..3fc69f8594 --- /dev/null +++ b/homeassistant/components/kulersky/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "Wollen Sie mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/es.json b/homeassistant/components/kulersky/translations/es.json new file mode 100644 index 0000000000..520df7ee4c --- /dev/null +++ b/homeassistant/components/kulersky/translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos en la red", + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "step": { + "confirm": { + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/et.json b/homeassistant/components/kulersky/translations/et.json new file mode 100644 index 0000000000..9e7bb472e0 --- /dev/null +++ b/homeassistant/components/kulersky/translations/et.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet", + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "step": { + "confirm": { + "description": "Kas soovid alustada seadistamist?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/fr.json b/homeassistant/components/kulersky/translations/fr.json new file mode 100644 index 0000000000..4c984a5569 --- /dev/null +++ b/homeassistant/components/kulersky/translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "Aucun appareil n'a \u00e9t\u00e9 d\u00e9tect\u00e9 sur le r\u00e9seau" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/hu.json b/homeassistant/components/kulersky/translations/hu.json new file mode 100644 index 0000000000..3d5be90042 --- /dev/null +++ b/homeassistant/components/kulersky/translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "confirm": { + "description": "El akarod kezdeni a be\u00e1ll\u00edt\u00e1st?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/it.json b/homeassistant/components/kulersky/translations/it.json new file mode 100644 index 0000000000..0278fe07bf --- /dev/null +++ b/homeassistant/components/kulersky/translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nessun dispositivo trovato sulla rete", + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "step": { + "confirm": { + "description": "Vuoi iniziare la configurazione?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/no.json b/homeassistant/components/kulersky/translations/no.json new file mode 100644 index 0000000000..b3d6b5d782 --- /dev/null +++ b/homeassistant/components/kulersky/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "step": { + "confirm": { + "description": "Vil du starte oppsettet?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/pl.json b/homeassistant/components/kulersky/translations/pl.json new file mode 100644 index 0000000000..a8ee3fa57a --- /dev/null +++ b/homeassistant/components/kulersky/translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", + "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/kulersky/translations/pt.json b/homeassistant/components/kulersky/translations/pt.json new file mode 100644 index 0000000000..e25888655a --- /dev/null +++ b/homeassistant/components/kulersky/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/ru.json b/homeassistant/components/kulersky/translations/ru.json new file mode 100644 index 0000000000..85a42bf1be --- /dev/null +++ b/homeassistant/components/kulersky/translations/ru.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/sl.json b/homeassistant/components/kulersky/translations/sl.json new file mode 100644 index 0000000000..0108cb98d6 --- /dev/null +++ b/homeassistant/components/kulersky/translations/sl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "V omre\u017eju ni najdenih naprav.", + "single_instance_allowed": "Je \u017ee name\u0161\u010deno. Mo\u017ena je le ena konfiguracija." + }, + "step": { + "confirm": { + "description": "\u017delite pri\u010deti namestitev?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/tr.json b/homeassistant/components/kulersky/translations/tr.json new file mode 100644 index 0000000000..49fa9545e9 --- /dev/null +++ b/homeassistant/components/kulersky/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/zh-Hant.json b/homeassistant/components/kulersky/translations/zh-Hant.json new file mode 100644 index 0000000000..90c98e491d --- /dev/null +++ b/homeassistant/components/kulersky/translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "step": { + "confirm": { + "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index faba23a52b..72f11b7b00 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -2,11 +2,8 @@ import logging import pypck -import voluptuous as vol -from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.const import ( - CONF_ADDRESS, CONF_BINARY_SENSORS, CONF_COVERS, CONF_HOST, @@ -16,52 +13,21 @@ from homeassistant.const import ( CONF_PORT, CONF_SENSORS, CONF_SWITCHES, - CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity from .const import ( - BINSENSOR_PORTS, CONF_CLIMATES, CONF_CONNECTIONS, CONF_DIM_MODE, - CONF_DIMMABLE, - CONF_LOCKABLE, - CONF_MAX_TEMP, - CONF_MIN_TEMP, - CONF_MOTOR, - CONF_OUTPUT, - CONF_OUTPUTS, - CONF_REGISTER, - CONF_REVERSE_TIME, - CONF_SCENE, CONF_SCENES, - CONF_SETPOINT, CONF_SK_NUM_TRIES, - CONF_SOURCE, - CONF_TRANSITION, DATA_LCN, - DIM_MODES, DOMAIN, - KEYS, - LED_PORTS, - LOGICOP_PORTS, - MOTOR_PORTS, - MOTOR_REVERSE_TIME, - OUTPUT_PORTS, - RELAY_PORTS, - S0_INPUTS, - SETPOINTS, - THRESHOLDS, - VAR_UNITS, - VARIABLES, ) -from .helpers import has_unique_connection_names, is_address +from .schemas import CONFIG_SCHEMA # noqa: 401 from .services import ( DynText, Led, @@ -80,141 +46,6 @@ from .services import ( _LOGGER = logging.getLogger(__name__) -BINARY_SENSORS_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_SOURCE): vol.All( - vol.Upper, vol.In(SETPOINTS + KEYS + BINSENSOR_PORTS) - ), - } -) - -CLIMATES_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_SOURCE): vol.All(vol.Upper, vol.In(VARIABLES)), - vol.Required(CONF_SETPOINT): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS)), - vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), - vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), - vol.Optional(CONF_LOCKABLE, default=False): vol.Coerce(bool), - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=TEMP_CELSIUS): vol.In( - TEMP_CELSIUS, TEMP_FAHRENHEIT - ), - } -) - -COVERS_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_MOTOR): vol.All(vol.Upper, vol.In(MOTOR_PORTS)), - vol.Optional(CONF_REVERSE_TIME): vol.All(vol.Upper, vol.In(MOTOR_REVERSE_TIME)), - } -) - -LIGHTS_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_OUTPUT): vol.All( - vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS) - ), - vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), - vol.Optional(CONF_TRANSITION, default=0): vol.All( - vol.Coerce(float), vol.Range(min=0.0, max=486.0), lambda value: value * 1000 - ), - } -) - -SCENES_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_REGISTER): vol.All(vol.Coerce(int), vol.Range(0, 9)), - vol.Required(CONF_SCENE): vol.All(vol.Coerce(int), vol.Range(0, 9)), - vol.Optional(CONF_OUTPUTS): vol.All( - cv.ensure_list, [vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS))] - ), - vol.Optional(CONF_TRANSITION, default=None): vol.Any( - vol.All( - vol.Coerce(int), - vol.Range(min=0.0, max=486.0), - lambda value: value * 1000, - ), - None, - ), - } -) - -SENSORS_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_SOURCE): vol.All( - vol.Upper, - vol.In( - VARIABLES - + SETPOINTS - + THRESHOLDS - + S0_INPUTS - + LED_PORTS - + LOGICOP_PORTS - ), - ), - vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All( - vol.Upper, vol.In(VAR_UNITS) - ), - } -) - -SWITCHES_SCHEMA = vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ADDRESS): is_address, - vol.Required(CONF_OUTPUT): vol.All( - vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS) - ), - } -) - -CONNECTION_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PORT): cv.port, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SK_NUM_TRIES, default=0): cv.positive_int, - vol.Optional(CONF_DIM_MODE, default="steps50"): vol.All( - vol.Upper, vol.In(DIM_MODES) - ), - vol.Optional(CONF_NAME): cv.string, - } -) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CONNECTIONS): vol.All( - cv.ensure_list, has_unique_connection_names, [CONNECTION_SCHEMA] - ), - vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [BINARY_SENSORS_SCHEMA] - ), - vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATES_SCHEMA]), - vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), - vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), - vol.Optional(CONF_SCENES): vol.All(cv.ensure_list, [SCENES_SCHEMA]), - vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSORS_SCHEMA]), - vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - async def async_setup(hass, config): """Set up the LCN component.""" @@ -286,19 +117,19 @@ async def async_setup(hass, config): ("pck", Pck), ): hass.services.async_register( - DOMAIN, service_name, service(hass), service.schema + DOMAIN, service_name, service(hass).async_call_service, service.schema ) return True -class LcnDevice(Entity): +class LcnEntity(Entity): """Parent class for all devices associated with the LCN component.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN device.""" self.config = config - self.address_connection = address_connection + self.device_connection = device_connection self._name = config[CONF_NAME] @property @@ -308,7 +139,7 @@ class LcnDevice(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" - self.address_connection.register_for_inputs(self.input_received) + self.device_connection.register_for_inputs(self.input_received) @property def name(self): @@ -317,4 +148,3 @@ class LcnDevice(Entity): def input_received(self, input_obj): """Set state/value when LCN input object (command) is received.""" - raise NotImplementedError("Pure virtual function.") diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 7b4cedfeba..5d712045c9 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -4,7 +4,7 @@ import pypck from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import BINSENSOR_PORTS, CONF_CONNECTIONS, CONF_SOURCE, DATA_LCN, SETPOINTS from .helpers import get_connection @@ -36,12 +36,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnRegulatorLockSensor(LcnDevice, BinarySensorEntity): +class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): """Representation of a LCN binary sensor for regulator locks.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN binary sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.setpoint_variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] @@ -50,7 +50,7 @@ class LcnRegulatorLockSensor(LcnDevice, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler( + await self.device_connection.activate_status_request_handler( self.setpoint_variable ) @@ -71,12 +71,12 @@ class LcnRegulatorLockSensor(LcnDevice, BinarySensorEntity): self.async_write_ha_state() -class LcnBinarySensor(LcnDevice, BinarySensorEntity): +class LcnBinarySensor(LcnEntity, BinarySensorEntity): """Representation of a LCN binary sensor for binary sensor ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN binary sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.bin_sensor_port = pypck.lcn_defs.BinSensorPort[config[CONF_SOURCE]] @@ -85,7 +85,7 @@ class LcnBinarySensor(LcnDevice, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler( + await self.device_connection.activate_status_request_handler( self.bin_sensor_port ) @@ -103,12 +103,12 @@ class LcnBinarySensor(LcnDevice, BinarySensorEntity): self.async_write_ha_state() -class LcnLockKeysSensor(LcnDevice, BinarySensorEntity): +class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): """Representation of a LCN sensor for key locks.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.source = pypck.lcn_defs.Key[config[CONF_SOURCE]] self._value = None @@ -116,7 +116,7 @@ class LcnLockKeysSensor(LcnDevice, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.source) + await self.device_connection.activate_status_request_handler(self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index 8b0f4951bf..e3eb92a426 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -5,7 +5,7 @@ import pypck from homeassistant.components.climate import ClimateEntity, const from homeassistant.const import ATTR_TEMPERATURE, CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT -from . import LcnDevice +from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_LOCKABLE, @@ -40,12 +40,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnClimate(LcnDevice, ClimateEntity): +class LcnClimate(LcnEntity, ClimateEntity): """Representation of a LCN climate device.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize of a LCN climate device.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] self.setpoint = pypck.lcn_defs.Var[config[CONF_SETPOINT]] @@ -63,8 +63,8 @@ class LcnClimate(LcnDevice, ClimateEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.variable) - await self.address_connection.activate_status_request_handler(self.setpoint) + await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.setpoint) @property def supported_features(self): @@ -120,16 +120,14 @@ class LcnClimate(LcnDevice, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode == const.HVAC_MODE_HEAT: - if not await self.address_connection.lock_regulator( + if not await self.device_connection.lock_regulator( self.regulator_id, False ): return self._is_on = True self.async_write_ha_state() elif hvac_mode == const.HVAC_MODE_OFF: - if not await self.address_connection.lock_regulator( - self.regulator_id, True - ): + if not await self.device_connection.lock_regulator(self.regulator_id, True): return self._is_on = False self._target_temperature = None @@ -141,7 +139,7 @@ class LcnClimate(LcnDevice, ClimateEntity): if temperature is None: return - if not await self.address_connection.var_abs( + if not await self.device_connection.var_abs( self.setpoint, temperature, self.unit ): return diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index ae88441f89..c5e407573b 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -4,7 +4,7 @@ import pypck from homeassistant.components.cover import CoverEntity from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import CONF_CONNECTIONS, CONF_MOTOR, CONF_REVERSE_TIME, DATA_LCN from .helpers import get_connection @@ -34,12 +34,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputsCover(LcnDevice, CoverEntity): +class LcnOutputsCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to output ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN cover.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output_ids = [ pypck.lcn_defs.OutputPort["OUTPUTUP"].value, @@ -59,10 +59,10 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler( + await self.device_connection.activate_status_request_handler( pypck.lcn_defs.OutputPort["OUTPUTUP"] ) - await self.address_connection.activate_status_request_handler( + await self.device_connection.activate_status_request_handler( pypck.lcn_defs.OutputPort["OUTPUTDOWN"] ) @@ -89,7 +89,7 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_close_cover(self, **kwargs): """Close the cover.""" state = pypck.lcn_defs.MotorStateModifier.DOWN - if not await self.address_connection.control_motors_outputs( + if not await self.device_connection.control_motors_outputs( state, self.reverse_time ): return @@ -100,7 +100,7 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_open_cover(self, **kwargs): """Open the cover.""" state = pypck.lcn_defs.MotorStateModifier.UP - if not await self.address_connection.control_motors_outputs( + if not await self.device_connection.control_motors_outputs( state, self.reverse_time ): return @@ -112,7 +112,7 @@ class LcnOutputsCover(LcnDevice, CoverEntity): async def async_stop_cover(self, **kwargs): """Stop the cover.""" state = pypck.lcn_defs.MotorStateModifier.STOP - if not await self.address_connection.control_motors_outputs(state): + if not await self.device_connection.control_motors_outputs(state): return self._is_closing = False self._is_opening = False @@ -143,12 +143,12 @@ class LcnOutputsCover(LcnDevice, CoverEntity): self.async_write_ha_state() -class LcnRelayCover(LcnDevice, CoverEntity): +class LcnRelayCover(LcnEntity, CoverEntity): """Representation of a LCN cover connected to relays.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN cover.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.motor = pypck.lcn_defs.MotorPort[config[CONF_MOTOR]] self.motor_port_onoff = self.motor.value * 2 @@ -161,7 +161,7 @@ class LcnRelayCover(LcnDevice, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.motor) + await self.device_connection.activate_status_request_handler(self.motor) @property def is_closed(self): @@ -187,7 +187,7 @@ class LcnRelayCover(LcnDevice, CoverEntity): """Close the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.DOWN - if not await self.address_connection.control_motors_relays(states): + if not await self.device_connection.control_motors_relays(states): return self._is_opening = False self._is_closing = True @@ -197,7 +197,7 @@ class LcnRelayCover(LcnDevice, CoverEntity): """Open the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.UP - if not await self.address_connection.control_motors_relays(states): + if not await self.device_connection.control_motors_relays(states): return self._is_closed = False self._is_opening = True @@ -208,7 +208,7 @@ class LcnRelayCover(LcnDevice, CoverEntity): """Stop the cover.""" states = [pypck.lcn_defs.MotorStateModifier.NOCHANGE] * 4 states[self.motor.value] = pypck.lcn_defs.MotorStateModifier.STOP - if not await self.address_connection.control_motors_relays(states): + if not await self.device_connection.control_motors_relays(states): return self._is_closing = False self._is_opening = False diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index f4545817c9..18342aa1d9 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -26,23 +26,25 @@ def get_connection(connections, connection_id=None): return connection -def has_unique_connection_names(connections): +def has_unique_host_names(hosts): """Validate that all connection names are unique. Use 'pchk' as default connection_name (or add a numeric suffix if pchk' is already in use. """ - for suffix, connection in enumerate(connections): - connection_name = connection.get(CONF_NAME) - if connection_name is None: + suffix = 0 + for host in hosts: + host_name = host.get(CONF_NAME) + if host_name is None: if suffix == 0: - connection[CONF_NAME] = DEFAULT_NAME + host[CONF_NAME] = DEFAULT_NAME else: - connection[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}" + host[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}" + suffix += 1 schema = vol.Schema(vol.Unique()) - schema([connection.get(CONF_NAME) for connection in connections]) - return connections + schema([host.get(CONF_NAME) for host in hosts]) + return hosts def is_address(value): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index def025e0cf..c6ef895b7d 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_DIMMABLE, @@ -49,12 +49,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputLight(LcnDevice, LightEntity): +class LcnOutputLight(LcnEntity, LightEntity): """Representation of a LCN light for output ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN light.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] @@ -68,7 +68,7 @@ class LcnOutputLight(LcnDevice, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def supported_features(self): @@ -100,7 +100,7 @@ class LcnOutputLight(LcnDevice, LightEntity): else: transition = self._transition - if not await self.address_connection.dim_output( + if not await self.device_connection.dim_output( self.output.value, percent, transition ): return @@ -117,7 +117,7 @@ class LcnOutputLight(LcnDevice, LightEntity): else: transition = self._transition - if not await self.address_connection.dim_output( + if not await self.device_connection.dim_output( self.output.value, 0, transition ): return @@ -141,12 +141,12 @@ class LcnOutputLight(LcnDevice, LightEntity): self.async_write_ha_state() -class LcnRelayLight(LcnDevice, LightEntity): +class LcnRelayLight(LcnEntity, LightEntity): """Representation of a LCN light for relay ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN light.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] @@ -155,7 +155,7 @@ class LcnRelayLight(LcnDevice, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -167,7 +167,7 @@ class LcnRelayLight(LcnDevice, LightEntity): states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON - if not await self.address_connection.control_relays(states): + if not await self.device_connection.control_relays(states): return self._is_on = True self.async_write_ha_state() @@ -177,7 +177,7 @@ class LcnRelayLight(LcnDevice, LightEntity): states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF - if not await self.address_connection.control_relays(states): + if not await self.device_connection.control_relays(states): return self._is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py index cac13ee165..ed211473e2 100644 --- a/homeassistant/components/lcn/scene.py +++ b/homeassistant/components/lcn/scene.py @@ -6,7 +6,7 @@ import pypck from homeassistant.components.scene import Scene from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_OUTPUTS, @@ -41,12 +41,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnScene(LcnDevice, Scene): +class LcnScene(LcnEntity, Scene): """Representation of a LCN scene.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN scene.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.register_id = config[CONF_REGISTER] self.scene_id = config[CONF_SCENE] @@ -69,7 +69,7 @@ class LcnScene(LcnDevice, Scene): async def async_activate(self, **kwargs: Any) -> None: """Activate scene.""" - await self.address_connection.activate_scene( + await self.device_connection.activate_scene( self.register_id, self.scene_id, self.output_ports, diff --git a/homeassistant/components/lcn/schemas.py b/homeassistant/components/lcn/schemas.py new file mode 100644 index 0000000000..1cc51f400d --- /dev/null +++ b/homeassistant/components/lcn/schemas.py @@ -0,0 +1,190 @@ +"""Schema definitions for LCN configuration and websockets api.""" +import voluptuous as vol + +from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP +from homeassistant.const import ( + CONF_ADDRESS, + CONF_BINARY_SENSORS, + CONF_COVERS, + CONF_HOST, + CONF_LIGHTS, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + BINSENSOR_PORTS, + CONF_CLIMATES, + CONF_CONNECTIONS, + CONF_DIM_MODE, + CONF_DIMMABLE, + CONF_LOCKABLE, + CONF_MAX_TEMP, + CONF_MIN_TEMP, + CONF_MOTOR, + CONF_OUTPUT, + CONF_OUTPUTS, + CONF_REGISTER, + CONF_REVERSE_TIME, + CONF_SCENE, + CONF_SCENES, + CONF_SETPOINT, + CONF_SK_NUM_TRIES, + CONF_SOURCE, + CONF_TRANSITION, + DIM_MODES, + DOMAIN, + KEYS, + LED_PORTS, + LOGICOP_PORTS, + MOTOR_PORTS, + MOTOR_REVERSE_TIME, + OUTPUT_PORTS, + RELAY_PORTS, + S0_INPUTS, + SETPOINTS, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + THRESHOLDS, + VAR_UNITS, + VARIABLES, +) +from .helpers import has_unique_host_names, is_address + +# +# Domain data +# + +DOMAIN_DATA_BINARY_SENSOR = { + vol.Required(CONF_SOURCE): vol.All( + vol.Upper, vol.In(SETPOINTS + KEYS + BINSENSOR_PORTS) + ), +} + + +DOMAIN_DATA_CLIMATE = { + vol.Required(CONF_SOURCE): vol.All(vol.Upper, vol.In(VARIABLES)), + vol.Required(CONF_SETPOINT): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS)), + vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): vol.Coerce(float), + vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): vol.Coerce(float), + vol.Optional(CONF_LOCKABLE, default=False): vol.Coerce(bool), + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default=TEMP_CELSIUS): vol.In( + TEMP_CELSIUS, TEMP_FAHRENHEIT + ), +} + + +DOMAIN_DATA_COVER = { + vol.Required(CONF_MOTOR): vol.All(vol.Upper, vol.In(MOTOR_PORTS)), + vol.Optional(CONF_REVERSE_TIME, default="rt1200"): vol.All( + vol.Upper, vol.In(MOTOR_REVERSE_TIME) + ), +} + + +DOMAIN_DATA_LIGHT = { + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)), + vol.Optional(CONF_DIMMABLE, default=False): vol.Coerce(bool), + vol.Optional(CONF_TRANSITION, default=0): vol.All( + vol.Coerce(float), vol.Range(min=0.0, max=486.0), lambda value: value * 1000 + ), +} + + +DOMAIN_DATA_SCENE = { + vol.Required(CONF_REGISTER): vol.All(vol.Coerce(int), vol.Range(0, 9)), + vol.Required(CONF_SCENE): vol.All(vol.Coerce(int), vol.Range(0, 9)), + vol.Optional(CONF_OUTPUTS, default=[]): vol.All( + cv.ensure_list, [vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS))] + ), + vol.Optional(CONF_TRANSITION, default=None): vol.Any( + vol.All( + vol.Coerce(int), + vol.Range(min=0.0, max=486.0), + lambda value: value * 1000, + ), + None, + ), +} + +DOMAIN_DATA_SENSOR = { + vol.Required(CONF_SOURCE): vol.All( + vol.Upper, + vol.In( + VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS + LED_PORTS + LOGICOP_PORTS + ), + ), + vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All( + vol.Upper, vol.In(VAR_UNITS) + ), +} + + +DOMAIN_DATA_SWITCH = { + vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS + RELAY_PORTS)), +} + +# +# Configuration +# + +DOMAIN_DATA_BASE = { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ADDRESS): is_address, +} + +BINARY_SENSORS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_BINARY_SENSOR}) + +CLIMATES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_CLIMATE}) + +COVERS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_COVER}) + +LIGHTS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_LIGHT}) + +SCENES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SCENE}) + +SENSORS_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SENSOR}) + +SWITCHES_SCHEMA = vol.Schema({**DOMAIN_DATA_BASE, **DOMAIN_DATA_SWITCH}) + +CONNECTION_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SK_NUM_TRIES, default=0): cv.positive_int, + vol.Optional(CONF_DIM_MODE, default="steps50"): vol.All( + vol.Upper, vol.In(DIM_MODES) + ), + vol.Optional(CONF_NAME): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CONNECTIONS): vol.All( + cv.ensure_list, has_unique_host_names, [CONNECTION_SCHEMA] + ), + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [BINARY_SENSORS_SCHEMA] + ), + vol.Optional(CONF_CLIMATES): vol.All(cv.ensure_list, [CLIMATES_SCHEMA]), + vol.Optional(CONF_COVERS): vol.All(cv.ensure_list, [COVERS_SCHEMA]), + vol.Optional(CONF_LIGHTS): vol.All(cv.ensure_list, [LIGHTS_SCHEMA]), + vol.Optional(CONF_SCENES): vol.All(cv.ensure_list, [SCENES_SCHEMA]), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSORS_SCHEMA]), + vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCHES_SCHEMA]), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index ddf7e61a3f..26b54def97 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -3,7 +3,7 @@ import pypck from homeassistant.const import CONF_ADDRESS, CONF_UNIT_OF_MEASUREMENT -from . import LcnDevice +from . import LcnEntity from .const import ( CONF_CONNECTIONS, CONF_SOURCE, @@ -30,24 +30,24 @@ async def async_setup_platform( addr = pypck.lcn_addr.LcnAddr(*address) connections = hass.data[DATA_LCN][CONF_CONNECTIONS] connection = get_connection(connections, connection_id) - address_connection = connection.get_address_conn(addr) + device_connection = connection.get_address_conn(addr) if config[CONF_SOURCE] in VARIABLES + SETPOINTS + THRESHOLDS + S0_INPUTS: - device = LcnVariableSensor(config, address_connection) + device = LcnVariableSensor(config, device_connection) else: # in LED_PORTS + LOGICOP_PORTS - device = LcnLedLogicSensor(config, address_connection) + device = LcnLedLogicSensor(config, device_connection) devices.append(device) async_add_entities(devices) -class LcnVariableSensor(LcnDevice): +class LcnVariableSensor(LcnEntity): """Representation of a LCN sensor for variables.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.variable = pypck.lcn_defs.Var[config[CONF_SOURCE]] self.unit = pypck.lcn_defs.VarUnit.parse(config[CONF_UNIT_OF_MEASUREMENT]) @@ -57,7 +57,7 @@ class LcnVariableSensor(LcnDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.variable) @property def state(self): @@ -81,12 +81,12 @@ class LcnVariableSensor(LcnDevice): self.async_write_ha_state() -class LcnLedLogicSensor(LcnDevice): +class LcnLedLogicSensor(LcnEntity): """Representation of a LCN sensor for leds and logicops.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN sensor.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) if config[CONF_SOURCE] in LED_PORTS: self.source = pypck.lcn_defs.LedPort[config[CONF_SOURCE]] @@ -98,7 +98,7 @@ class LcnLedLogicSensor(LcnDevice): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.source) + await self.device_connection.activate_status_request_handler(self.source) @property def state(self): diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index baa318f891..d7d8acf4f2 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -1,4 +1,5 @@ """Service calls related dependencies for LCN component.""" + import pypck import voluptuous as vol @@ -54,11 +55,12 @@ class LcnServiceCall: def __init__(self, hass): """Initialize service call.""" + self.hass = hass self.connections = hass.data[DATA_LCN][CONF_CONNECTIONS] - def get_address_connection(self, call): - """Get address connection object.""" - addr, connection_id = call.data[CONF_ADDRESS] + def get_device_connection(self, service): + """Get device connection object.""" + addr, connection_id = service.data[CONF_ADDRESS] addr = pypck.lcn_addr.LcnAddr(*addr) if connection_id is None: connection = self.connections[0] @@ -67,6 +69,10 @@ class LcnServiceCall: return connection.get_address_conn(addr) + async def async_call_service(self, service): + """Execute service call.""" + raise NotImplementedError + class OutputAbs(LcnServiceCall): """Set absolute brightness of output port in percent.""" @@ -83,16 +89,16 @@ class OutputAbs(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] - brightness = call.data[CONF_BRIGHTNESS] + output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] + brightness = service.data[CONF_BRIGHTNESS] transition = pypck.lcn_defs.time_to_ramp_value( - call.data[CONF_TRANSITION] * 1000 + service.data[CONF_TRANSITION] * 1000 ) - address_connection = self.get_address_connection(call) - address_connection.dim_output(output.value, brightness, transition) + device_connection = self.get_device_connection(service) + await device_connection.dim_output(output.value, brightness, transition) class OutputRel(LcnServiceCall): @@ -107,13 +113,13 @@ class OutputRel(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] - brightness = call.data[CONF_BRIGHTNESS] + output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] + brightness = service.data[CONF_BRIGHTNESS] - address_connection = self.get_address_connection(call) - address_connection.rel_output(output.value, brightness) + device_connection = self.get_device_connection(service) + await device_connection.rel_output(output.value, brightness) class OutputToggle(LcnServiceCall): @@ -128,15 +134,15 @@ class OutputToggle(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]] + output = pypck.lcn_defs.OutputPort[service.data[CONF_OUTPUT]] transition = pypck.lcn_defs.time_to_ramp_value( - call.data[CONF_TRANSITION] * 1000 + service.data[CONF_TRANSITION] * 1000 ) - address_connection = self.get_address_connection(call) - address_connection.toggle_output(output.value, transition) + device_connection = self.get_device_connection(service) + await device_connection.toggle_output(output.value, transition) class Relays(LcnServiceCall): @@ -146,14 +152,15 @@ class Relays(LcnServiceCall): {vol.Required(CONF_STATE): is_relays_states_string} ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" states = [ - pypck.lcn_defs.RelayStateModifier[state] for state in call.data[CONF_STATE] + pypck.lcn_defs.RelayStateModifier[state] + for state in service.data[CONF_STATE] ] - address_connection = self.get_address_connection(call) - address_connection.control_relays(states) + device_connection = self.get_device_connection(service) + await device_connection.control_relays(states) class Led(LcnServiceCall): @@ -166,13 +173,13 @@ class Led(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - led = pypck.lcn_defs.LedPort[call.data[CONF_LED]] - led_state = pypck.lcn_defs.LedStatus[call.data[CONF_STATE]] + led = pypck.lcn_defs.LedPort[service.data[CONF_LED]] + led_state = pypck.lcn_defs.LedStatus[service.data[CONF_STATE]] - address_connection = self.get_address_connection(call) - address_connection.control_led(led, led_state) + device_connection = self.get_device_connection(service) + await device_connection.control_led(led, led_state) class VarAbs(LcnServiceCall): @@ -194,14 +201,14 @@ class VarAbs(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] - value = call.data[CONF_VALUE] - unit = pypck.lcn_defs.VarUnit.parse(call.data[CONF_UNIT_OF_MEASUREMENT]) + var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] + value = service.data[CONF_VALUE] + unit = pypck.lcn_defs.VarUnit.parse(service.data[CONF_UNIT_OF_MEASUREMENT]) - address_connection = self.get_address_connection(call) - address_connection.var_abs(var, value, unit) + device_connection = self.get_device_connection(service) + await device_connection.var_abs(var, value, unit) class VarReset(LcnServiceCall): @@ -211,12 +218,12 @@ class VarReset(LcnServiceCall): {vol.Required(CONF_VARIABLE): vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS))} ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] + var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] - address_connection = self.get_address_connection(call) - address_connection.var_reset(var) + device_connection = self.get_device_connection(service) + await device_connection.var_reset(var) class VarRel(LcnServiceCall): @@ -237,15 +244,15 @@ class VarRel(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]] - value = call.data[CONF_VALUE] - unit = pypck.lcn_defs.VarUnit.parse(call.data[CONF_UNIT_OF_MEASUREMENT]) - value_ref = pypck.lcn_defs.RelVarRef[call.data[CONF_RELVARREF]] + var = pypck.lcn_defs.Var[service.data[CONF_VARIABLE]] + value = service.data[CONF_VALUE] + unit = pypck.lcn_defs.VarUnit.parse(service.data[CONF_UNIT_OF_MEASUREMENT]) + value_ref = pypck.lcn_defs.RelVarRef[service.data[CONF_RELVARREF]] - address_connection = self.get_address_connection(call) - address_connection.var_rel(var, value, unit, value_ref) + device_connection = self.get_device_connection(service) + await device_connection.var_rel(var, value, unit, value_ref) class LockRegulator(LcnServiceCall): @@ -258,14 +265,14 @@ class LockRegulator(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - setpoint = pypck.lcn_defs.Var[call.data[CONF_SETPOINT]] - state = call.data[CONF_STATE] + setpoint = pypck.lcn_defs.Var[service.data[CONF_SETPOINT]] + state = service.data[CONF_STATE] reg_id = pypck.lcn_defs.Var.to_set_point_id(setpoint) - address_connection = self.get_address_connection(call) - address_connection.lock_regulator(reg_id, state) + device_connection = self.get_device_connection(service) + await device_connection.lock_regulator(reg_id, state) class SendKeys(LcnServiceCall): @@ -286,31 +293,31 @@ class SendKeys(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - address_connection = self.get_address_connection(call) + device_connection = self.get_device_connection(service) keys = [[False] * 8 for i in range(4)] - key_strings = zip(call.data[CONF_KEYS][::2], call.data[CONF_KEYS][1::2]) + key_strings = zip(service.data[CONF_KEYS][::2], service.data[CONF_KEYS][1::2]) for table, key in key_strings: table_id = ord(table) - 65 key_id = int(key) - 1 keys[table_id][key_id] = True - delay_time = call.data[CONF_TIME] + delay_time = service.data[CONF_TIME] if delay_time != 0: hit = pypck.lcn_defs.SendKeyCommand.HIT - if pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]] != hit: + if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] != hit: raise ValueError( "Only hit command is allowed when sending deferred keys." ) - delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) - address_connection.send_keys_hit_deferred(keys, delay_time, delay_unit) + delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT]) + await device_connection.send_keys_hit_deferred(keys, delay_time, delay_unit) else: - state = pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]] - address_connection.send_keys(keys, state) + state = pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] + await device_connection.send_keys(keys, state) class LockKeys(LcnServiceCall): @@ -329,28 +336,31 @@ class LockKeys(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - address_connection = self.get_address_connection(call) + device_connection = self.get_device_connection(service) states = [ pypck.lcn_defs.KeyLockStateModifier[state] - for state in call.data[CONF_STATE] + for state in service.data[CONF_STATE] ] - table_id = ord(call.data[CONF_TABLE]) - 65 + table_id = ord(service.data[CONF_TABLE]) - 65 - delay_time = call.data[CONF_TIME] + delay_time = service.data[CONF_TIME] if delay_time != 0: if table_id != 0: raise ValueError( "Only table A is allowed when locking keys for a specific time." ) - delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) - address_connection.lock_keys_tab_a_temporary(delay_time, delay_unit, states) + delay_unit = pypck.lcn_defs.TimeUnit.parse(service.data[CONF_TIME_UNIT]) + await device_connection.lock_keys_tab_a_temporary( + delay_time, delay_unit, states + ) else: - address_connection.lock_keys(table_id, states) + await device_connection.lock_keys(table_id, states) - address_connection.request_status_locked_keys_timeout() + handler = device_connection.status_request_handler + await handler.request_status_locked_keys_timeout() class DynText(LcnServiceCall): @@ -363,13 +373,13 @@ class DynText(LcnServiceCall): } ) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - row_id = call.data[CONF_ROW] - 1 - text = call.data[CONF_TEXT] + row_id = service.data[CONF_ROW] - 1 + text = service.data[CONF_TEXT] - address_connection = self.get_address_connection(call) - address_connection.dyn_text(row_id, text) + device_connection = self.get_device_connection(service) + await device_connection.dyn_text(row_id, text) class Pck(LcnServiceCall): @@ -377,8 +387,8 @@ class Pck(LcnServiceCall): schema = LcnServiceCall.schema.extend({vol.Required(CONF_PCK): str}) - def __call__(self, call): + async def async_call_service(self, service): """Execute service call.""" - pck = call.data[CONF_PCK] - address_connection = self.get_address_connection(call) - address_connection.pck(pck) + pck = service.data[CONF_PCK] + device_connection = self.get_device_connection(service) + await device_connection.pck(pck) diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 1d6f7cb6df..5891629627 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -4,7 +4,7 @@ import pypck from homeassistant.components.switch import SwitchEntity from homeassistant.const import CONF_ADDRESS -from . import LcnDevice +from . import LcnEntity from .const import CONF_CONNECTIONS, CONF_OUTPUT, DATA_LCN, OUTPUT_PORTS from .helpers import get_connection @@ -36,12 +36,12 @@ async def async_setup_platform( async_add_entities(devices) -class LcnOutputSwitch(LcnDevice, SwitchEntity): +class LcnOutputSwitch(LcnEntity, SwitchEntity): """Representation of a LCN switch for output ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN switch.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output = pypck.lcn_defs.OutputPort[config[CONF_OUTPUT]] @@ -50,7 +50,7 @@ class LcnOutputSwitch(LcnDevice, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -59,14 +59,14 @@ class LcnOutputSwitch(LcnDevice, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - if not await self.address_connection.dim_output(self.output.value, 100, 0): + if not await self.device_connection.dim_output(self.output.value, 100, 0): return self._is_on = True self.async_write_ha_state() async def async_turn_off(self, **kwargs): """Turn the entity off.""" - if not await self.address_connection.dim_output(self.output.value, 0, 0): + if not await self.device_connection.dim_output(self.output.value, 0, 0): return self._is_on = False self.async_write_ha_state() @@ -83,12 +83,12 @@ class LcnOutputSwitch(LcnDevice, SwitchEntity): self.async_write_ha_state() -class LcnRelaySwitch(LcnDevice, SwitchEntity): +class LcnRelaySwitch(LcnEntity, SwitchEntity): """Representation of a LCN switch for relay ports.""" - def __init__(self, config, address_connection): + def __init__(self, config, device_connection): """Initialize the LCN switch.""" - super().__init__(config, address_connection) + super().__init__(config, device_connection) self.output = pypck.lcn_defs.RelayPort[config[CONF_OUTPUT]] @@ -97,7 +97,7 @@ class LcnRelaySwitch(LcnDevice, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.address_connection.activate_status_request_handler(self.output) + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -108,7 +108,7 @@ class LcnRelaySwitch(LcnDevice, SwitchEntity): """Turn the entity on.""" states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.ON - if not await self.address_connection.control_relays(states): + if not await self.device_connection.control_relays(states): return self._is_on = True self.async_write_ha_state() @@ -118,7 +118,7 @@ class LcnRelaySwitch(LcnDevice, SwitchEntity): states = [pypck.lcn_defs.RelayStateModifier.NOCHANGE] * 8 states[self.output.value] = pypck.lcn_defs.RelayStateModifier.OFF - if not await self.address_connection.control_relays(states): + if not await self.device_connection.control_relays(states): return self._is_on = False self.async_write_ha_state() diff --git a/homeassistant/components/life360/translations/pt.json b/homeassistant/components/life360/translations/pt.json index 9c848bd8ec..71370e4006 100644 --- a/homeassistant/components/life360/translations/pt.json +++ b/homeassistant/components/life360/translations/pt.json @@ -1,7 +1,14 @@ { "config": { + "abort": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "error": { - "invalid_username": "Nome de utilizador incorreto" + "already_configured": "Conta j\u00e1 configurada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "invalid_username": "Nome de utilizador incorreto", + "unknown": "Erro inesperado" }, "step": { "user": { diff --git a/homeassistant/components/lifx/translations/zh-Hant.json b/homeassistant/components/lifx/translations/zh-Hant.json index ed704711a6..154e82ec30 100644 --- a/homeassistant/components/lifx/translations/zh-Hant.json +++ b/homeassistant/components/lifx/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/local_ip/translations/pt.json b/homeassistant/components/local_ip/translations/pt.json new file mode 100644 index 0000000000..c5a4032636 --- /dev/null +++ b/homeassistant/components/local_ip/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + }, + "title": "Endere\u00e7o IP Local" +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/zh-Hant.json b/homeassistant/components/local_ip/translations/zh-Hant.json index d0238ff743..b14abdd6b6 100644 --- a/homeassistant/components/local_ip/translations/zh-Hant.json +++ b/homeassistant/components/local_ip/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/locative/translations/pt.json b/homeassistant/components/locative/translations/pt.json index 6ca0b0b194..9357506812 100644 --- a/homeassistant/components/locative/translations/pt.json +++ b/homeassistant/components/locative/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar um webhook no Locative. \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n\n Veja [the documentation]({docs_url}) para obter mais detalhes." }, diff --git a/homeassistant/components/locative/translations/zh-Hant.json b/homeassistant/components/locative/translations/zh-Hant.json index 65dc4ff8da..8c2dcdb53e 100644 --- a/homeassistant/components/locative/translations/zh-Hant.json +++ b/homeassistant/components/locative/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/logi_circle/translations/no.json b/homeassistant/components/logi_circle/translations/no.json index 14ffab8711..94aa56bb63 100644 --- a/homeassistant/components/logi_circle/translations/no.json +++ b/homeassistant/components/logi_circle/translations/no.json @@ -4,23 +4,23 @@ "already_configured": "Kontoen er allerede konfigurert", "external_error": "Det oppstod et unntak fra en annen flow.", "external_setup": "Logi Circle er vellykket konfigurert fra en annen flow.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" }, "error": { - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send", "invalid_auth": "Ugyldig godkjenning" }, "step": { "auth": { - "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Logi Circle kontoen din, kom deretter tilbake og trykk **Send** nedenfor. \n\n [Link]({authorization_url})", + "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Logi Circle kontoen din, kom deretter tilbake og trykk **Send** nedenfor \n\n [Link]({authorization_url})", "title": "Godkjenn med Logi Circle" }, "user": { "data": { "flow_impl": "Tilbyder" }, - "description": "Velg med hvilken godkjenningsleverand\u00f8r du vil godkjenne Logi Circle.", + "description": "Velg med hvilken godkjenningsleverand\u00f8r du vil godkjenne Logi Circle", "title": "Godkjenningsleverand\u00f8r" } } diff --git a/homeassistant/components/logi_circle/translations/pt.json b/homeassistant/components/logi_circle/translations/pt.json new file mode 100644 index 0000000000..3837580154 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o." + }, + "error": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "title": "Provedor de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/cs.json b/homeassistant/components/lovelace/translations/cs.json index f946a859ea..d08fcdc7fe 100644 --- a/homeassistant/components/lovelace/translations/cs.json +++ b/homeassistant/components/lovelace/translations/cs.json @@ -1,7 +1,7 @@ { "system_health": { "info": { - "dashboards": "Dashboardy", + "dashboards": "Ovl\u00e1dac\u00ed panely", "mode": "Re\u017eim", "resources": "Zdroje", "views": "Pohledy" diff --git a/homeassistant/components/lovelace/translations/de.json b/homeassistant/components/lovelace/translations/de.json new file mode 100644 index 0000000000..c8680fcb7e --- /dev/null +++ b/homeassistant/components/lovelace/translations/de.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "views": "Ansichten" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/et.json b/homeassistant/components/lovelace/translations/et.json index 15c253dd4d..b5a1552efc 100644 --- a/homeassistant/components/lovelace/translations/et.json +++ b/homeassistant/components/lovelace/translations/et.json @@ -1,10 +1,10 @@ { "system_health": { "info": { - "dashboards": "Vaated", + "dashboards": "Vaateid", "mode": "Re\u017eiim", "resources": "Ressursid", - "views": "Vaated" + "views": "Paneele" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/nl.json b/homeassistant/components/lovelace/translations/nl.json new file mode 100644 index 0000000000..ca8388f5be --- /dev/null +++ b/homeassistant/components/lovelace/translations/nl.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "Dashboards", + "mode": "Modus", + "resources": "Bronnen", + "views": "Weergaven" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/tr.json b/homeassistant/components/lovelace/translations/tr.json new file mode 100644 index 0000000000..9f763d0d6c --- /dev/null +++ b/homeassistant/components/lovelace/translations/tr.json @@ -0,0 +1,9 @@ +{ + "system_health": { + "info": { + "dashboards": "Kontrol panelleri", + "mode": "Mod", + "resources": "Kaynaklar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/zh-Hans.json b/homeassistant/components/lovelace/translations/zh-Hans.json index a30b7b2518..5807cdd7c1 100644 --- a/homeassistant/components/lovelace/translations/zh-Hans.json +++ b/homeassistant/components/lovelace/translations/zh-Hans.json @@ -1,10 +1,10 @@ { "system_health": { "info": { - "dashboards": "\u4eea\u8868\u76d8", + "dashboards": "\u4eea\u8868\u76d8\u6570\u91cf", "mode": "\u6a21\u5f0f", - "resources": "\u8d44\u6e90", - "views": "\u89c6\u56fe" + "resources": "\u8d44\u6e90\u6570\u91cf", + "views": "\u89c6\u56fe\u6570\u91cf" } } } \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/pt.json b/homeassistant/components/luftdaten/translations/pt.json index 1f8cb29ff0..5811494fff 100644 --- a/homeassistant/components/luftdaten/translations/pt.json +++ b/homeassistant/components/luftdaten/translations/pt.json @@ -1,6 +1,8 @@ { "config": { "error": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_sensor": "Sensor n\u00e3o dispon\u00edvel ou inv\u00e1lido" }, "step": { diff --git a/homeassistant/components/lutron_caseta/translations/de.json b/homeassistant/components/lutron_caseta/translations/de.json index 12f0a2859e..13f8c6bd80 100644 --- a/homeassistant/components/lutron_caseta/translations/de.json +++ b/homeassistant/components/lutron_caseta/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { diff --git a/homeassistant/components/lutron_caseta/translations/pt.json b/homeassistant/components/lutron_caseta/translations/pt.json new file mode 100644 index 0000000000..a04f550a71 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index ab4e0832ed..4e8df0d5e9 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { diff --git a/homeassistant/components/mailgun/translations/pt.json b/homeassistant/components/mailgun/translations/pt.json index 2a193a19f9..614256fc70 100644 --- a/homeassistant/components/mailgun/translations/pt.json +++ b/homeassistant/components/mailgun/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar [Webhooks with Mailgun]({mailgun_url}). \n\n Preencha as seguintes informa\u00e7\u00f5es: \n\n - URL: `{webhook_url}`\n - M\u00e9todo: POST \n - Tipo de Conte\u00fado: application/json\n\n Veja [a documenta\u00e7\u00e3o]({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, diff --git a/homeassistant/components/mailgun/translations/zh-Hant.json b/homeassistant/components/mailgun/translations/zh-Hant.json index 19c41241a5..508a652ce9 100644 --- a/homeassistant/components/mailgun/translations/zh-Hant.json +++ b/homeassistant/components/mailgun/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index b834cbc0aa..2c17d85f7b 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.11.12"], + "requirements": ["youtube_dl==2020.12.29"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/melcloud/translations/pt.json b/homeassistant/components/melcloud/translations/pt.json index 25623dc04a..67f59434d4 100644 --- a/homeassistant/components/melcloud/translations/pt.json +++ b/homeassistant/components/melcloud/translations/pt.json @@ -4,6 +4,8 @@ "already_configured": "Integra\u00e7\u00e3o com o MELCloud j\u00e1 configurada para este email. O token de acesso foi atualizado." }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/met/translations/pt.json b/homeassistant/components/met/translations/pt.json index 6641658bd4..2cfc8af2d6 100644 --- a/homeassistant/components/met/translations/pt.json +++ b/homeassistant/components/met/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index 152999b557..3034135f84 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -3,8 +3,8 @@ import asyncio from datetime import timedelta import logging -from meteofrance.client import MeteoFranceClient -from meteofrance.helpers import is_valid_warning_department +from meteofrance_api.client import MeteoFranceClient +from meteofrance_api.helpers import is_valid_warning_department import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry diff --git a/homeassistant/components/meteo_france/config_flow.py b/homeassistant/components/meteo_france/config_flow.py index 4593a392ee..f4d7c5dccf 100644 --- a/homeassistant/components/meteo_france/config_flow.py +++ b/homeassistant/components/meteo_france/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure the Meteo-France integration.""" import logging -from meteofrance.client import MeteoFranceClient +from meteofrance_api.client import MeteoFranceClient import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index bfbaa828ea..d642d3c6e0 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -26,6 +26,7 @@ from homeassistant.const import ( PRESSURE_HPA, SPEED_KILOMETERS_PER_HOUR, TEMP_CELSIUS, + UV_INDEX, ) DOMAIN = "meteo_france" @@ -84,6 +85,14 @@ SENSOR_TYPES = { ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "probability_forecast:freezing", }, + "wind_gust": { + ENTITY_NAME: "Wind gust", + ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR, + ENTITY_ICON: "mdi:weather-windy-variant", + ENTITY_DEVICE_CLASS: None, + ENTITY_ENABLE: False, + ENTITY_API_DATA_PATH: "current_forecast:wind:gust", + }, "wind_speed": { ENTITY_NAME: "Wind speed", ENTITY_UNIT: SPEED_KILOMETERS_PER_HOUR, @@ -110,7 +119,7 @@ SENSOR_TYPES = { }, "uv": { ENTITY_NAME: "UV", - ENTITY_UNIT: None, + ENTITY_UNIT: UV_INDEX, ENTITY_ICON: "mdi:sunglasses", ENTITY_DEVICE_CLASS: None, ENTITY_ENABLE: True, @@ -140,6 +149,22 @@ SENSOR_TYPES = { ENTITY_ENABLE: True, ENTITY_API_DATA_PATH: "current_forecast:clouds", }, + "original_condition": { + ENTITY_NAME: "Original condition", + ENTITY_UNIT: None, + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: None, + ENTITY_ENABLE: False, + ENTITY_API_DATA_PATH: "current_forecast:weather:desc", + }, + "daily_original_condition": { + ENTITY_NAME: "Daily original condition", + ENTITY_UNIT: None, + ENTITY_ICON: None, + ENTITY_DEVICE_CLASS: None, + ENTITY_ENABLE: False, + ENTITY_API_DATA_PATH: "today_forecast:weather12H:desc", + }, } CONDITION_CLASSES = { diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index 97c9b589c4..8de4e76c6f 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/meteo_france", "requirements": [ - "meteofrance-api==0.1.1" + "meteofrance-api==1.0.1" ], "codeowners": [ "@hacf-fr", diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 3c88914aaf..8e6b036202 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -1,7 +1,7 @@ """Support for Meteo-France raining forecast sensor.""" import logging -from meteofrance.helpers import ( +from meteofrance_api.helpers import ( get_warning_text_status_from_indice_color, readeable_phenomenoms_dict, ) @@ -115,7 +115,7 @@ class MeteoFranceSensor(CoordinatorEntity): else: value = data[path[1]] - if self._type == "wind_speed": + if self._type in ["wind_speed", "wind_gust"]: # convert API wind speed from m/s to km/h value = round(value * 3.6) return value diff --git a/homeassistant/components/meteo_france/translations/pt.json b/homeassistant/components/meteo_france/translations/pt.json index 025d58f519..f53975ecf0 100644 --- a/homeassistant/components/meteo_france/translations/pt.json +++ b/homeassistant/components/meteo_france/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json index 0db5c5a422..74c204b968 100644 --- a/homeassistant/components/metoffice/translations/de.json +++ b/homeassistant/components/metoffice/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Service ist bereits konfiguriert" }, + "error": { + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/metoffice/translations/pt.json b/homeassistant/components/metoffice/translations/pt.json new file mode 100644 index 0000000000..d974101d0a --- /dev/null +++ b/homeassistant/components/metoffice/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/pt.json b/homeassistant/components/mikrotik/translations/pt.json index 77ce7025f7..72d275069c 100644 --- a/homeassistant/components/mikrotik/translations/pt.json +++ b/homeassistant/components/mikrotik/translations/pt.json @@ -1,12 +1,22 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "name_exists": "Nome existe" + }, "step": { "user": { "data": { "host": "Servidor", + "name": "Nome", "password": "Palavra-passe", "port": "Porta", - "username": "Nome de Utilizador" + "username": "Nome de Utilizador", + "verify_ssl": "Utilizar SSL" } } } diff --git a/homeassistant/components/mikrotik/translations/zh-Hant.json b/homeassistant/components/mikrotik/translations/zh-Hant.json index 0675ede61b..6c3049eff0 100644 --- a/homeassistant/components/mikrotik/translations/zh-Hant.json +++ b/homeassistant/components/mikrotik/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/mill/translations/pt.json b/homeassistant/components/mill/translations/pt.json index b8a454fbab..4348cecf5c 100644 --- a/homeassistant/components/mill/translations/pt.json +++ b/homeassistant/components/mill/translations/pt.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } diff --git a/homeassistant/components/mobile_app/translations/ca.json b/homeassistant/components/mobile_app/translations/ca.json index bb070f391a..a36fd1ca13 100644 --- a/homeassistant/components/mobile_app/translations/ca.json +++ b/homeassistant/components/mobile_app/translations/ca.json @@ -8,5 +8,10 @@ "description": "Vols configurar el component d'aplicaci\u00f3 m\u00f2bil?" } } + }, + "device_automation": { + "action_type": { + "notify": "Envia una notificaci\u00f3" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/de.json b/homeassistant/components/mobile_app/translations/de.json index 0a2f8461be..493ceb4dfd 100644 --- a/homeassistant/components/mobile_app/translations/de.json +++ b/homeassistant/components/mobile_app/translations/de.json @@ -8,5 +8,10 @@ "description": "M\u00f6chtest du die Mobile App-Komponente einrichten?" } } + }, + "device_automation": { + "action_type": { + "notify": "Sende eine Benachrichtigung" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/hu.json b/homeassistant/components/mobile_app/translations/hu.json index c44f51b02e..301075e0ad 100644 --- a/homeassistant/components/mobile_app/translations/hu.json +++ b/homeassistant/components/mobile_app/translations/hu.json @@ -8,5 +8,10 @@ "description": "Be szeretn\u00e9d \u00e1ll\u00edtani a mobil alkalmaz\u00e1s komponenst?" } } + }, + "device_automation": { + "action_type": { + "notify": "\u00c9rtes\u00edt\u00e9s k\u00fcld\u00e9se" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/it.json b/homeassistant/components/mobile_app/translations/it.json index 8ff6dfb982..f5ba52b1a5 100644 --- a/homeassistant/components/mobile_app/translations/it.json +++ b/homeassistant/components/mobile_app/translations/it.json @@ -8,5 +8,10 @@ "description": "Si desidera configurare il componente App per dispositivi mobili?" } } + }, + "device_automation": { + "action_type": { + "notify": "Invia una notifica" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/nl.json b/homeassistant/components/mobile_app/translations/nl.json index 9d5bdcfa20..17a20705cd 100644 --- a/homeassistant/components/mobile_app/translations/nl.json +++ b/homeassistant/components/mobile_app/translations/nl.json @@ -8,5 +8,10 @@ "description": "Wilt u de Mobile App component instellen?" } } + }, + "device_automation": { + "action_type": { + "notify": "Stuur een notificatie" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/pt.json b/homeassistant/components/mobile_app/translations/pt.json index bfef7be3f1..9ca512ca53 100644 --- a/homeassistant/components/mobile_app/translations/pt.json +++ b/homeassistant/components/mobile_app/translations/pt.json @@ -8,5 +8,10 @@ "description": "Deseja configurar o componente Mobile App?" } } + }, + "device_automation": { + "action_type": { + "notify": "Enviar uma notifica\u00e7\u00e3o" + } } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/sl.json b/homeassistant/components/mobile_app/translations/sl.json index 777776ce42..e0810147cd 100644 --- a/homeassistant/components/mobile_app/translations/sl.json +++ b/homeassistant/components/mobile_app/translations/sl.json @@ -8,5 +8,10 @@ "description": "Ali \u017eelite nastaviti komponento aplikacije Mobile App?" } } + }, + "device_automation": { + "action_type": { + "notify": "Po\u0161lji obvestilo" + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/pt.json b/homeassistant/components/monoprice/translations/pt.json index ccc0fc7c47..d73c17a62c 100644 --- a/homeassistant/components/monoprice/translations/pt.json +++ b/homeassistant/components/monoprice/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { @@ -10,5 +14,20 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nome da fonte #1", + "source_2": "Nome da fonte #2", + "source_3": "Nome da fonte #3", + "source_4": "Nome da fonte #4", + "source_5": "Nome da fonte #5", + "source_6": "Nome da fonte #6" + }, + "title": "Configurar fontes" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/zh-Hant.json b/homeassistant/components/monoprice/translations/zh-Hant.json index e653bda920..b54a678398 100644 --- a/homeassistant/components/monoprice/translations/zh-Hant.json +++ b/homeassistant/components/monoprice/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -18,7 +18,7 @@ "source_5": "\u4f86\u6e90 #5 \u540d\u7a31", "source_6": "\u4f86\u6e90 #6 \u540d\u7a31" }, - "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" } } }, diff --git a/homeassistant/components/moon/sensor.py b/homeassistant/components/moon/sensor.py index f7914177f8..9e0f8ef51d 100644 --- a/homeassistant/components/moon/sensor.py +++ b/homeassistant/components/moon/sensor.py @@ -49,6 +49,7 @@ class MoonSensor(Entity): """Initialize the moon sensor.""" self._name = name self._state = None + self._astral = Astral() @property def name(self): @@ -87,4 +88,4 @@ class MoonSensor(Entity): async def async_update(self): """Get the time and updates the states.""" today = dt_util.as_local(dt_util.utcnow()).date() - self._state = Astral().moon_phase(today) + self._state = self._astral.moon_phase(today) diff --git a/homeassistant/components/motion_blinds/__init__.py b/homeassistant/components/motion_blinds/__init__.py index 72929e1ecb..e10f1655d2 100644 --- a/homeassistant/components/motion_blinds/__init__.py +++ b/homeassistant/components/motion_blinds/__init__.py @@ -1,24 +1,31 @@ """The motion_blinds component.""" -from asyncio import TimeoutError as AsyncioTimeoutError +import asyncio from datetime import timedelta import logging from socket import timeout +from motionblinds import MotionMulticast + from homeassistant import config_entries, core -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER +from .const import ( + DOMAIN, + KEY_COORDINATOR, + KEY_GATEWAY, + KEY_MULTICAST_LISTENER, + MANUFACTURER, + MOTION_PLATFORMS, +) from .gateway import ConnectMotionGateway _LOGGER = logging.getLogger(__name__) -MOTION_PLATFORMS = ["cover", "sensor"] - -async def async_setup(hass: core.HomeAssistant, config: dict): +def setup(hass: core.HomeAssistant, config: dict): """Set up the Motion Blinds component.""" return True @@ -31,8 +38,23 @@ async def async_setup_entry( host = entry.data[CONF_HOST] key = entry.data[CONF_API_KEY] + # Create multicast Listener + if KEY_MULTICAST_LISTENER not in hass.data[DOMAIN]: + multicast = MotionMulticast() + hass.data[DOMAIN][KEY_MULTICAST_LISTENER] = multicast + # start listening for local pushes (only once) + await hass.async_add_executor_job(multicast.Start_listen) + + # register stop callback to shutdown listening for local pushes + def stop_motion_multicast(event): + """Stop multicast thread.""" + _LOGGER.debug("Shutting down Motion Listener") + multicast.Stop_listen() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_motion_multicast) + # Connect to motion gateway - connect_gateway_class = ConnectMotionGateway(hass) + connect_gateway_class = ConnectMotionGateway(hass, multicast) if not await connect_gateway_class.async_connect_gateway(host, key): raise ConfigEntryNotReady motion_gateway = connect_gateway_class.gateway_device @@ -41,14 +63,19 @@ async def async_setup_entry( """Call all updates using one async_add_executor_job.""" motion_gateway.Update() for blind in motion_gateway.device_list.values(): - blind.Update() + try: + blind.Update() + except timeout: + # let the error be logged and handled by the motionblinds library + pass async def async_update_data(): """Fetch data from the gateway and blinds.""" try: await hass.async_add_executor_job(update_gateway) - except timeout as socket_timeout: - raise AsyncioTimeoutError from socket_timeout + except timeout: + # let the error be logged and handled by the motionblinds library + pass coordinator = DataUpdateCoordinator( hass, @@ -57,7 +84,7 @@ async def async_setup_entry( name=entry.title, update_method=async_update_data, # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=10), + update_interval=timedelta(seconds=600), ) # Fetch initial data so we have data when entities subscribe @@ -91,11 +118,22 @@ async def async_unload_entry( hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry ): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - config_entry, "cover" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in MOTION_PLATFORMS + ] + ) ) if unload_ok: hass.data[DOMAIN].pop(config_entry.entry_id) + if len(hass.data[DOMAIN]) == 1: + # No motion gateways left, stop Motion multicast + _LOGGER.debug("Shutting down Motion Listener") + multicast = hass.data[DOMAIN].pop(KEY_MULTICAST_LISTENER) + await hass.async_add_executor_job(multicast.Stop_listen) + return unload_ok diff --git a/homeassistant/components/motion_blinds/config_flow.py b/homeassistant/components/motion_blinds/config_flow.py index fbee7d1b43..cb85b45e0e 100644 --- a/homeassistant/components/motion_blinds/config_flow.py +++ b/homeassistant/components/motion_blinds/config_flow.py @@ -1,22 +1,27 @@ """Config flow to configure Motion Blinds using their WLAN API.""" import logging +from motionblinds import MotionDiscovery import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_HOST # pylint: disable=unused-import -from .const import DOMAIN +from .const import DEFAULT_GATEWAY_NAME, DOMAIN from .gateway import ConnectMotionGateway _LOGGER = logging.getLogger(__name__) -DEFAULT_GATEWAY_NAME = "Motion Gateway" CONFIG_SCHEMA = vol.Schema( { - vol.Required(CONF_HOST): str, + vol.Optional(CONF_HOST): str, + } +) + +CONFIG_SETTINGS = vol.Schema( + { vol.Required(CONF_API_KEY): vol.All(str, vol.Length(min=16, max=16)), } ) @@ -26,39 +31,68 @@ class MotionBlindsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Motion Blinds config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Motion Blinds flow.""" - self.host = None - self.key = None + self._host = None + self._ips = [] async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} if user_input is not None: - self.host = user_input[CONF_HOST] - self.key = user_input[CONF_API_KEY] - return await self.async_step_connect() + self._host = user_input.get(CONF_HOST) + + if self._host is not None: + return await self.async_step_connect() + + # Use MotionGateway discovery + discover_class = MotionDiscovery() + gateways = await self.hass.async_add_executor_job(discover_class.discover) + self._ips = list(gateways) + + if len(self._ips) == 1: + self._host = self._ips[0] + return await self.async_step_connect() + + if len(self._ips) > 1: + return await self.async_step_select() + + errors["base"] = "discovery_error" return self.async_show_form( step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) + async def async_step_select(self, user_input=None): + """Handle multiple motion gateways found.""" + if user_input is not None: + self._host = user_input["select_ip"] + return await self.async_step_connect() + + select_schema = vol.Schema({vol.Required("select_ip"): vol.In(self._ips)}) + + return self.async_show_form(step_id="select", data_schema=select_schema) + async def async_step_connect(self, user_input=None): """Connect to the Motion Gateway.""" + if user_input is not None: + key = user_input[CONF_API_KEY] - connect_gateway_class = ConnectMotionGateway(self.hass) - if not await connect_gateway_class.async_connect_gateway(self.host, self.key): - return self.async_abort(reason="connection_error") - motion_gateway = connect_gateway_class.gateway_device + connect_gateway_class = ConnectMotionGateway(self.hass, multicast=None) + if not await connect_gateway_class.async_connect_gateway(self._host, key): + return self.async_abort(reason="connection_error") + motion_gateway = connect_gateway_class.gateway_device - mac_address = motion_gateway.mac + mac_address = motion_gateway.mac - await self.async_set_unique_id(mac_address) - self._abort_if_unique_id_configured() + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured() - return self.async_create_entry( - title=DEFAULT_GATEWAY_NAME, - data={CONF_HOST: self.host, CONF_API_KEY: self.key}, - ) + return self.async_create_entry( + title=DEFAULT_GATEWAY_NAME, + data={CONF_HOST: self._host, CONF_API_KEY: key}, + ) + + return self.async_show_form(step_id="connect", data_schema=CONFIG_SETTINGS) diff --git a/homeassistant/components/motion_blinds/const.py b/homeassistant/components/motion_blinds/const.py index c80c8f881c..27f2310c7c 100644 --- a/homeassistant/components/motion_blinds/const.py +++ b/homeassistant/components/motion_blinds/const.py @@ -1,6 +1,15 @@ """Constants for the Motion Blinds component.""" DOMAIN = "motion_blinds" -MANUFACTURER = "Motion, Coulisse B.V." +MANUFACTURER = "Motion Blinds, Coulisse B.V." +DEFAULT_GATEWAY_NAME = "Motion Blinds Gateway" + +MOTION_PLATFORMS = ["cover", "sensor"] KEY_GATEWAY = "gateway" KEY_COORDINATOR = "coordinator" +KEY_MULTICAST_LISTENER = "multicast_listener" + +ATTR_WIDTH = "width" +ATTR_ABSOLUTE_POSITION = "absolute_position" + +SERVICE_SET_ABSOLUTE_POSITION = "set_absolute_position" diff --git a/homeassistant/components/motion_blinds/cover.py b/homeassistant/components/motion_blinds/cover.py index 4273be3f43..3087401c3a 100644 --- a/homeassistant/components/motion_blinds/cover.py +++ b/homeassistant/components/motion_blinds/cover.py @@ -3,6 +3,7 @@ import logging from motionblinds import BlindType +import voluptuous as vol from homeassistant.components.cover import ( ATTR_POSITION, @@ -15,9 +16,18 @@ from homeassistant.components.cover import ( DEVICE_CLASS_SHUTTER, CoverEntity, ) +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, KEY_COORDINATOR, KEY_GATEWAY, MANUFACTURER +from .const import ( + ATTR_ABSOLUTE_POSITION, + ATTR_WIDTH, + DOMAIN, + KEY_COORDINATOR, + KEY_GATEWAY, + MANUFACTURER, + SERVICE_SET_ABSOLUTE_POSITION, +) _LOGGER = logging.getLogger(__name__) @@ -48,6 +58,12 @@ TDBU_DEVICE_MAP = { } +SET_ABSOLUTE_POSITION_SCHEMA = { + vol.Required(ATTR_ABSOLUTE_POSITION): vol.All(cv.positive_int, vol.Range(max=100)), + vol.Optional(ATTR_WIDTH): vol.All(cv.positive_int, vol.Range(max=100)), +} + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Motion Blind from a config entry.""" entities = [] @@ -84,12 +100,28 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "Bottom", ) ) + entities.append( + MotionTDBUDevice( + coordinator, + blind, + TDBU_DEVICE_MAP[blind.type], + config_entry, + "Combined", + ) + ) else: _LOGGER.warning("Blind type '%s' not yet supported", blind.blind_type) async_add_entities(entities) + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_SET_ABSOLUTE_POSITION, + SET_ABSOLUTE_POSITION_SCHEMA, + SERVICE_SET_ABSOLUTE_POSITION, + ) + class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Representation of a Motion Blind Device.""" @@ -125,6 +157,11 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Return the name of the blind.""" return f"{self._blind.blind_type}-{self._blind.mac[12:]}" + @property + def available(self): + """Return True if entity is available.""" + return self._blind.available + @property def current_cover_position(self): """ @@ -146,6 +183,16 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): """Return if the cover is closed or not.""" return self._blind.position == 100 + async def async_added_to_hass(self): + """Subscribe to multicast pushes and register signal handler.""" + self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._blind.Remove_callback(self.unique_id) + await super().async_will_remove_from_hass() + def open_cover(self, **kwargs): """Open the cover.""" self._blind.Open() @@ -159,6 +206,11 @@ class MotionPositionDevice(CoordinatorEntity, CoverEntity): position = kwargs[ATTR_POSITION] self._blind.Set_position(100 - position) + def set_absolute_position(self, **kwargs): + """Move the cover to a specific absolute position (see TDBU).""" + position = kwargs[ATTR_ABSOLUTE_POSITION] + self._blind.Set_position(100 - position) + def stop_cover(self, **kwargs): """Stop the cover.""" self._blind.Stop() @@ -205,7 +257,7 @@ class MotionTDBUDevice(MotionPositionDevice): self._motor = motor self._motor_key = motor[0] - if self._motor not in ["Bottom", "Top"]: + if self._motor not in ["Bottom", "Top", "Combined"]: _LOGGER.error("Unknown motor '%s'", self._motor) @property @@ -225,10 +277,10 @@ class MotionTDBUDevice(MotionPositionDevice): None is unknown, 0 is open, 100 is closed. """ - if self._blind.position is None: + if self._blind.scaled_position is None: return None - return 100 - self._blind.position[self._motor_key] + return 100 - self._blind.scaled_position[self._motor_key] @property def is_closed(self): @@ -236,8 +288,23 @@ class MotionTDBUDevice(MotionPositionDevice): if self._blind.position is None: return None + if self._motor == "Combined": + return self._blind.width == 100 + return self._blind.position[self._motor_key] == 100 + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attributes = {} + if self._blind.position is not None: + attributes[ATTR_ABSOLUTE_POSITION] = ( + 100 - self._blind.position[self._motor_key] + ) + if self._blind.width is not None: + attributes[ATTR_WIDTH] = self._blind.width + return attributes + def open_cover(self, **kwargs): """Open the cover.""" self._blind.Open(motor=self._motor_key) @@ -247,9 +314,18 @@ class MotionTDBUDevice(MotionPositionDevice): self._blind.Close(motor=self._motor_key) def set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" + """Move the cover to a specific scaled position.""" position = kwargs[ATTR_POSITION] - self._blind.Set_position(100 - position, motor=self._motor_key) + self._blind.Set_scaled_position(100 - position, motor=self._motor_key) + + def set_absolute_position(self, **kwargs): + """Move the cover to a specific absolute position.""" + position = kwargs[ATTR_ABSOLUTE_POSITION] + target_width = kwargs.get(ATTR_WIDTH, None) + + self._blind.Set_position( + 100 - position, motor=self._motor_key, width=target_width + ) def stop_cover(self, **kwargs): """Stop the cover.""" diff --git a/homeassistant/components/motion_blinds/gateway.py b/homeassistant/components/motion_blinds/gateway.py index e7e665d65f..14dd36ce5b 100644 --- a/homeassistant/components/motion_blinds/gateway.py +++ b/homeassistant/components/motion_blinds/gateway.py @@ -10,9 +10,10 @@ _LOGGER = logging.getLogger(__name__) class ConnectMotionGateway: """Class to async connect to a Motion Gateway.""" - def __init__(self, hass): + def __init__(self, hass, multicast): """Initialize the entity.""" self._hass = hass + self._multicast = multicast self._gateway_device = None @property @@ -24,11 +25,15 @@ class ConnectMotionGateway: """Update all information of the gateway.""" self.gateway_device.GetDeviceList() self.gateway_device.Update() + for blind in self.gateway_device.device_list.values(): + blind.Update_from_cache() async def async_connect_gateway(self, host, key): """Connect to the Motion Gateway.""" _LOGGER.debug("Initializing with host %s (key %s...)", host, key[:3]) - self._gateway_device = MotionGateway(ip=host, key=key) + self._gateway_device = MotionGateway( + ip=host, key=key, multicast=self._multicast + ) try: # update device info and get the connected sub devices await self._hass.async_add_executor_job(self.update_gateway) diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index 84cf711ac9..ce781266a6 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,6 +3,6 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.1.6"], + "requirements": ["motionblinds==0.4.7"], "codeowners": ["@starkillerOG"] -} \ No newline at end of file +} diff --git a/homeassistant/components/motion_blinds/sensor.py b/homeassistant/components/motion_blinds/sensor.py index 81d555806e..dd637696e7 100644 --- a/homeassistant/components/motion_blinds/sensor.py +++ b/homeassistant/components/motion_blinds/sensor.py @@ -71,6 +71,11 @@ class MotionBatterySensor(CoordinatorEntity, Entity): """Return the name of the blind battery sensor.""" return f"{self._blind.blind_type}-battery-{self._blind.mac[12:]}" + @property + def available(self): + """Return True if entity is available.""" + return self._blind.available + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -91,6 +96,16 @@ class MotionBatterySensor(CoordinatorEntity, Entity): """Return device specific state attributes.""" return {ATTR_BATTERY_VOLTAGE: self._blind.battery_voltage} + async def async_added_to_hass(self): + """Subscribe to multicast pushes.""" + self._blind.Register_callback(self.unique_id, self.schedule_update_ha_state) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._blind.Remove_callback(self.unique_id) + await super().async_will_remove_from_hass() + class MotionTDBUBatterySensor(MotionBatterySensor): """ @@ -160,6 +175,11 @@ class MotionSignalStrengthSensor(CoordinatorEntity, Entity): return "Motion gateway signal strength" return f"{self._device.blind_type} signal strength - {self._device.mac[12:]}" + @property + def available(self): + """Return True if entity is available.""" + return self._device.available + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" @@ -179,3 +199,13 @@ class MotionSignalStrengthSensor(CoordinatorEntity, Entity): def state(self): """Return the state of the sensor.""" return self._device.RSSI + + async def async_added_to_hass(self): + """Subscribe to multicast pushes.""" + self._device.Register_callback(self.unique_id, self.schedule_update_ha_state) + await super().async_added_to_hass() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._device.Remove_callback(self.unique_id) + await super().async_will_remove_from_hass() diff --git a/homeassistant/components/motion_blinds/services.yaml b/homeassistant/components/motion_blinds/services.yaml new file mode 100644 index 0000000000..f46cc94bd4 --- /dev/null +++ b/homeassistant/components/motion_blinds/services.yaml @@ -0,0 +1,14 @@ +# Describes the format for available motion blinds services + +set_absolute_position: + description: "Set the absolute position of the cover." + fields: + entity_id: + description: Name of the motion blind cover entity to control. + example: "cover.TopDownBottomUp-Bottom-0001" + absolute_position: + description: Absolute position to move to. + example: 70 + width: + description: Optionally specify the width that is covered, only for TDBU Combined entities. + example: 30 diff --git a/homeassistant/components/motion_blinds/strings.json b/homeassistant/components/motion_blinds/strings.json index d9c8a4099a..d922923d47 100644 --- a/homeassistant/components/motion_blinds/strings.json +++ b/homeassistant/components/motion_blinds/strings.json @@ -3,14 +3,30 @@ "flow_title": "Motion Blinds", "step": { "user": { + "title": "Motion Blinds", + "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", + "data": { + "host": "[%key:common::config_flow::data::ip%]" + } + }, + "connect": { "title": "Motion Blinds", "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", "data": { - "host": "[%key:common::config_flow::data::ip%]", "api_key": "[%key:common::config_flow::data::api_key%]" } + }, + "select": { + "title": "Select the Motion Gateway that you wish to connect", + "description": "Run the setup again if you want to connect additional Motion Gateways", + "data": { + "select_ip": "[%key:common::config_flow::data::ip%]" + } } }, + "error": { + "discovery_error": "Failed to discover a Motion Gateway" + }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", @@ -18,3 +34,6 @@ } } } + + + diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json new file mode 100644 index 0000000000..dd1acc230f --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "connection_error": "Verbindung fehlgeschlagen" + }, + "flow_title": "Jalousien", + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "host": "IP-Adresse" + }, + "description": "Ein 16-Zeichen-API-Schl\u00fcssel wird ben\u00f6tigt, siehe https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Jalousien" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json new file mode 100644 index 0000000000..fe188057e4 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "connection_error": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "host": "Endere\u00e7o IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/sl.json b/homeassistant/components/motion_blinds/translations/sl.json new file mode 100644 index 0000000000..bb61b03520 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/sl.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json new file mode 100644 index 0000000000..545a3547ff --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "connection_error": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "IP adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/zh-Hans.json b/homeassistant/components/motion_blinds/translations/zh-Hans.json new file mode 100644 index 0000000000..f8dac15948 --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "connection_error": "\u8fde\u63a5\u5931\u8d25" + }, + "step": { + "user": { + "data": { + "api_key": "API\u5bc6\u7801", + "host": "IP\u5730\u5740" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 8c8d23b565..37925ca628 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index de7b8b8f0d..5e9b4f8e69 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -2,6 +2,6 @@ "domain": "mpd", "name": "Music Player Daemon (MPD)", "documentation": "https://www.home-assistant.io/integrations/mpd", - "requirements": ["python-mpd2==1.0.0"], + "requirements": ["python-mpd2==3.0.1"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 69ab0a3421..1273b720dd 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -1,9 +1,11 @@ """Support to interact with a Music Player Daemon.""" from datetime import timedelta +import hashlib import logging import os import mpd +from mpd.asyncio import MPDClient import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -75,15 +77,15 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -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 the MPD platform.""" host = config.get(CONF_HOST) port = config.get(CONF_PORT) name = config.get(CONF_NAME) password = config.get(CONF_PASSWORD) - device = MpdDevice(host, port, password, name) - add_entities([device], True) + entity = MpdDevice(host, port, password, name) + async_add_entities([entity], True) class MpdDevice(MediaPlayerEntity): @@ -106,19 +108,20 @@ class MpdDevice(MediaPlayerEntity): self._muted_volume = 0 self._media_position_updated_at = None self._media_position = None + self._commands = None # set up MPD client - self._client = mpd.MPDClient() + self._client = MPDClient() self._client.timeout = 30 self._client.idletimeout = None - def _connect(self): + async def _connect(self): """Connect to MPD.""" try: - self._client.connect(self.server, self.port) + await self._client.connect(self.server, self.port) if self.password is not None: - self._client.password(self.password) + await self._client.password(self.password) except mpd.ConnectionError: return @@ -133,10 +136,10 @@ class MpdDevice(MediaPlayerEntity): self._is_connected = False self._status = None - def _fetch_status(self): + async def _fetch_status(self): """Fetch status from MPD.""" - self._status = self._client.status() - self._currentsong = self._client.currentsong() + self._status = await self._client.status() + self._currentsong = await self._client.currentsong() position = self._status.get("elapsed") @@ -150,20 +153,21 @@ class MpdDevice(MediaPlayerEntity): self._media_position_updated_at = dt_util.utcnow() self._media_position = int(float(position)) - self._update_playlists() + await self._update_playlists() @property def available(self): """Return true if MPD is available and connected.""" return self._is_connected - def update(self): + async def async_update(self): """Get the latest data and update the state.""" try: if not self._is_connected: - self._connect() + await self._connect() + self._commands = list(await self._client.commands()) - self._fetch_status() + await self._fetch_status() except (mpd.ConnectionError, OSError, BrokenPipeError, ValueError) as error: # Cleanly disconnect in case connection is not in valid state _LOGGER.debug("Error updating status: %s", error) @@ -251,6 +255,56 @@ class MpdDevice(MediaPlayerEntity): """Return the album of current playing media (Music track only).""" return self._currentsong.get("album") + @property + def media_image_hash(self): + """Hash value for media image.""" + file = self._currentsong.get("file") + if file: + return hashlib.sha256(file.encode("utf-8")).hexdigest()[:16] + + return None + + async def async_get_media_image(self): + """Fetch media image of current playing track.""" + file = self._currentsong.get("file") + if not file: + return None, None + + # not all MPD implementations and versions support the `albumart` and `fetchpicture` commands + can_albumart = "albumart" in self._commands + can_readpicture = "readpicture" in self._commands + + response = None + + # read artwork embedded into the media file + if can_readpicture: + try: + response = await self._client.readpicture(file) + except mpd.CommandError as error: + _LOGGER.warning( + "Retrieving artwork through `readpicture` command failed: %s", + error, + ) + + # read artwork contained in the media directory (cover.{jpg,png,tiff,bmp}) if none is embedded + if can_albumart and not response: + try: + response = await self._client.albumart(file) + except mpd.CommandError as error: + _LOGGER.warning( + "Retrieving artwork through `albumart` command failed: %s", + error, + ) + + if not response: + return None, None + + image = bytes(response.get("binary")) + mime = response.get( + "type", "image/png" + ) # readpicture has type, albumart does not + return (image, mime) + @property def volume_level(self): """Return the volume level.""" @@ -282,27 +336,27 @@ class MpdDevice(MediaPlayerEntity): """Return the list of available input sources.""" return self._playlists - def select_source(self, source): + async def async_select_source(self, source): """Choose a different available playlist and play it.""" - self.play_media(MEDIA_TYPE_PLAYLIST, source) + await self.async_play_media(MEDIA_TYPE_PLAYLIST, source) @Throttle(PLAYLIST_UPDATE_INTERVAL) - def _update_playlists(self, **kwargs): + async def _update_playlists(self, **kwargs): """Update available MPD playlists.""" try: self._playlists = [] - for playlist_data in self._client.listplaylists(): + for playlist_data in await self._client.listplaylists(): self._playlists.append(playlist_data["playlist"]) except mpd.CommandError as error: self._playlists = None _LOGGER.warning("Playlists could not be updated: %s:", error) - def set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set volume of media player.""" if "volume" in self._status: - self._client.setvol(int(volume * 100)) + await self._client.setvol(int(volume * 100)) - def volume_up(self): + async def async_volume_up(self): """Service to send the MPD the command for volume up.""" if "volume" in self._status: current_volume = int(self._status["volume"]) @@ -310,48 +364,48 @@ class MpdDevice(MediaPlayerEntity): if current_volume <= 100: self._client.setvol(current_volume + 5) - def volume_down(self): + async def async_volume_down(self): """Service to send the MPD the command for volume down.""" if "volume" in self._status: current_volume = int(self._status["volume"]) if current_volume >= 0: - self._client.setvol(current_volume - 5) + await self._client.setvol(current_volume - 5) - def media_play(self): + async def async_media_play(self): """Service to send the MPD the command for play/pause.""" if self._status["state"] == "pause": - self._client.pause(0) + await self._client.pause(0) else: - self._client.play() + await self._client.play() - def media_pause(self): + async def async_media_pause(self): """Service to send the MPD the command for play/pause.""" - self._client.pause(1) + await self._client.pause(1) - def media_stop(self): + async def async_media_stop(self): """Service to send the MPD the command for stop.""" - self._client.stop() + await self._client.stop() - def media_next_track(self): + async def async_media_next_track(self): """Service to send the MPD the command for next track.""" - self._client.next() + await self._client.next() - def media_previous_track(self): + async def async_media_previous_track(self): """Service to send the MPD the command for previous track.""" - self._client.previous() + await self._client.previous() - def mute_volume(self, mute): + async def async_mute_volume(self, mute): """Mute. Emulated with set_volume_level.""" if "volume" in self._status: if mute: self._muted_volume = self.volume_level - self.set_volume_level(0) + await self.async_set_volume_level(0) else: - self.set_volume_level(self._muted_volume) + await self.async_set_volume_level(self._muted_volume) self._muted = mute - def play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """Send the media player the command for playing a playlist.""" _LOGGER.debug("Playing playlist: %s", media_id) if media_type == MEDIA_TYPE_PLAYLIST: @@ -360,13 +414,14 @@ class MpdDevice(MediaPlayerEntity): else: self._currentplaylist = None _LOGGER.warning("Unknown playlist name %s", media_id) - self._client.clear() - self._client.load(media_id) - self._client.play() + await self._client.clear() + await self._client.load(media_id) + await self._client.play() else: - self._client.clear() - self._client.add(media_id) - self._client.play() + await self._client.clear() + self._currentplaylist = None + await self._client.add(media_id) + await self._client.play() @property def repeat(self): @@ -377,40 +432,40 @@ class MpdDevice(MediaPlayerEntity): return REPEAT_MODE_ALL return REPEAT_MODE_OFF - def set_repeat(self, repeat): + async def async_set_repeat(self, repeat): """Set repeat mode.""" if repeat == REPEAT_MODE_OFF: - self._client.repeat(0) - self._client.single(0) + await self._client.repeat(0) + await self._client.single(0) else: - self._client.repeat(1) + await self._client.repeat(1) if repeat == REPEAT_MODE_ONE: - self._client.single(1) + await self._client.single(1) else: - self._client.single(0) + await self._client.single(0) @property def shuffle(self): """Boolean if shuffle is enabled.""" return bool(int(self._status["random"])) - def set_shuffle(self, shuffle): + async def async_set_shuffle(self, shuffle): """Enable/disable shuffle mode.""" - self._client.random(int(shuffle)) + await self._client.random(int(shuffle)) - def turn_off(self): + async def async_turn_off(self): """Service to send the MPD the command to stop playing.""" - self._client.stop() + await self._client.stop() - def turn_on(self): + async def async_turn_on(self): """Service to send the MPD the command to start playing.""" - self._client.play() - self._update_playlists(no_throttle=True) + await self._client.play() + await self._update_playlists(no_throttle=True) - def clear_playlist(self): + async def async_clear_playlist(self): """Clear players playlist.""" - self._client.clear() + await self._client.clear() - def media_seek(self, position): + async def async_media_seek(self, position): """Send seek command.""" - self._client.seekcur(position) + await self._client.seekcur(position) diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index 35d0e1fb42..edf383a681 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -4,7 +4,6 @@ import re import voluptuous as vol -from homeassistant.components import mqtt import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -48,6 +47,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 339b41a9dd..e081423d59 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components import binary_sensor, mqtt +from homeassistant.components import binary_sensor from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, BinarySensorEntity, @@ -40,6 +40,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 82e5cb8b27..e8783f74bd 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import camera, mqtt +from homeassistant.components import camera from homeassistant.components.camera import Camera from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback @@ -23,6 +23,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 8b762a82f0..c5835f8e7c 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import climate, mqtt +from homeassistant.components import climate from homeassistant.components.climate import ( PLATFORM_SCHEMA as CLIMATE_PLATFORM_SCHEMA, ClimateEntity, @@ -64,6 +64,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index c3a7813324..25fcf0ad0d 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import cover, mqtt +from homeassistant.components import cover from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -51,6 +51,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index 4fcfd8f66f..c064cca599 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ATTR_DISCOVERY_HASH, device_trigger +from .. import mqtt from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/device_tracker/__init__.py b/homeassistant/components/mqtt/device_tracker/__init__.py new file mode 100644 index 0000000000..03574e6554 --- /dev/null +++ b/homeassistant/components/mqtt/device_tracker/__init__.py @@ -0,0 +1,7 @@ +"""Support for tracking MQTT enabled devices.""" +from .schema_discovery import async_setup_entry_from_discovery +from .schema_yaml import PLATFORM_SCHEMA_YAML, async_setup_scanner_from_yaml + +PLATFORM_SCHEMA = PLATFORM_SCHEMA_YAML +async_setup_scanner = async_setup_scanner_from_yaml +async_setup_entry = async_setup_entry_from_discovery diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py new file mode 100644 index 0000000000..4de2ae4fa6 --- /dev/null +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -0,0 +1,230 @@ +"""Support for tracking MQTT enabled devices identified through discovery.""" +import logging + +import voluptuous as vol + +from homeassistant.components import device_tracker +from homeassistant.components.device_tracker import SOURCE_TYPES +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.const import ( + ATTR_GPS_ACCURACY, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_DEVICE, + CONF_ICON, + CONF_NAME, + CONF_UNIQUE_ID, + CONF_VALUE_TEMPLATE, + STATE_HOME, + STATE_NOT_HOME, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .. import ( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt +from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC +from ..debug_info import log_messages +from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash + +_LOGGER = logging.getLogger(__name__) + +CONF_PAYLOAD_HOME = "payload_home" +CONF_PAYLOAD_NOT_HOME = "payload_not_home" +CONF_SOURCE_TYPE = "source_type" + +PLATFORM_SCHEMA_DISCOVERY = ( + mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, + vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) + .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) +) + + +async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): + """Set up MQTT device tracker dynamically through MQTT discovery.""" + + async def async_discover(discovery_payload): + """Discover and add an MQTT device tracker.""" + discovery_data = discovery_payload.discovery_data + try: + config = PLATFORM_SCHEMA_DISCOVERY(discovery_payload) + await _async_setup_entity( + hass, config, async_add_entities, config_entry, discovery_data + ) + except Exception: + clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) + raise + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(device_tracker.DOMAIN, "mqtt"), async_discover + ) + + +async def _async_setup_entity( + hass, config, async_add_entities, config_entry=None, discovery_data=None +): + """Set up the MQTT Device Tracker entity.""" + async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) + + +class MqttDeviceTracker( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + TrackerEntity, +): + """Representation of a device tracker using MQTT.""" + + def __init__(self, hass, config, config_entry, discovery_data): + """Initialize the tracker.""" + self.hass = hass + self._location_name = None + self._sub_state = None + self._unique_id = config.get(CONF_UNIQUE_ID) + + # Load config + self._setup_from_config(config) + + device_config = config.get(CONF_DEVICE) + + MqttAttributes.__init__(self, config) + MqttAvailability.__init__(self, config) + MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + + async def async_added_to_hass(self): + """Subscribe to MQTT events.""" + await super().async_added_to_hass() + await self._subscribe_topics() + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = PLATFORM_SCHEMA_DISCOVERY(discovery_payload) + self._setup_from_config(config) + await self.attributes_discovery_update(config) + await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) + await self._subscribe_topics() + self.async_write_ha_state() + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + self._config = config + + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = self.hass + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def message_received(msg): + """Handle new MQTT messages.""" + payload = msg.payload + value_template = self._config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + payload = value_template.async_render_with_possible_json_value(payload) + if payload == self._config[CONF_PAYLOAD_HOME]: + self._location_name = STATE_HOME + elif payload == self._config[CONF_PAYLOAD_NOT_HOME]: + self._location_name = STATE_NOT_HOME + else: + self._location_name = msg.payload + + self.async_write_ha_state() + + self._sub_state = await subscription.async_subscribe_topics( + self.hass, + self._sub_state, + { + "state_topic": { + "topic": self._config[CONF_STATE_TOPIC], + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + } + }, + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._sub_state = await subscription.async_unsubscribe_topics( + self.hass, self._sub_state + ) + await MqttAttributes.async_will_remove_from_hass(self) + await MqttAvailability.async_will_remove_from_hass(self) + await MqttDiscoveryUpdate.async_will_remove_from_hass(self) + + @property + def icon(self): + """Return the icon of the device.""" + return self._config.get(CONF_ICON) + + @property + def latitude(self): + """Return latitude if provided in device_state_attributes or None.""" + if ( + self.device_state_attributes is not None + and ATTR_LATITUDE in self.device_state_attributes + ): + return self.device_state_attributes[ATTR_LATITUDE] + return None + + @property + def location_accuracy(self): + """Return location accuracy if provided in device_state_attributes or None.""" + if ( + self.device_state_attributes is not None + and ATTR_GPS_ACCURACY in self.device_state_attributes + ): + return self.device_state_attributes[ATTR_GPS_ACCURACY] + return None + + @property + def longitude(self): + """Return longitude if provided in device_state_attributes or None.""" + if ( + self.device_state_attributes is not None + and ATTR_LONGITUDE in self.device_state_attributes + ): + return self.device_state_attributes[ATTR_LONGITUDE] + return None + + @property + def location_name(self): + """Return a location name for the current location of the device.""" + return self._location_name + + @property + def name(self): + """Return the name of the device tracker.""" + return self._config.get(CONF_NAME) + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return self._config.get(CONF_SOURCE_TYPE) diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker/schema_yaml.py similarity index 84% rename from homeassistant/components/mqtt/device_tracker.py rename to homeassistant/components/mqtt/device_tracker/schema_yaml.py index bcc969f035..f871ac89c2 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker/schema_yaml.py @@ -1,23 +1,20 @@ -"""Support for tracking MQTT enabled devices.""" -import logging +"""Support for tracking MQTT enabled devices defined in YAML.""" import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from . import CONF_QOS - -_LOGGER = logging.getLogger(__name__) +from ... import mqtt +from ..const import CONF_QOS CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" CONF_SOURCE_TYPE = "source_type" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( +PLATFORM_SCHEMA_YAML = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( { vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, @@ -27,7 +24,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( ) -async def async_setup_scanner(hass, config, async_see, discovery_info=None): +async def async_setup_scanner_from_yaml(hass, config, async_see, discovery_info=None): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 676252c313..9fa51bebf0 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -5,7 +5,6 @@ from typing import Callable, List, Optional import attr import voluptuous as vol -from homeassistant.components import mqtt 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 @@ -28,6 +27,7 @@ from . import ( debug_info, trigger as mqtt_trigger, ) +from .. import mqtt from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d1e64d44bb..5452d15aa3 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -6,12 +6,12 @@ import logging import re import time -from homeassistant.components import mqtt from homeassistant.const import CONF_DEVICE, CONF_PLATFORM from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_mqtt +from .. import mqtt from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS from .const import ( ATTR_DISCOVERY_HASH, @@ -34,6 +34,7 @@ SUPPORTED_COMPONENTS = [ "climate", "cover", "device_automation", + "device_tracker", "fan", "light", "lock", diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 14469e415e..96d5fe720c 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import fan, mqtt +from homeassistant.components import fan from homeassistant.components.fan import ( ATTR_SPEED, SPEED_HIGH, @@ -43,6 +43,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 2375fb86e5..393cb2fcf1 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -4,16 +4,12 @@ import logging import voluptuous as vol from homeassistant.components import light -from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH -from homeassistant.components.mqtt.discovery import ( - MQTT_DISCOVERY_NEW, - clear_discovery_hash, -) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .. import DOMAIN, PLATFORMS +from .. import ATTR_DISCOVERY_HASH, DOMAIN, PLATFORMS +from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 4796652f57..00ad267139 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -17,17 +16,6 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -43,6 +31,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util +from .. import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index bba5605348..bb10fd52ae 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -4,7 +4,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -24,17 +23,6 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.const import ( CONF_BRIGHTNESS, CONF_COLOR_TEMP, @@ -54,6 +42,18 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util +from .. import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index faf987881b..e6b22da5af 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -21,17 +20,6 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, LightEntity, ) -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.const import ( CONF_DEVICE, CONF_NAME, @@ -45,6 +33,18 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util +from .. import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index aea1e40b0f..712f2e0e37 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import lock, mqtt +from homeassistant.components import lock from homeassistant.components.lock import LockEntity from homeassistant.const import ( CONF_DEVICE, @@ -32,6 +32,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 4f4380332f..673eb169b1 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt, scene +from homeassistant.components import scene from homeassistant.components.scene import Scene from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID import homeassistant.helpers.config_validation as cv @@ -21,6 +21,7 @@ from . import ( MqttAvailability, MqttDiscoveryUpdate, ) +from .. import mqtt from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index ffd34cef8c..1fda8986ef 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -5,7 +5,7 @@ from typing import Optional import voluptuous as vol -from homeassistant.components import mqtt, sensor +from homeassistant.components import sensor from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( CONF_DEVICE, @@ -38,6 +38,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index 24c1c6ff3a..c61c30c922 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -4,11 +4,11 @@ from typing import Any, Callable, Dict, Optional import attr -from homeassistant.components import mqtt from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from . import debug_info +from .. import mqtt from .const import DEFAULT_QOS from .models import MessageCallbackType diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 761f19ef05..7601968011 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt, switch +from homeassistant.components import switch from homeassistant.components.switch import SwitchEntity from homeassistant.const import ( CONF_DEVICE, @@ -37,6 +37,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .. import mqtt from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash @@ -73,7 +74,7 @@ async def async_setup_platform( ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities, discovery_info) + await _async_setup_entity(hass, config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 94356ccf77..75f3bb5030 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -3,7 +3,6 @@ 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 @@ -21,6 +20,7 @@ from . import ( cleanup_device_registry, subscription, ) +from .. import mqtt from .discovery import MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash from .util import valid_subscribe_topic diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index 2f0e67a977..325e8dde09 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -63,7 +63,11 @@ "options": { "data": { "birth_enable": "Povolit zpr\u00e1vu p\u0159i p\u0159ipojen\u00ed", - "discovery": "Povolit zji\u0161\u0165ov\u00e1n\u00ed" + "discovery": "Povolit zji\u0161\u0165ov\u00e1n\u00ed", + "will_payload": "Obsah zpr\u00e1vy se z\u00e1v\u011bt\u00ed", + "will_qos": "QoS zpr\u00e1vy se z\u00e1v\u011bt\u00ed", + "will_retain": "Zachov\u00e1n\u00ed zpr\u00e1vy se z\u00e1v\u011bt\u00ed", + "will_topic": "T\u00e9ma zpr\u00e1vy se z\u00e1v\u011bt\u00ed (will message)" }, "description": "Zvolte mo\u017enosti MQTT." } diff --git a/homeassistant/components/mqtt/translations/et.json b/homeassistant/components/mqtt/translations/et.json index 586d96a78a..53d6d391e8 100644 --- a/homeassistant/components/mqtt/translations/et.json +++ b/homeassistant/components/mqtt/translations/et.json @@ -15,7 +15,7 @@ "port": "Port", "username": "Kasutajanimi" }, - "description": "Sisestage oma MQTT vahendaja andmed." + "description": "Sisesta oma MQTT vahendaja andmed." }, "hassio_confirm": { "data": { @@ -62,7 +62,7 @@ "port": "Port", "username": "Kasutajanimi" }, - "description": "Sisestage oma MQTT vahendaja \u00fchenduse teave." + "description": "Sisesta oma MQTT vahendaja \u00fchenduse teave." }, "options": { "data": { diff --git a/homeassistant/components/mqtt/translations/zh-Hans.json b/homeassistant/components/mqtt/translations/zh-Hans.json index e508f2cb29..63ceded565 100644 --- a/homeassistant/components/mqtt/translations/zh-Hans.json +++ b/homeassistant/components/mqtt/translations/zh-Hans.json @@ -39,12 +39,12 @@ }, "trigger_type": { "button_double_press": "\"{subtype}\" \u53cc\u51fb", - "button_long_press": "\"{subtype}\" \u6301\u7eed\u6309\u4e0b", - "button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u91ca\u653e", + "button_long_press": "\"{subtype}\" \u957f\u6309", + "button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00", "button_quadruple_press": "\"{subtype}\" \u56db\u8fde\u51fb", "button_quintuple_press": "\"{subtype}\" \u4e94\u8fde\u51fb", "button_short_press": "\"{subtype}\" \u6309\u4e0b", - "button_short_release": "\"{subtype}\" \u91ca\u653e", + "button_short_release": "\"{subtype}\" \u677e\u5f00", "button_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb" } }, diff --git a/homeassistant/components/mqtt/translations/zh-Hant.json b/homeassistant/components/mqtt/translations/zh-Hant.json index de92aee317..bfb2736188 100644 --- a/homeassistant/components/mqtt/translations/zh-Hant.json +++ b/homeassistant/components/mqtt/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/mqtt/trigger.py b/homeassistant/components/mqtt/trigger.py index 58ec51c4b1..1c96b3de26 100644 --- a/homeassistant/components/mqtt/trigger.py +++ b/homeassistant/components/mqtt/trigger.py @@ -3,11 +3,12 @@ import json import voluptuous as vol -from homeassistant.components import mqtt from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM from homeassistant.core import HassJob, callback import homeassistant.helpers.config_validation as cv +from .. import mqtt + # mypy: allow-untyped-defs CONF_ENCODING = "encoding" diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index b954a97e8f..f6265d1b96 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -3,16 +3,12 @@ import logging import voluptuous as vol -from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH -from homeassistant.components.mqtt.discovery import ( - MQTT_DISCOVERY_NEW, - clear_discovery_hash, -) from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service -from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS +from .. import ATTR_DISCOVERY_HASH, DOMAIN as MQTT_DOMAIN, PLATFORMS +from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state @@ -34,7 +30,7 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up MQTT vacuum through configuration.yaml.""" await async_setup_reload_service(hass, MQTT_DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities, discovery_info) + await _async_setup_entity(config, async_add_entities) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -58,7 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def _async_setup_entity( - config, async_add_entities, config_entry, discovery_data=None + config, async_add_entities, config_entry=None, discovery_data=None ): """Set up the MQTT vacuum.""" setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 907e2e4a08..65acc9afc7 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -4,14 +4,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt -from homeassistant.components.mqtt import ( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, @@ -36,6 +28,14 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level +from .. import ( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 9f75f38f1b..5a8666e5a2 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -4,18 +4,6 @@ import logging import voluptuous as vol -from homeassistant.components import mqtt -from homeassistant.components.mqtt import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) from homeassistant.components.vacuum import ( STATE_CLEANING, STATE_DOCKED, @@ -44,6 +32,18 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from .. import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) +from ... import mqtt from ..debug_info import log_messages from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services diff --git a/homeassistant/components/myq/translations/pt.json b/homeassistant/components/myq/translations/pt.json index 4a071063d4..14f1703524 100644 --- a/homeassistant/components/myq/translations/pt.json +++ b/homeassistant/components/myq/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 9775dc592f..1d9d3de4f8 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -3,26 +3,30 @@ import asyncio from datetime import timedelta import logging -from pybotvac import Account, Neato, Vorwerk -from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException +from pybotvac import Account, Neato +from pybotvac.exceptions import NeatoException import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_SOURCE, + CONF_TOKEN, +) from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle -from .config_flow import NeatoConfigFlow +from . import api, config_flow from .const import ( - CONF_VENDOR, NEATO_CONFIG, NEATO_DOMAIN, NEATO_LOGIN, NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, - VALID_VENDORS, ) _LOGGER = logging.getLogger(__name__) @@ -32,82 +36,74 @@ CONFIG_SCHEMA = vol.Schema( { NEATO_DOMAIN: vol.Schema( { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, } ) }, extra=vol.ALLOW_EXTRA, ) +PLATFORMS = ["camera", "vacuum", "switch", "sensor"] -async def async_setup(hass, config): + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the Neato component.""" + hass.data[NEATO_DOMAIN] = {} if NEATO_DOMAIN not in config: - # There is an entry and nothing in configuration.yaml return True - entries = hass.config_entries.async_entries(NEATO_DOMAIN) hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN] - - if entries: - # There is an entry and something in the configuration.yaml - entry = entries[0] - conf = config[NEATO_DOMAIN] - if ( - entry.data[CONF_USERNAME] == conf[CONF_USERNAME] - and entry.data[CONF_PASSWORD] == conf[CONF_PASSWORD] - and entry.data[CONF_VENDOR] == conf[CONF_VENDOR] - ): - # The entry is not outdated - return True - - # The entry is outdated - error = await hass.async_add_executor_job( - NeatoConfigFlow.try_login, - conf[CONF_USERNAME], - conf[CONF_PASSWORD], - conf[CONF_VENDOR], - ) - if error is not None: - _LOGGER.error(error) - return False - - # Update the entry - hass.config_entries.async_update_entry(entry, data=config[NEATO_DOMAIN]) - else: - # Create the new entry - hass.async_create_task( - hass.config_entries.flow.async_init( - NEATO_DOMAIN, - context={"source": SOURCE_IMPORT}, - data=config[NEATO_DOMAIN], - ) - ) + vendor = Neato() + config_flow.OAuth2FlowHandler.async_register_implementation( + hass, + api.NeatoImplementation( + hass, + NEATO_DOMAIN, + config[NEATO_DOMAIN][CONF_CLIENT_ID], + config[NEATO_DOMAIN][CONF_CLIENT_SECRET], + vendor.auth_endpoint, + vendor.token_endpoint, + ), + ) return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up config entry.""" - hub = NeatoHub(hass, entry.data, Account) - - await hass.async_add_executor_job(hub.login) - if not hub.logged_in: - _LOGGER.debug("Failed to login to Neato API") + if CONF_TOKEN not in entry.data: + # Init reauth flow + hass.async_create_task( + hass.config_entries.flow.async_init( + NEATO_DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + ) + ) return False + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) + + neato_session = api.ConfigEntryAuth(hass, entry, session) + hass.data[NEATO_DOMAIN][entry.entry_id] = neato_session + hub = NeatoHub(hass, Account(neato_session)) + try: await hass.async_add_executor_job(hub.update_robots) - except NeatoRobotException as ex: + except NeatoException as ex: _LOGGER.debug("Failed to connect to Neato API") raise ConfigEntryNotReady from ex hass.data[NEATO_LOGIN] = hub - for component in ("camera", "vacuum", "switch", "sensor"): + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -115,53 +111,27 @@ async def async_setup_entry(hass, entry): return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: """Unload config entry.""" - hass.data.pop(NEATO_LOGIN) - await asyncio.gather( - hass.config_entries.async_forward_entry_unload(entry, "camera"), - hass.config_entries.async_forward_entry_unload(entry, "vacuum"), - hass.config_entries.async_forward_entry_unload(entry, "switch"), - hass.config_entries.async_forward_entry_unload(entry, "sensor"), + unload_functions = ( + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS ) - return True + + unload_ok = all(await asyncio.gather(*unload_functions)) + if unload_ok: + hass.data[NEATO_DOMAIN].pop(entry.entry_id) + + return unload_ok class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass, domain_config, neato): + def __init__(self, hass: HomeAssistantType, neato: Account): """Initialize the Neato hub.""" - self.config = domain_config - self._neato = neato - self._hass = hass - - if self.config[CONF_VENDOR] == "vorwerk": - self._vendor = Vorwerk() - else: # Neato - self._vendor = Neato() - - self.my_neato = None - self.logged_in = False - - def login(self): - """Login to My Neato.""" - _LOGGER.debug("Trying to connect to Neato API") - try: - self.my_neato = self._neato( - self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor - ) - except NeatoException as ex: - if isinstance(ex, NeatoLoginException): - _LOGGER.error("Invalid credentials") - else: - _LOGGER.error("Unable to connect to Neato API") - raise ConfigEntryNotReady from ex - self.logged_in = False - return - - self.logged_in = True - _LOGGER.debug("Successfully connected to Neato API") + self._hass: HomeAssistantType = hass + self.my_neato: Account = neato @Throttle(timedelta(minutes=1)) def update_robots(self): diff --git a/homeassistant/components/neato/api.py b/homeassistant/components/neato/api.py new file mode 100644 index 0000000000..931d7cdb71 --- /dev/null +++ b/homeassistant/components/neato/api.py @@ -0,0 +1,55 @@ +"""API for Neato Botvac bound to Home Assistant OAuth.""" +from asyncio import run_coroutine_threadsafe +import logging + +import pybotvac + +from homeassistant import config_entries, core +from homeassistant.helpers import config_entry_oauth2_flow + +_LOGGER = logging.getLogger(__name__) + + +class ConfigEntryAuth(pybotvac.OAuthSession): + """Provide Neato Botvac authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize Neato Botvac Auth.""" + self.hass = hass + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(self.session.token, vendor=pybotvac.Neato()) + + def refresh_tokens(self) -> str: + """Refresh and return new Neato Botvac tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token["access_token"] + + +class NeatoImplementation(config_entry_oauth2_flow.LocalOAuth2Implementation): + """Neato implementation of LocalOAuth2Implementation. + + We need this class because we have to add client_secret and scope to the authorization request. + """ + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"client_secret": self.client_secret} + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize. + + We must make sure that the plus signs are not encoded. + """ + url = await super().async_generate_authorize_url(flow_id) + return f"{url}&scope=public_profile+control_robots+maps" diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 4d7c4129d8..1698a1d944 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -45,7 +45,7 @@ class NeatoCleaningMap(Camera): self.robot = robot self.neato = neato self._mapdata = mapdata - self._available = self.neato.logged_in if self.neato is not None else False + self._available = neato is not None self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial self._generated_at = None diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index f74364bc8b..449de72b15 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -1,112 +1,65 @@ -"""Config flow to configure Neato integration.""" - +"""Config flow for Neato Botvac.""" import logging +from typing import Optional -from pybotvac import Account, Neato, Vorwerk -from pybotvac.exceptions import NeatoLoginException, NeatoRobotException import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_TOKEN +from homeassistant.helpers import config_entry_oauth2_flow # pylint: disable=unused-import -from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS - -DOCS_URL = "https://www.home-assistant.io/integrations/neato" -DEFAULT_VENDOR = "neato" +from .const import NEATO_DOMAIN _LOGGER = logging.getLogger(__name__) -class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN): - """Neato integration config flow.""" +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=NEATO_DOMAIN +): + """Config flow to handle Neato Botvac OAuth2 authentication.""" - VERSION = 1 + DOMAIN = NEATO_DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Initialize flow.""" - self._username = vol.UNDEFINED - self._password = vol.UNDEFINED - self._vendor = vol.UNDEFINED + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) - async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - errors = {} - - if self._async_current_entries(): + async def async_step_user(self, user_input: Optional[dict] = None) -> dict: + """Create an entry for the flow.""" + current_entries = self._async_current_entries() + if current_entries and CONF_TOKEN in current_entries[0].data: + # Already configured return self.async_abort(reason="already_configured") - if user_input is not None: - self._username = user_input["username"] - self._password = user_input["password"] - self._vendor = user_input["vendor"] + return await super().async_step_user(user_input=user_input) - error = await self.hass.async_add_executor_job( - self.try_login, self._username, self._password, self._vendor + async def async_step_reauth(self, data) -> dict: + """Perform reauth upon migration of old entries.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: Optional[dict] = None + ) -> dict: + """Confirm reauth upon migration of old entries.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=vol.Schema({}) ) - if error: - errors["base"] = error - else: - return self.async_create_entry( - title=user_input[CONF_USERNAME], - data=user_input, - description_placeholders={"docs_url": DOCS_URL}, - ) + return await self.async_step_user() - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), - } - ), - description_placeholders={"docs_url": DOCS_URL}, - errors=errors, - ) - - async def async_step_import(self, user_input): - """Import a config flow from configuration.""" - - if self._async_current_entries(): - return self.async_abort(reason="already_configured") - - username = user_input[CONF_USERNAME] - password = user_input[CONF_PASSWORD] - vendor = user_input[CONF_VENDOR] - - error = await self.hass.async_add_executor_job( - self.try_login, username, password, vendor - ) - if error is not None: - _LOGGER.error(error) - return self.async_abort(reason=error) - - return self.async_create_entry( - title=f"{username} (from configuration)", - data={ - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_VENDOR: vendor, - }, - ) - - @staticmethod - def try_login(username, password, vendor): - """Try logging in to device and return any errors.""" - this_vendor = None - if vendor == "vorwerk": - this_vendor = Vorwerk() - else: # Neato - this_vendor = Neato() - - try: - Account(username, password, this_vendor) - except NeatoLoginException: - return "invalid_auth" - except NeatoRobotException: - return "unknown" - - return None + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for the flow. Update an entry if one already exist.""" + current_entries = self._async_current_entries() + if current_entries and CONF_TOKEN not in current_entries[0].data: + # Update entry + self.hass.config_entries.async_update_entry( + current_entries[0], title=self.flow_impl.name, data=data + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(current_entries[0].entry_id) + ) + return self.async_abort(reason="reauth_successful") + return self.async_create_entry(title=self.flow_impl.name, data=data) diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 53948e2b19..248e455b6d 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -11,8 +11,6 @@ NEATO_ROBOTS = "neato_robots" SCAN_INTERVAL_MINUTES = 1 -VALID_VENDORS = ["neato", "vorwerk"] - MODE = {1: "Eco", 2: "Turbo"} ACTION = { diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index d36e3fa503..d3ea8a8525 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -3,6 +3,14 @@ "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": ["pybotvac==0.0.17"], - "codeowners": ["@dshokouhi", "@Santobert"] -} + "requirements": [ + "pybotvac==0.0.19" + ], + "codeowners": [ + "@dshokouhi", + "@Santobert" + ], + "dependencies": [ + "http" + ] +} \ No newline at end of file diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index efcbfb8d54..b083ec1d7d 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -37,7 +37,7 @@ class NeatoSensor(Entity): def __init__(self, neato, robot): """Initialize Neato sensor.""" self.robot = robot - self._available = neato.logged_in if neato is not None else False + self._available = neato is not None self._robot_name = f"{self.robot.name} {BATTERY}" self._robot_serial = self.robot.serial self._state = None diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index 5d71d4889a..21af0f91d1 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -1,26 +1,23 @@ { "config": { "step": { - "user": { - "title": "Neato Account Info", - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "vendor": "Vendor" - }, - "description": "See [Neato documentation]({docs_url})." + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::description::confirm_setup%]" } }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" + "abort": { + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { - "default": "See [Neato documentation]({docs_url})." - }, - "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + "default": "[%key:common::config_flow::create_entry::authenticated%]" } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index a6aa19abe2..204adb108a 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -40,7 +40,7 @@ class NeatoConnectedSwitch(ToggleEntity): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self._available = neato.logged_in if neato is not None else False + self._available = neato is not None self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None diff --git a/homeassistant/components/neato/translations/ca.json b/homeassistant/components/neato/translations/ca.json index 601af3a68e..f818135c51 100644 --- a/homeassistant/components/neato/translations/ca.json +++ b/homeassistant/components/neato/translations/ca.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "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})", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "create_entry": { - "default": "Consulta la [documentaci\u00f3 de Neato]({docs_url})." + "default": "Autenticaci\u00f3 exitosa" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "title": "Vols comen\u00e7ar la configuraci\u00f3?" + }, "user": { "data": { "password": "Contrasenya", @@ -22,5 +32,6 @@ "title": "Informaci\u00f3 del compte Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/cs.json b/homeassistant/components/neato/translations/cs.json index bc53bc93f7..5d45710f4a 100644 --- a/homeassistant/components/neato/translations/cs.json +++ b/homeassistant/components/neato/translations/cs.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", - "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", + "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "create_entry": { - "default": "Viz [dokumentace Neato]({docs_url})." + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" }, "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, + "reauth_confirm": { + "title": "Chcete za\u010d\u00edt nastavovat?" + }, "user": { "data": { "password": "Heslo", @@ -21,5 +31,6 @@ "title": "Informace o \u00fa\u010dtu Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index c41d4e6d93..94fcd3c4cb 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -1,12 +1,27 @@ { "config": { "abort": { - "already_configured": "Bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte beachte die Dokumentation.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler sind [im Hilfebereich]({docs_url}) zu finden", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "create_entry": { - "default": "Siehe [Neato-Dokumentation]({docs_url})." + "default": "Erfolgreich authentifiziert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" }, "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "title": "Wollen Sie mit der Einrichtung beginnen?" + }, "user": { "data": { "password": "Passwort", @@ -17,5 +32,6 @@ "title": "Neato-Kontoinformationen" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/en.json b/homeassistant/components/neato/translations/en.json index 61b6ad44df..cc63397964 100644 --- a/homeassistant/components/neato/translations/en.json +++ b/homeassistant/components/neato/translations/en.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Device is already configured", - "invalid_auth": "Invalid authentication" + "authorize_url_timeout": "Timeout generating authorize URL.", + "invalid_auth": "Invalid authentication", + "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})", + "reauth_successful": "Re-authentication was successful" }, "create_entry": { - "default": "See [Neato documentation]({docs_url})." + "default": "Successfully authenticated" }, "error": { "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "title": "Do you want to start set up?" + }, "user": { "data": { "password": "Password", @@ -22,5 +32,6 @@ "title": "Neato Account Info" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/es.json b/homeassistant/components/neato/translations/es.json index abe1d21c90..b88a9d0cfa 100644 --- a/homeassistant/components/neato/translations/es.json +++ b/homeassistant/components/neato/translations/es.json @@ -2,7 +2,11 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "missing_configuration": "El componente no est\u00e1 configurado. 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})", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "create_entry": { "default": "Ver [documentaci\u00f3n Neato]({docs_url})." @@ -12,6 +16,12 @@ "unknown": "Error inesperado" }, "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "title": "\u00bfQuieres iniciar la configuraci\u00f3n?" + }, "user": { "data": { "password": "Contrase\u00f1a", @@ -22,5 +32,6 @@ "title": "Informaci\u00f3n de la cuenta de Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/et.json b/homeassistant/components/neato/translations/et.json index 72ce208db3..0c0aaa5f17 100644 --- a/homeassistant/components/neato/translations/et.json +++ b/homeassistant/components/neato/translations/et.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "invalid_auth": "Tuvastamise viga" + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "invalid_auth": "Tuvastamise viga", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni.", + "no_url_available": "URL-i pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "create_entry": { - "default": "Vaata [Neato documentation] ( {docs_url} )." + "default": "Tuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamise viga", "unknown": "Ootamatu t\u00f5rge" }, "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "title": "Kas soovid alustada seadistamist?" + }, "user": { "data": { "password": "Salas\u00f5na", @@ -22,5 +32,6 @@ "title": "Neato konto teave" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/hu.json b/homeassistant/components/neato/translations/hu.json index 1d88e45b2c..f2fd30f323 100644 --- a/homeassistant/components/neato/translations/hu.json +++ b/homeassistant/components/neato/translations/hu.json @@ -1,12 +1,16 @@ { "config": { "abort": { - "already_configured": "M\u00e1r konfigur\u00e1lva van" + "already_configured": "M\u00e1r konfigur\u00e1lva van", + "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" }, "create_entry": { "default": "L\u00e1sd: [Neato dokument\u00e1ci\u00f3] ( {docs_url} )." }, "step": { + "reauth_confirm": { + "title": "El akarja kezdeni a be\u00e1ll\u00edt\u00e1st?" + }, "user": { "data": { "password": "Jelsz\u00f3", diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index 989bf9ce13..100237c33e 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "invalid_auth": "Autenticazione non valida" + "authorize_url_timeout": "Timeout nella generazione dell'URL di autorizzazione.", + "invalid_auth": "Autenticazione non valida", + "missing_configuration": "Questo componente non \u00e8 configurato. Per favore segui la documentazione.", + "no_url_available": "Nessun URL disponibile. Per altre informazioni su questo errore, [controlla la sezione di aiuto]({docs_url})", + "reauth_successful": "Ri-autenticazione completata con successo" }, "create_entry": { - "default": "Vedere la [Documentazione di Neato]({docs_url})." + "default": "Autenticato con successo" }, "error": { "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "step": { + "pick_implementation": { + "title": "Scegli un metodo di autenticazione" + }, + "reauth_confirm": { + "title": "Vuoi cominciare la configurazione?" + }, "user": { "data": { "password": "Password", @@ -22,5 +32,6 @@ "title": "Informazioni sull'account Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/no.json b/homeassistant/components/neato/translations/no.json index fcbc0361c2..a788c79ff5 100644 --- a/homeassistant/components/neato/translations/no.json +++ b/homeassistant/components/neato/translations/no.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "invalid_auth": "Ugyldig godkjenning" + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "invalid_auth": "Ugyldig godkjenning", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "create_entry": { - "default": "Se [Neato dokumentasjon]({docs_url})." + "default": "Vellykket godkjenning" }, "error": { "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "title": "Vil du starte oppsettet?" + }, "user": { "data": { "password": "Passord", @@ -22,5 +32,6 @@ "title": "Neato kontoinformasjon" } } - } + }, + "title": "" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pl.json b/homeassistant/components/neato/translations/pl.json index 3b7054ea66..3177ed9d8e 100644 --- a/homeassistant/components/neato/translations/pl.json +++ b/homeassistant/components/neato/translations/pl.json @@ -2,16 +2,26 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "invalid_auth": "Niepoprawne uwierzytelnienie" + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "create_entry": { - "default": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url})." + "default": "Pomy\u015blnie uwierzytelniono" }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie", "unknown": "Nieoczekiwany b\u0142\u0105d" }, "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "title": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?" + }, "user": { "data": { "password": "Has\u0142o", @@ -22,5 +32,6 @@ "title": "Informacje o koncie Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json index b464235997..0672c9af33 100644 --- a/homeassistant/components/neato/translations/pt.json +++ b/homeassistant/components/neato/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/neato/translations/ru.json b/homeassistant/components/neato/translations/ru.json index e4a3053967..30ea15c60c 100644 --- a/homeassistant/components/neato/translations/ru.json +++ b/homeassistant/components/neato/translations/ru.json @@ -2,16 +2,26 @@ "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.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "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.", + "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." }, "create_entry": { - "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + "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." }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "reauth_confirm": { + "title": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0447\u0430\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443?" + }, "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", @@ -22,5 +32,6 @@ "title": "Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/sl.json b/homeassistant/components/neato/translations/sl.json index 3ab2d0fd09..96af3b0453 100644 --- a/homeassistant/components/neato/translations/sl.json +++ b/homeassistant/components/neato/translations/sl.json @@ -1,12 +1,22 @@ { "config": { "abort": { - "already_configured": "\u017de konfigurirano" + "already_configured": "\u017de konfigurirano", + "authorize_url_timeout": "\u010casovna omejitev pri ustvarjanju overitvenega URL je potekla.", + "missing_configuration": "Ta komponenta ni konfigurirana. Sledite dokumentaciji.", + "no_url_available": "URL ni na voljo. Za ve\u010d podatkov o tej napaki preverite [razdelek za pomo\u010d]({docs_url})", + "reauth_successful": "Ponovno overjanje je uspelo" }, "create_entry": { "default": "Glejte [neato dokumentacija] ({docs_url})." }, "step": { + "pick_implementation": { + "title": "Izberite na\u010din overjanja" + }, + "reauth_confirm": { + "title": "Bi radi zagnali namestitev?" + }, "user": { "data": { "password": "Geslo", @@ -17,5 +27,6 @@ "title": "Podatki o ra\u010dunu Neato" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/zh-Hant.json b/homeassistant/components/neato/translations/zh-Hant.json index 2c5c8c61fe..beddee423a 100644 --- a/homeassistant/components/neato/translations/zh-Hant.json +++ b/homeassistant/components/neato/translations/zh-Hant.json @@ -1,17 +1,27 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "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})", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "create_entry": { - "default": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "title": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f" + }, "user": { "data": { "password": "\u5bc6\u78bc", @@ -22,5 +32,6 @@ "title": "Neato \u5e33\u865f\u8cc7\u8a0a" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 677bed1565..ce4156244b 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -24,7 +24,7 @@ from homeassistant.components.vacuum import ( SUPPORT_STOP, StateVacuumEntity, ) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE +from homeassistant.const import ATTR_MODE from homeassistant.helpers import config_validation as cv, entity_platform from .const import ( @@ -93,7 +93,6 @@ async def async_setup_entry(hass, entry, async_add_entities): platform.async_register_entity_service( "custom_cleaning", { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(ATTR_MODE, default=2): cv.positive_int, vol.Optional(ATTR_NAVIGATION, default=1): cv.positive_int, vol.Optional(ATTR_CATEGORY, default=4): cv.positive_int, @@ -109,7 +108,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): def __init__(self, neato, robot, mapdata, persistent_maps): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self._available = neato.logged_in if neato is not None else False + self._available = neato is not None self._mapdata = mapdata self._name = f"{self.robot.name}" self._robot_has_map = self.robot.has_persistent_maps diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 97c9da5794..1240d30f02 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,45 +1,35 @@ """Support for Nest devices.""" import asyncio -from datetime import datetime, timedelta import logging -import threading -from google_nest_sdm.event import AsyncEventCallback, EventMessage -from google_nest_sdm.exceptions import GoogleNestException +from google_nest_sdm.event import EventMessage +from google_nest_sdm.exceptions import ( + AuthException, + ConfigurationException, + GoogleNestException, +) from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber -from nest import Nest -from nest.nest import APIError, AuthorizationError import voluptuous as vol -from homeassistant import config_entries -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_CLIENT_ID, CONF_CLIENT_SECRET, - CONF_FILENAME, CONF_MONITORED_CONDITIONS, CONF_SENSORS, CONF_STRUCTURE, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, config_validation as cv, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, - dispatcher_send, -) -from homeassistant.helpers.entity import Entity -from . import api, config_flow, local_auth +from . import api, config_flow from .const import ( API_URL, DATA_SDM, @@ -47,36 +37,19 @@ from .const import ( DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN, - SIGNAL_NEST_UPDATE, ) from .events import EVENT_NAME_MAP, NEST_EVENT +from .legacy import async_setup_legacy, async_setup_legacy_entry _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) CONF_PROJECT_ID = "project_id" CONF_SUBSCRIBER_ID = "subscriber_id" - - -# Configuration for the legacy nest API -SERVICE_CANCEL_ETA = "cancel_eta" -SERVICE_SET_ETA = "set_eta" - -DATA_NEST = "nest" DATA_NEST_CONFIG = "nest_config" +DATA_NEST_UNAVAILABLE = "nest_unavailable" -NEST_CONFIG_FILE = "nest.conf" - -ATTR_ETA = "eta" -ATTR_ETA_WINDOW = "eta_window" -ATTR_STRUCTURE = "structure" -ATTR_TRIP_ID = "trip_id" - -AWAY_MODE_AWAY = "away" -AWAY_MODE_HOME = "home" - -ATTR_AWAY_MODE = "away_mode" -SERVICE_SET_AWAY_MODE = "set_away_mode" +NEST_SETUP_NOTIFICATION = "nest_setup" SENSOR_SCHEMA = vol.Schema( {vol.Optional(CONF_MONITORED_CONDITIONS): vol.All(cv.ensure_list)} @@ -104,31 +77,6 @@ CONFIG_SCHEMA = vol.Schema( # Platforms for SDM API PLATFORMS = ["sensor", "camera", "climate"] -# Services for the legacy API - -SET_AWAY_MODE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME]), - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - -SET_ETA_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ETA): cv.time_period, - vol.Optional(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_ETA_WINDOW): cv.time_period, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - -CANCEL_ETA_SCHEMA = vol.Schema( - { - vol.Required(ATTR_TRIP_ID): cv.string, - vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), - } -) - async def async_setup(hass: HomeAssistant, config: dict): """Set up Nest components with dispatch between old/new flows.""" @@ -163,7 +111,7 @@ async def async_setup(hass: HomeAssistant, config: dict): return True -class SignalUpdateCallback(AsyncEventCallback): +class SignalUpdateCallback: """An EventCallback invoked when new events arrive from subscriber.""" def __init__(self, hass: HomeAssistant): @@ -173,17 +121,8 @@ class SignalUpdateCallback(AsyncEventCallback): async def async_handle_event(self, event_message: EventMessage): """Process an incoming EventMessage.""" if not event_message.resource_update_name: - _LOGGER.debug("Ignoring event with no device_id") return device_id = event_message.resource_update_name - _LOGGER.debug("Update for %s @ %s", device_id, event_message.timestamp) - traits = event_message.resource_update_traits - if traits: - _LOGGER.debug("Trait update %s", traits.keys()) - # This event triggered an update to a device that changed some - # properties which the DeviceManager should already have received. - # Send a signal to refresh state of all listening devices. - async_dispatcher_send(self._hass, SIGNAL_NEST_UPDATE) events = event_message.resource_update_events if not events: return @@ -191,7 +130,6 @@ class SignalUpdateCallback(AsyncEventCallback): device_registry = await self._hass.helpers.device_registry.async_get_registry() device_entry = device_registry.async_get_device({(DOMAIN, device_id)}, ()) if not device_entry: - _LOGGER.debug("Ignoring event for unregistered device '%s'", device_id) return for event in events: event_type = EVENT_NAME_MAP.get(event) @@ -200,6 +138,7 @@ class SignalUpdateCallback(AsyncEventCallback): message = { "device_id": device_entry.id, "type": event_type, + "timestamp": event_message.timestamp, } self._hass.bus.async_fire(NEST_EVENT, message) @@ -227,22 +166,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): subscriber = GoogleNestSubscriber( auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] ) - subscriber.set_update_callback(SignalUpdateCallback(hass)) + callback = SignalUpdateCallback(hass) + subscriber.set_update_callback(callback.async_handle_event) try: await subscriber.start_async() + except AuthException as err: + _LOGGER.debug("Subscriber authentication error: %s", err) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data=entry.data, + ) + ) + return False + except ConfigurationException as err: + _LOGGER.error("Configuration error: %s", err) + subscriber.stop_async() + return False except GoogleNestException as err: - _LOGGER.error("Subscriber error: %s", err) + if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: + _LOGGER.error("Subscriber error: %s", err) + hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True subscriber.stop_async() raise ConfigEntryNotReady from err try: await subscriber.async_get_device_manager() except GoogleNestException as err: - _LOGGER.error("Device Manager error: %s", err) + if DATA_NEST_UNAVAILABLE not in hass.data[DOMAIN]: + _LOGGER.error("Device manager error: %s", err) + hass.data[DOMAIN][DATA_NEST_UNAVAILABLE] = True subscriber.stop_async() raise ConfigEntryNotReady from err + hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) hass.data[DOMAIN][DATA_SUBSCRIBER] = subscriber for component in PLATFORMS: @@ -271,350 +230,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) if unload_ok: hass.data[DOMAIN].pop(DATA_SUBSCRIBER) + hass.data[DOMAIN].pop(DATA_NEST_UNAVAILABLE, None) return unload_ok - - -def nest_update_event_broker(hass, nest): - """ - Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. - - Used for the legacy nest API. - - Runs in its own thread. - """ - _LOGGER.debug("Listening for nest.update_event") - - while hass.is_running: - nest.update_event.wait() - - if not hass.is_running: - break - - nest.update_event.clear() - _LOGGER.debug("Dispatching nest data update") - dispatcher_send(hass, SIGNAL_NEST_UPDATE) - - _LOGGER.debug("Stop listening for nest.update_event") - - -async def async_setup_legacy(hass, config): - """Set up Nest components using the legacy nest API.""" - if DOMAIN not in config: - return True - - conf = config[DOMAIN] - - local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]) - - filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) - access_token_cache_file = hass.config.path(filename) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"nest_conf_path": access_token_cache_file}, - ) - ) - - # Store config to be used during entry setup - hass.data[DATA_NEST_CONFIG] = conf - - return True - - -async def async_setup_legacy_entry(hass, entry): - """Set up Nest from legacy config entry.""" - - nest = Nest(access_token=entry.data["tokens"]["access_token"]) - - _LOGGER.debug("proceeding with setup") - conf = hass.data.get(DATA_NEST_CONFIG, {}) - hass.data[DATA_NEST] = NestLegacyDevice(hass, conf, nest) - if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): - return False - - for component in "climate", "camera", "sensor", "binary_sensor": - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) - ) - - def validate_structures(target_structures): - all_structures = [structure.name for structure in nest.structures] - for target in target_structures: - if target not in all_structures: - _LOGGER.info("Invalid structure: %s", target) - - def set_away_mode(service): - """Set the away mode for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - _LOGGER.info( - "Setting away mode for: %s to: %s", - structure.name, - service.data[ATTR_AWAY_MODE], - ) - structure.away = service.data[ATTR_AWAY_MODE] - - def set_eta(service): - """Set away mode to away and include ETA for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - if structure.thermostats: - _LOGGER.info( - "Setting away mode for: %s to: %s", - structure.name, - AWAY_MODE_AWAY, - ) - structure.away = AWAY_MODE_AWAY - - now = datetime.utcnow() - trip_id = service.data.get( - ATTR_TRIP_ID, f"trip_{int(now.timestamp())}" - ) - eta_begin = now + service.data[ATTR_ETA] - eta_window = service.data.get(ATTR_ETA_WINDOW, timedelta(minutes=1)) - eta_end = eta_begin + eta_window - _LOGGER.info( - "Setting ETA for trip: %s, " - "ETA window starts at: %s and ends at: %s", - trip_id, - eta_begin, - eta_end, - ) - structure.set_eta(trip_id, eta_begin, eta_end) - else: - _LOGGER.info( - "No thermostats found in structure: %s, unable to set ETA", - structure.name, - ) - - def cancel_eta(service): - """Cancel ETA for a Nest structure.""" - if ATTR_STRUCTURE in service.data: - target_structures = service.data[ATTR_STRUCTURE] - validate_structures(target_structures) - else: - target_structures = hass.data[DATA_NEST].local_structure - - for structure in nest.structures: - if structure.name in target_structures: - if structure.thermostats: - trip_id = service.data[ATTR_TRIP_ID] - _LOGGER.info("Cancelling ETA for trip: %s", trip_id) - structure.cancel_eta(trip_id) - else: - _LOGGER.info( - "No thermostats found in structure: %s, " - "unable to cancel ETA", - structure.name, - ) - - hass.services.async_register( - DOMAIN, SERVICE_SET_AWAY_MODE, set_away_mode, schema=SET_AWAY_MODE_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_SET_ETA, set_eta, schema=SET_ETA_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_CANCEL_ETA, cancel_eta, schema=CANCEL_ETA_SCHEMA - ) - - @callback - def start_up(event): - """Start Nest update event listener.""" - threading.Thread( - name="Nest update listener", - target=nest_update_event_broker, - args=(hass, nest), - ).start() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) - - @callback - def shut_down(event): - """Stop Nest update event listener.""" - nest.update_event.set() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) - - _LOGGER.debug("async_setup_nest is done") - - return True - - -class NestLegacyDevice: - """Structure Nest functions for hass for legacy API.""" - - def __init__(self, hass, conf, nest): - """Init Nest Devices.""" - self.hass = hass - self.nest = nest - self.local_structure = conf.get(CONF_STRUCTURE) - - def initialize(self): - """Initialize Nest.""" - try: - # Do not optimize next statement, it is here for initialize - # persistence Nest API connection. - structure_names = [s.name for s in self.nest.structures] - if self.local_structure is None: - self.local_structure = structure_names - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - return False - return True - - def structures(self): - """Generate a list of structures.""" - try: - for structure in self.nest.structures: - if structure.name not in self.local_structure: - _LOGGER.debug( - "Ignoring structure %s, not in %s", - structure.name, - self.local_structure, - ) - continue - yield structure - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - - def thermostats(self): - """Generate a list of thermostats.""" - return self._devices("thermostats") - - def smoke_co_alarms(self): - """Generate a list of smoke co alarms.""" - return self._devices("smoke_co_alarms") - - def cameras(self): - """Generate a list of cameras.""" - return self._devices("cameras") - - def _devices(self, device_type): - """Generate a list of Nest devices.""" - try: - for structure in self.nest.structures: - if structure.name not in self.local_structure: - _LOGGER.debug( - "Ignoring structure %s, not in %s", - structure.name, - self.local_structure, - ) - continue - - for device in getattr(structure, device_type, []): - try: - # Do not optimize next statement, - # it is here for verify Nest API permission. - device.name_long - except KeyError: - _LOGGER.warning( - "Cannot retrieve device name for [%s]" - ", please check your Nest developer " - "account permission settings", - device.serial, - ) - continue - yield (structure, device) - - except (AuthorizationError, APIError, OSError) as err: - _LOGGER.error("Connection error while access Nest web service: %s", err) - - -class NestSensorDevice(Entity): - """Representation of a Nest sensor.""" - - def __init__(self, structure, device, variable): - """Initialize the sensor.""" - self.structure = structure - self.variable = variable - - if device is not None: - # device specific - self.device = device - self._name = f"{self.device.name_long} {self.variable.replace('_', ' ')}" - else: - # structure only - self.device = structure - self._name = f"{self.structure.name} {self.variable.replace('_', ' ')}" - - self._state = None - self._unit = None - - @property - def name(self): - """Return the name of the nest, if any.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return self._unit - - @property - def should_poll(self): - """Do not need poll thanks using Nest streaming API.""" - return False - - @property - def unique_id(self): - """Return unique id based on device serial and variable.""" - return f"{self.device.serial}-{self.variable}" - - @property - def device_info(self): - """Return information about the device.""" - if not hasattr(self.device, "name_long"): - name = self.structure.name - model = "Structure" - else: - name = self.device.name_long - if self.device.is_thermostat: - model = "Thermostat" - elif self.device.is_camera: - model = "Camera" - elif self.device.is_smoke_co_alarm: - model = "Nest Protect" - else: - model = None - - return { - "identifiers": {(DOMAIN, self.device.serial)}, - "name": name, - "manufacturer": "Nest Labs", - "model": model, - } - - def update(self): - """Do not use NestSensorDevice directly.""" - raise NotImplementedError - - async def async_added_to_hass(self): - """Register update signal handler.""" - - async def async_update_state(): - """Update sensor state.""" - await self.async_update_ha_state(True) - - self.async_on_remove( - async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) - ) diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 56d4ac31b7..d49ec8535c 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -1,166 +1,15 @@ -"""Support for Nest Thermostat binary sensors.""" -from itertools import chain -import logging +"""Support for Nest binary sensors that dispatches between API versions.""" -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 homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType -from . import CONF_BINARY_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice - -_LOGGER = logging.getLogger(__name__) - -BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY} - -CLIMATE_BINARY_TYPES = { - "fan": None, - "is_using_emergency_heat": "heat", - "is_locked": None, - "has_leaf": None, -} - -CAMERA_BINARY_TYPES = { - "motion_detected": DEVICE_CLASS_MOTION, - "sound_detected": DEVICE_CLASS_SOUND, - "person_detected": DEVICE_CLASS_OCCUPANCY, -} - -STRUCTURE_BINARY_TYPES = {"away": None} - -STRUCTURE_BINARY_STATE_MAP = {"away": {"away": True, "home": False}} - -_BINARY_TYPES_DEPRECATED = [ - "hvac_ac_state", - "hvac_aux_heater_state", - "hvac_heater_state", - "hvac_heat_x2_state", - "hvac_heat_x3_state", - "hvac_alt_heat_state", - "hvac_alt_heat_x2_state", - "hvac_emer_heat_state", -] - -_VALID_BINARY_SENSOR_TYPES = { - **BINARY_TYPES, - **CLIMATE_BINARY_TYPES, - **CAMERA_BINARY_TYPES, - **STRUCTURE_BINARY_TYPES, -} +from .const import DATA_SDM +from .legacy.binary_sensor import async_setup_legacy_entry -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Nest binary sensors. - - No longer used. - """ - - -async def async_setup_entry(hass, entry, async_add_entities): - """Set up a Nest binary sensor based on a config entry.""" - nest = hass.data[DATA_NEST] - - discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {}) - - # Add all available binary sensors if no Nest binary sensor config is set - if discovery_info == {}: - conditions = _VALID_BINARY_SENSOR_TYPES - else: - conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) - - for variable in conditions: - if variable in _BINARY_TYPES_DEPRECATED: - wstr = ( - f"{variable} is no a longer supported " - "monitored_conditions. See " - "https://www.home-assistant.io/integrations/binary_sensor.nest/ " - "for valid options." - ) - _LOGGER.error(wstr) - - def get_binary_sensors(): - """Get the Nest binary sensors.""" - sensors = [] - for structure in nest.structures(): - sensors += [ - NestBinarySensor(structure, None, variable) - for variable in conditions - if variable in STRUCTURE_BINARY_TYPES - ] - device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) - for structure, device in device_chain: - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in BINARY_TYPES - ] - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CLIMATE_BINARY_TYPES and device.is_thermostat - ] - - if device.is_camera: - sensors += [ - NestBinarySensor(structure, device, variable) - for variable in conditions - if variable in CAMERA_BINARY_TYPES - ] - for activity_zone in device.activity_zones: - sensors += [ - NestActivityZoneSensor(structure, device, activity_zone) - ] - - return sensors - - async_add_entities(await hass.async_add_executor_job(get_binary_sensors), True) - - -class NestBinarySensor(NestSensorDevice, BinarySensorEntity): - """Represents a Nest binary sensor.""" - - @property - def is_on(self): - """Return true if the binary sensor is on.""" - return self._state - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return _VALID_BINARY_SENSOR_TYPES.get(self.variable) - - def update(self): - """Retrieve latest state.""" - value = getattr(self.device, self.variable) - if self.variable in STRUCTURE_BINARY_TYPES: - self._state = bool(STRUCTURE_BINARY_STATE_MAP[self.variable].get(value)) - else: - self._state = bool(value) - - -class NestActivityZoneSensor(NestBinarySensor): - """Represents a Nest binary sensor for activity in a zone.""" - - def __init__(self, structure, device, zone): - """Initialize the sensor.""" - super().__init__(structure, device, "") - self.zone = zone - self._name = f"{self._name} {self.zone.name} activity" - - @property - def unique_id(self): - """Return unique id based on camera serial and zone id.""" - return f"{self.device.serial}-{self.zone.zone_id}" - - @property - def device_class(self): - """Return the device class of the binary sensor.""" - return DEVICE_CLASS_MOTION - - def update(self): - """Retrieve latest state.""" - self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id) +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up the binary sensors.""" + assert DATA_SDM not in entry.data + await async_setup_legacy_entry(hass, entry, async_add_entities) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index dfa365a36c..f0e0b8e05f 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -3,9 +3,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from .camera_legacy import async_setup_legacy_entry from .camera_sdm import async_setup_sdm_entry from .const import DATA_SDM +from .legacy.camera import async_setup_legacy_entry async def async_setup_entry( diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index cec35eeca2..a643de0e6c 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -13,12 +13,11 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow -from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -95,9 +94,10 @@ class NestCamera(Camera): @property def supported_features(self): """Flag supported features.""" + supported_features = 0 if CameraLiveStreamTrait.NAME in self._device.traits: - return SUPPORT_STREAM - return 0 + supported_features |= SUPPORT_STREAM + return supported_features async def stream_source(self): """Return the source of the stream.""" @@ -131,7 +131,6 @@ class NestCamera(Camera): if not self._stream: return _LOGGER.debug("Extending stream url") - self._stream_refresh_unsub = None try: self._stream = await self._stream.extend_rtsp_stream() except GoogleNestException as err: @@ -151,13 +150,8 @@ class NestCamera(Camera): async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" - # Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted - # here to re-fresh the signals from _device. Unregister this callback - # when the entity is removed. self.async_on_remove( - async_dispatcher_connect( - self.hass, SIGNAL_NEST_UPDATE, self.async_write_ha_state - ) + self._device.add_update_listener(self.async_write_ha_state) ) async def async_camera_image(self): diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 6e457da039..a74a50b0f3 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -3,9 +3,9 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType -from .climate_legacy import async_setup_legacy_entry from .climate_sdm import async_setup_sdm_entry from .const import DATA_SDM +from .legacy.climate import async_setup_legacy_entry async def async_setup_entry( diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index e56d35c1df..08cb0161bd 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -36,10 +36,9 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType -from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo # Mapping for sdm.devices.traits.ThermostatMode mode field @@ -126,16 +125,9 @@ class ThermostatEntity(ClimateEntity): async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" - # Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted - # here to re-fresh the signals from _device. Unregister this callback - # when the entity is removed. self._supported_features = self._get_supported_features() self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_NEST_UPDATE, - self.async_write_ha_state, - ) + self._device.add_update_listener(self.async_write_ha_state) ) @property @@ -186,8 +178,6 @@ class ThermostatEntity(ClimateEntity): @property def _target_temperature_trait(self): """Return the correct trait with a target temp depending on mode.""" - if not self.hvac_mode: - return None if self.preset_mode == PRESET_ECO: if ThermostatEcoTrait.NAME in self._device.traits: return self._device.traits[ThermostatEcoTrait.NAME] @@ -225,8 +215,6 @@ class ThermostatEntity(ClimateEntity): @property def hvac_action(self): """Return the current HVAC action (heating, cooling).""" - if ThermostatHvacTrait.NAME not in self._device.traits: - return None trait = self._device.traits[ThermostatHvacTrait.NAME] if trait.status in THERMOSTAT_HVAC_STATUS_MAP: return THERMOSTAT_HVAC_STATUS_MAP[trait.status] @@ -262,9 +250,10 @@ class ThermostatEntity(ClimateEntity): @property def fan_modes(self): """Return the list of available fan modes.""" + modes = [] if FanTrait.NAME in self._device.traits: - return list(FAN_INV_MODE_MAP) - return [] + modes = list(FAN_INV_MODE_MAP) + return modes @property def supported_features(self): @@ -290,12 +279,8 @@ class ThermostatEntity(ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: - return - if hvac_mode not in THERMOSTAT_INV_MODE_MAP: - return + raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] - if ThermostatModeTrait.NAME not in self._device.traits: - return trait = self._device.traits[ThermostatModeTrait.NAME] await trait.set_mode(api_mode) @@ -318,17 +303,13 @@ class ThermostatEntity(ClimateEntity): async def async_set_preset_mode(self, preset_mode): """Set new target preset mode.""" if preset_mode not in self.preset_modes: - return - if ThermostatEcoTrait.NAME not in self._device.traits: - return + raise ValueError(f"Unsupported preset_mode '{preset_mode}'") trait = self._device.traits[ThermostatEcoTrait.NAME] await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" if fan_mode not in self.fan_modes: - return - if FanTrait.NAME not in self._device.traits: - return + raise ValueError(f"Unsupported fan_mode '{fan_mode}'") trait = self._device.traits[FanTrait.NAME] await trait.set_timer(FAN_INV_MODE_MAP[fan_mode]) diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index 6aaa5bcc48..36b0da239a 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -75,6 +75,12 @@ class NestFlowHandler( VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + def __init__(self): + """Initialize NestFlowHandler.""" + super().__init__() + # When invoked for reauth, allows updating an existing config entry + self._reauth = False + @classmethod def register_sdm_api(cls, hass): """Configure the flow handler to use the SDM API.""" @@ -103,19 +109,56 @@ class NestFlowHandler( async def async_oauth_create_entry(self, data: dict) -> dict: """Create an entry for the SDM flow.""" + assert self.is_sdm_api(), "Step only supported for SDM API" data[DATA_SDM] = {} + await self.async_set_unique_id(DOMAIN) + # Update existing config entry when in the reauth flow. This + # integration only supports one config entry so remove any prior entries + # added before the "single_instance_allowed" check was added + existing_entries = self.hass.config_entries.async_entries(DOMAIN) + if existing_entries: + updated = False + for entry in existing_entries: + if updated: + await self.hass.config_entries.async_remove(entry.entry_id) + continue + updated = True + self.hass.config_entries.async_update_entry( + entry, data=data, unique_id=DOMAIN + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + return await super().async_oauth_create_entry(data) + async def async_step_reauth(self, user_input=None): + """Perform reauth upon an API authentication error.""" + assert self.is_sdm_api(), "Step only supported for SDM API" + self._reauth = True # Forces update of existing config entry + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm(self, user_input=None): + """Confirm reauth dialog.""" + assert self.is_sdm_api(), "Step only supported for SDM API" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + return await self.async_step_user() + async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" if self.is_sdm_api(): + # Reauth will update an existing entry + if self.hass.config_entries.async_entries(DOMAIN) and not self._reauth: + return self.async_abort(reason="single_instance_allowed") return await super().async_step_user(user_input) return await self.async_step_init(user_input) async def async_step_init(self, user_input=None): """Handle a flow start.""" - if self.is_sdm_api(): - raise UnexpectedStateError("Step only supported for legacy API") + assert not self.is_sdm_api(), "Step only supported for legacy API" flows = self.hass.data.get(DATA_FLOW_IMPL, {}) @@ -145,8 +188,7 @@ class NestFlowHandler( implementation type we expect a pin or an external component to deliver the authentication code. """ - if self.is_sdm_api(): - raise UnexpectedStateError("Step only supported for legacy API") + assert not self.is_sdm_api(), "Step only supported for legacy API" flow = self.hass.data[DATA_FLOW_IMPL][self.flow_impl] @@ -188,8 +230,7 @@ class NestFlowHandler( async def async_step_import(self, info): """Import existing auth from Nest.""" - if self.is_sdm_api(): - raise UnexpectedStateError("Step only supported for legacy API") + assert not self.is_sdm_api(), "Step only supported for legacy API" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="single_instance_allowed") diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 199dcf425d..e5bd7ea1ca 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -54,7 +54,7 @@ async def async_get_device_trigger_types( # Determine the set of event types based on the supported device traits trigger_types = [] - for trait in nest_device.traits.keys(): + for trait in nest_device.traits: trigger_type = DEVICE_TRAIT_TRIGGER_MAP.get(trait) if trigger_type: trigger_types.append(trigger_type) diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py new file mode 100644 index 0000000000..218b01fd71 --- /dev/null +++ b/homeassistant/components/nest/legacy/__init__.py @@ -0,0 +1,416 @@ +"""Support for Nest devices.""" + +from datetime import datetime, timedelta +import logging +import threading + +from nest import Nest +from nest.nest import APIError, AuthorizationError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_CLIENT_ID, + CONF_CLIENT_SECRET, + CONF_FILENAME, + CONF_STRUCTURE, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity + +from . import local_auth +from .const import DATA_NEST, DATA_NEST_CONFIG, DOMAIN, SIGNAL_NEST_UPDATE + +_CONFIGURING = {} +_LOGGER = logging.getLogger(__name__) + +# Configuration for the legacy nest API +SERVICE_CANCEL_ETA = "cancel_eta" +SERVICE_SET_ETA = "set_eta" + +NEST_CONFIG_FILE = "nest.conf" + +ATTR_ETA = "eta" +ATTR_ETA_WINDOW = "eta_window" +ATTR_STRUCTURE = "structure" +ATTR_TRIP_ID = "trip_id" + +AWAY_MODE_AWAY = "away" +AWAY_MODE_HOME = "home" + +ATTR_AWAY_MODE = "away_mode" +SERVICE_SET_AWAY_MODE = "set_away_mode" + +# Services for the legacy API + +SET_AWAY_MODE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_AWAY_MODE): vol.In([AWAY_MODE_AWAY, AWAY_MODE_HOME]), + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), + } +) + +SET_ETA_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ETA): cv.time_period, + vol.Optional(ATTR_TRIP_ID): cv.string, + vol.Optional(ATTR_ETA_WINDOW): cv.time_period, + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), + } +) + +CANCEL_ETA_SCHEMA = vol.Schema( + { + vol.Required(ATTR_TRIP_ID): cv.string, + vol.Optional(ATTR_STRUCTURE): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def nest_update_event_broker(hass, nest): + """ + Dispatch SIGNAL_NEST_UPDATE to devices when nest stream API received data. + + Used for the legacy nest API. + + Runs in its own thread. + """ + _LOGGER.debug("Listening for nest.update_event") + + while hass.is_running: + nest.update_event.wait() + + if not hass.is_running: + break + + nest.update_event.clear() + _LOGGER.debug("Dispatching nest data update") + dispatcher_send(hass, SIGNAL_NEST_UPDATE) + + _LOGGER.debug("Stop listening for nest.update_event") + + +async def async_setup_legacy(hass, config): + """Set up Nest components using the legacy nest API.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + local_auth.initialize(hass, conf[CONF_CLIENT_ID], conf[CONF_CLIENT_SECRET]) + + filename = config.get(CONF_FILENAME, NEST_CONFIG_FILE) + access_token_cache_file = hass.config.path(filename) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"nest_conf_path": access_token_cache_file}, + ) + ) + + # Store config to be used during entry setup + hass.data[DATA_NEST_CONFIG] = conf + + return True + + +async def async_setup_legacy_entry(hass, entry): + """Set up Nest from legacy config entry.""" + + nest = Nest(access_token=entry.data["tokens"]["access_token"]) + + _LOGGER.debug("proceeding with setup") + conf = hass.data.get(DATA_NEST_CONFIG, {}) + hass.data[DATA_NEST] = NestLegacyDevice(hass, conf, nest) + if not await hass.async_add_executor_job(hass.data[DATA_NEST].initialize): + return False + + for component in "climate", "camera", "sensor", "binary_sensor": + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + def validate_structures(target_structures): + all_structures = [structure.name for structure in nest.structures] + for target in target_structures: + if target not in all_structures: + _LOGGER.info("Invalid structure: %s", target) + + def set_away_mode(service): + """Set the away mode for a Nest structure.""" + if ATTR_STRUCTURE in service.data: + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) + else: + target_structures = hass.data[DATA_NEST].local_structure + + for structure in nest.structures: + if structure.name in target_structures: + _LOGGER.info( + "Setting away mode for: %s to: %s", + structure.name, + service.data[ATTR_AWAY_MODE], + ) + structure.away = service.data[ATTR_AWAY_MODE] + + def set_eta(service): + """Set away mode to away and include ETA for a Nest structure.""" + if ATTR_STRUCTURE in service.data: + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) + else: + target_structures = hass.data[DATA_NEST].local_structure + + for structure in nest.structures: + if structure.name in target_structures: + if structure.thermostats: + _LOGGER.info( + "Setting away mode for: %s to: %s", + structure.name, + AWAY_MODE_AWAY, + ) + structure.away = AWAY_MODE_AWAY + + now = datetime.utcnow() + trip_id = service.data.get( + ATTR_TRIP_ID, f"trip_{int(now.timestamp())}" + ) + eta_begin = now + service.data[ATTR_ETA] + eta_window = service.data.get(ATTR_ETA_WINDOW, timedelta(minutes=1)) + eta_end = eta_begin + eta_window + _LOGGER.info( + "Setting ETA for trip: %s, " + "ETA window starts at: %s and ends at: %s", + trip_id, + eta_begin, + eta_end, + ) + structure.set_eta(trip_id, eta_begin, eta_end) + else: + _LOGGER.info( + "No thermostats found in structure: %s, unable to set ETA", + structure.name, + ) + + def cancel_eta(service): + """Cancel ETA for a Nest structure.""" + if ATTR_STRUCTURE in service.data: + target_structures = service.data[ATTR_STRUCTURE] + validate_structures(target_structures) + else: + target_structures = hass.data[DATA_NEST].local_structure + + for structure in nest.structures: + if structure.name in target_structures: + if structure.thermostats: + trip_id = service.data[ATTR_TRIP_ID] + _LOGGER.info("Cancelling ETA for trip: %s", trip_id) + structure.cancel_eta(trip_id) + else: + _LOGGER.info( + "No thermostats found in structure: %s, " + "unable to cancel ETA", + structure.name, + ) + + hass.services.async_register( + DOMAIN, SERVICE_SET_AWAY_MODE, set_away_mode, schema=SET_AWAY_MODE_SCHEMA + ) + + hass.services.async_register( + DOMAIN, SERVICE_SET_ETA, set_eta, schema=SET_ETA_SCHEMA + ) + + hass.services.async_register( + DOMAIN, SERVICE_CANCEL_ETA, cancel_eta, schema=CANCEL_ETA_SCHEMA + ) + + @callback + def start_up(event): + """Start Nest update event listener.""" + threading.Thread( + name="Nest update listener", + target=nest_update_event_broker, + args=(hass, nest), + ).start() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_up) + + @callback + def shut_down(event): + """Stop Nest update event listener.""" + nest.update_event.set() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shut_down) + + _LOGGER.debug("async_setup_nest is done") + + return True + + +class NestLegacyDevice: + """Structure Nest functions for hass for legacy API.""" + + def __init__(self, hass, conf, nest): + """Init Nest Devices.""" + self.hass = hass + self.nest = nest + self.local_structure = conf.get(CONF_STRUCTURE) + + def initialize(self): + """Initialize Nest.""" + try: + # Do not optimize next statement, it is here for initialize + # persistence Nest API connection. + structure_names = [s.name for s in self.nest.structures] + if self.local_structure is None: + self.local_structure = structure_names + + except (AuthorizationError, APIError, OSError) as err: + _LOGGER.error("Connection error while access Nest web service: %s", err) + return False + return True + + def structures(self): + """Generate a list of structures.""" + try: + for structure in self.nest.structures: + if structure.name not in self.local_structure: + _LOGGER.debug( + "Ignoring structure %s, not in %s", + structure.name, + self.local_structure, + ) + continue + yield structure + + except (AuthorizationError, APIError, OSError) as err: + _LOGGER.error("Connection error while access Nest web service: %s", err) + + def thermostats(self): + """Generate a list of thermostats.""" + return self._devices("thermostats") + + def smoke_co_alarms(self): + """Generate a list of smoke co alarms.""" + return self._devices("smoke_co_alarms") + + def cameras(self): + """Generate a list of cameras.""" + return self._devices("cameras") + + def _devices(self, device_type): + """Generate a list of Nest devices.""" + try: + for structure in self.nest.structures: + if structure.name not in self.local_structure: + _LOGGER.debug( + "Ignoring structure %s, not in %s", + structure.name, + self.local_structure, + ) + continue + + for device in getattr(structure, device_type, []): + try: + # Do not optimize next statement, + # it is here for verify Nest API permission. + device.name_long + except KeyError: + _LOGGER.warning( + "Cannot retrieve device name for [%s]" + ", please check your Nest developer " + "account permission settings", + device.serial, + ) + continue + yield (structure, device) + + except (AuthorizationError, APIError, OSError) as err: + _LOGGER.error("Connection error while access Nest web service: %s", err) + + +class NestSensorDevice(Entity): + """Representation of a Nest sensor.""" + + def __init__(self, structure, device, variable): + """Initialize the sensor.""" + self.structure = structure + self.variable = variable + + if device is not None: + # device specific + self.device = device + self._name = f"{self.device.name_long} {self.variable.replace('_', ' ')}" + else: + # structure only + self.device = structure + self._name = f"{self.structure.name} {self.variable.replace('_', ' ')}" + + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the nest, if any.""" + return self._name + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @property + def should_poll(self): + """Do not need poll thanks using Nest streaming API.""" + return False + + @property + def unique_id(self): + """Return unique id based on device serial and variable.""" + return f"{self.device.serial}-{self.variable}" + + @property + def device_info(self): + """Return information about the device.""" + if not hasattr(self.device, "name_long"): + name = self.structure.name + model = "Structure" + else: + name = self.device.name_long + if self.device.is_thermostat: + model = "Thermostat" + elif self.device.is_camera: + model = "Camera" + elif self.device.is_smoke_co_alarm: + model = "Nest Protect" + else: + model = None + + return { + "identifiers": {(DOMAIN, self.device.serial)}, + "name": name, + "manufacturer": "Nest Labs", + "model": model, + } + + def update(self): + """Do not use NestSensorDevice directly.""" + raise NotImplementedError + + async def async_added_to_hass(self): + """Register update signal handler.""" + + async def async_update_state(): + """Update sensor state.""" + await self.async_update_ha_state(True) + + self.async_on_remove( + async_dispatcher_connect(self.hass, SIGNAL_NEST_UPDATE, async_update_state) + ) diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py new file mode 100644 index 0000000000..32c30f747d --- /dev/null +++ b/homeassistant/components/nest/legacy/binary_sensor.py @@ -0,0 +1,167 @@ +"""Support for Nest Thermostat binary sensors.""" +from itertools import chain +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_SOUND, + BinarySensorEntity, +) +from homeassistant.const import CONF_BINARY_SENSORS, CONF_MONITORED_CONDITIONS + +from . import NestSensorDevice +from .const import DATA_NEST, DATA_NEST_CONFIG + +_LOGGER = logging.getLogger(__name__) + +BINARY_TYPES = {"online": DEVICE_CLASS_CONNECTIVITY} + +CLIMATE_BINARY_TYPES = { + "fan": None, + "is_using_emergency_heat": "heat", + "is_locked": None, + "has_leaf": None, +} + +CAMERA_BINARY_TYPES = { + "motion_detected": DEVICE_CLASS_MOTION, + "sound_detected": DEVICE_CLASS_SOUND, + "person_detected": DEVICE_CLASS_OCCUPANCY, +} + +STRUCTURE_BINARY_TYPES = {"away": None} + +STRUCTURE_BINARY_STATE_MAP = {"away": {"away": True, "home": False}} + +_BINARY_TYPES_DEPRECATED = [ + "hvac_ac_state", + "hvac_aux_heater_state", + "hvac_heater_state", + "hvac_heat_x2_state", + "hvac_heat_x3_state", + "hvac_alt_heat_state", + "hvac_alt_heat_x2_state", + "hvac_emer_heat_state", +] + +_VALID_BINARY_SENSOR_TYPES = { + **BINARY_TYPES, + **CLIMATE_BINARY_TYPES, + **CAMERA_BINARY_TYPES, + **STRUCTURE_BINARY_TYPES, +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Nest binary sensors. + + No longer used. + """ + + +async def async_setup_legacy_entry(hass, entry, async_add_entities): + """Set up a Nest binary sensor based on a config entry.""" + nest = hass.data[DATA_NEST] + + discovery_info = hass.data.get(DATA_NEST_CONFIG, {}).get(CONF_BINARY_SENSORS, {}) + + # Add all available binary sensors if no Nest binary sensor config is set + if discovery_info == {}: + conditions = _VALID_BINARY_SENSOR_TYPES + else: + conditions = discovery_info.get(CONF_MONITORED_CONDITIONS, {}) + + for variable in conditions: + if variable in _BINARY_TYPES_DEPRECATED: + wstr = ( + f"{variable} is no a longer supported " + "monitored_conditions. See " + "https://www.home-assistant.io/integrations/binary_sensor.nest/ " + "for valid options." + ) + _LOGGER.error(wstr) + + def get_binary_sensors(): + """Get the Nest binary sensors.""" + sensors = [] + for structure in nest.structures(): + sensors += [ + NestBinarySensor(structure, None, variable) + for variable in conditions + if variable in STRUCTURE_BINARY_TYPES + ] + device_chain = chain(nest.thermostats(), nest.smoke_co_alarms(), nest.cameras()) + for structure, device in device_chain: + sensors += [ + NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in BINARY_TYPES + ] + sensors += [ + NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in CLIMATE_BINARY_TYPES and device.is_thermostat + ] + + if device.is_camera: + sensors += [ + NestBinarySensor(structure, device, variable) + for variable in conditions + if variable in CAMERA_BINARY_TYPES + ] + for activity_zone in device.activity_zones: + sensors += [ + NestActivityZoneSensor(structure, device, activity_zone) + ] + + return sensors + + async_add_entities(await hass.async_add_executor_job(get_binary_sensors), True) + + +class NestBinarySensor(NestSensorDevice, BinarySensorEntity): + """Represents a Nest binary sensor.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return _VALID_BINARY_SENSOR_TYPES.get(self.variable) + + def update(self): + """Retrieve latest state.""" + value = getattr(self.device, self.variable) + if self.variable in STRUCTURE_BINARY_TYPES: + self._state = bool(STRUCTURE_BINARY_STATE_MAP[self.variable].get(value)) + else: + self._state = bool(value) + + +class NestActivityZoneSensor(NestBinarySensor): + """Represents a Nest binary sensor for activity in a zone.""" + + def __init__(self, structure, device, zone): + """Initialize the sensor.""" + super().__init__(structure, device, "") + self.zone = zone + self._name = f"{self._name} {self.zone.name} activity" + + @property + def unique_id(self): + """Return unique id based on camera serial and zone id.""" + return f"{self.device.serial}-{self.zone.zone_id}" + + @property + def device_class(self): + """Return the device class of the binary sensor.""" + return DEVICE_CLASS_MOTION + + def update(self): + """Retrieve latest state.""" + self._state = self.device.has_ongoing_motion_in_zone(self.zone.zone_id) diff --git a/homeassistant/components/nest/camera_legacy.py b/homeassistant/components/nest/legacy/camera.py similarity index 95% rename from homeassistant/components/nest/camera_legacy.py rename to homeassistant/components/nest/legacy/camera.py index 48d9cb0078..cc9be9d758 100644 --- a/homeassistant/components/nest/camera_legacy.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -4,10 +4,11 @@ import logging import requests -from homeassistant.components import nest from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_ON_OFF, Camera from homeassistant.util.dt import utcnow +from .const import DATA_NEST, DOMAIN + _LOGGER = logging.getLogger(__name__) NEST_BRAND = "Nest" @@ -24,9 +25,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_legacy_entry(hass, entry, async_add_entities): """Set up a Nest sensor based on a config entry.""" - camera_devices = await hass.async_add_executor_job( - hass.data[nest.DATA_NEST].cameras - ) + camera_devices = await hass.async_add_executor_job(hass.data[DATA_NEST].cameras) cameras = [NestCamera(structure, device) for structure, device in camera_devices] async_add_entities(cameras, True) @@ -63,7 +62,7 @@ class NestCamera(Camera): def device_info(self): """Return information about the device.""" return { - "identifiers": {(nest.DOMAIN, self.device.device_id)}, + "identifiers": {(DOMAIN, self.device.device_id)}, "name": self.device.name_long, "manufacturer": "Nest Labs", "model": "Camera", diff --git a/homeassistant/components/nest/climate_legacy.py b/homeassistant/components/nest/legacy/climate.py similarity index 98% rename from homeassistant/components/nest/climate_legacy.py rename to homeassistant/components/nest/legacy/climate.py index ee28a0905c..cd0d66acba 100644 --- a/homeassistant/components/nest/climate_legacy.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -1,4 +1,4 @@ -"""Support for Nest thermostats.""" +"""Legacy Works with Nest climate implementation.""" import logging from nest.nest import APIError @@ -33,8 +33,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_NEST, DOMAIN as NEST_DOMAIN -from .const import SIGNAL_NEST_UPDATE +from .const import DATA_NEST, DOMAIN, SIGNAL_NEST_UPDATE _LOGGER = logging.getLogger(__name__) @@ -170,7 +169,7 @@ class NestThermostat(ClimateEntity): def device_info(self): """Return information about the device.""" return { - "identifiers": {(NEST_DOMAIN, self.device.device_id)}, + "identifiers": {(DOMAIN, self.device.device_id)}, "name": self.device.name_long, "manufacturer": "Nest Labs", "model": "Thermostat", diff --git a/homeassistant/components/nest/legacy/const.py b/homeassistant/components/nest/legacy/const.py new file mode 100644 index 0000000000..664606b9ed --- /dev/null +++ b/homeassistant/components/nest/legacy/const.py @@ -0,0 +1,6 @@ +"""Constants used by the legacy Nest component.""" + +DOMAIN = "nest" +DATA_NEST = "nest" +DATA_NEST_CONFIG = "nest_config" +SIGNAL_NEST_UPDATE = "nest_update" diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/legacy/local_auth.py similarity index 85% rename from homeassistant/components/nest/local_auth.py rename to homeassistant/components/nest/legacy/local_auth.py index 8be2693325..f5fb286df7 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/legacy/local_auth.py @@ -7,14 +7,14 @@ from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth from homeassistant.const import HTTP_UNAUTHORIZED from homeassistant.core import callback -from . import config_flow +from ..config_flow import CodeInvalid, NestAuthError, register_flow_implementation from .const import DOMAIN @callback def initialize(hass, client_id, client_secret): """Initialize a local auth provider.""" - config_flow.register_flow_implementation( + register_flow_implementation( hass, DOMAIN, "configuration.yaml", @@ -44,7 +44,7 @@ async def resolve_auth_code(hass, client_id, client_secret, code): return await result except AuthorizationError as err: if err.response.status_code == HTTP_UNAUTHORIZED: - raise config_flow.CodeInvalid() - raise config_flow.NestAuthError( + raise CodeInvalid() from err + raise NestAuthError( f"Unknown error: {err} ({err.response.status_code})" - ) + ) from err diff --git a/homeassistant/components/nest/sensor_legacy.py b/homeassistant/components/nest/legacy/sensor.py similarity index 98% rename from homeassistant/components/nest/sensor_legacy.py rename to homeassistant/components/nest/legacy/sensor.py index 2df668513e..34f525ca7a 100644 --- a/homeassistant/components/nest/sensor_legacy.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -3,6 +3,7 @@ import logging from homeassistant.const import ( CONF_MONITORED_CONDITIONS, + CONF_SENSORS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, PERCENTAGE, @@ -11,7 +12,8 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) -from . import CONF_SENSORS, DATA_NEST, DATA_NEST_CONFIG, NestSensorDevice +from . import NestSensorDevice +from .const import DATA_NEST, DATA_NEST_CONFIG SENSOR_TYPES = ["humidity", "operation_mode", "hvac_state"] diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 60293612cd..7d60bb1cf5 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/nest", "requirements": [ "python-nest==4.1.0", - "google-nest-sdm==0.2.0" + "google-nest-sdm==0.2.5" ], "codeowners": [ "@awarecan", diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index 6245c5d83d..0dcc89e226 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -4,7 +4,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from .const import DATA_SDM -from .sensor_legacy import async_setup_legacy_entry +from .legacy.sensor import async_setup_legacy_entry from .sensor_sdm import async_setup_sdm_entry diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index 9009414c5b..52490f41f8 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -15,11 +15,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from .const import DATA_SUBSCRIBER, DOMAIN, SIGNAL_NEST_UPDATE +from .const import DATA_SUBSCRIBER, DOMAIN from .device_info import DeviceInfo _LOGGER = logging.getLogger(__name__) @@ -80,15 +79,8 @@ class SensorBase(Entity): async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" - # Event messages trigger the SIGNAL_NEST_UPDATE, which is intercepted - # here to re-fresh the signals from _device. Unregister this callback - # when the entity is removed. self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_NEST_UPDATE, - self.async_write_ha_state, - ) + self._device.add_update_listener(self.async_write_ha_state) ) diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index f945469e26..6ce529621a 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -4,6 +4,10 @@ "pick_implementation": { "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The Nest integration needs to re-authenticate your account" + }, "init": { "title": "Authentication Provider", "description": "[%key:common::config_flow::title::oauth2_pick_implementation%]", @@ -30,7 +34,8 @@ "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", "unknown_authorize_url_generation": "[%key:common::config_flow::abort::unknown_authorize_url_generation%]", - "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]" + "no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" diff --git a/homeassistant/components/nest/translations/ca.json b/homeassistant/components/nest/translations/ca.json index 46558f2e89..08d8cb9745 100644 --- a/homeassistant/components/nest/translations/ca.json +++ b/homeassistant/components/nest/translations/ca.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3.", "no_url_available": "No hi ha cap URL disponible. Per a m\u00e9s informaci\u00f3 sobre aquest error, [consulta la secci\u00f3 d'ajuda]({docs_url})", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "unknown_authorize_url_generation": "S'ha produ\u00eft un error desconegut al generar URL d'autoritzaci\u00f3." }, @@ -34,7 +35,19 @@ }, "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + }, + "reauth_confirm": { + "description": "La integraci\u00f3 de Nest ha de tornar a autenticar-se amb el teu compte", + "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Moviment detectat", + "camera_person": "Persona detectada", + "camera_sound": "So detectat", + "doorbell_chime": "Timbre premut" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/cs.json b/homeassistant/components/nest/translations/cs.json index 9ab94c993e..843ce98330 100644 --- a/homeassistant/components/nest/translations/cs.json +++ b/homeassistant/components/nest/translations/cs.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace.", "no_url_available": "Nen\u00ed k dispozici \u017e\u00e1dn\u00e1 adresa URL. Informace o t\u00e9to chyb\u011b naleznete [v sekci n\u00e1pov\u011bdy]({docs_url})", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "unknown_authorize_url_generation": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL adresy." }, @@ -34,6 +35,9 @@ }, "pick_implementation": { "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + }, + "reauth_confirm": { + "title": "Znovu ov\u011b\u0159it integraci" } } }, diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 3f55b19b25..2bc328ff8f 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -2,7 +2,9 @@ "config": { "abort": { "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL" + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL", + "reauth_successful": "Neuathentifizierung erfolgreich", + "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, "error": { "internal_error": "Ein interner Fehler ist aufgetreten", @@ -24,7 +26,19 @@ }, "description": "[Autorisiere dein Konto] ( {url} ), um deinen Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcge anschlie\u00dfend den erhaltenen PIN Code hier ein.", "title": "Nest-Konto verkn\u00fcpfen" + }, + "reauth_confirm": { + "description": "Die Nest-Integration muss das Konto neu authentifizieren", + "title": "Integration neu authentifizieren" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Bewegung erkannt", + "camera_person": "Person erkannt", + "camera_sound": "Ger\u00e4usch erkannt", + "doorbell_chime": "T\u00fcrklingel gedr\u00fcckt" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/en.json b/homeassistant/components/nest/translations/en.json index 739d77c826..6693c2e561 100644 --- a/homeassistant/components/nest/translations/en.json +++ b/homeassistant/components/nest/translations/en.json @@ -5,6 +5,7 @@ "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})", + "reauth_successful": "Re-authentication was successful", "single_instance_allowed": "Already configured. Only a single configuration possible.", "unknown_authorize_url_generation": "Unknown error generating an authorize url." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Pick Authentication Method" + }, + "reauth_confirm": { + "description": "The Nest integration needs to re-authenticate your account", + "title": "Reauthenticate Integration" } } }, diff --git a/homeassistant/components/nest/translations/es.json b/homeassistant/components/nest/translations/es.json index da5d717cb3..4c0b8b2617 100644 --- a/homeassistant/components/nest/translations/es.json +++ b/homeassistant/components/nest/translations/es.json @@ -5,6 +5,7 @@ "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_url_available": "No hay URL disponible. Para obtener informaci\u00f3n sobre este error, [consulta la secci\u00f3n de ayuda]({docs_url})", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n.", "unknown_authorize_url_generation": "Error desconocido al generar una URL de autorizaci\u00f3n." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Elija el m\u00e9todo de autenticaci\u00f3n" + }, + "reauth_confirm": { + "description": "La integraci\u00f3n de Nest necesita volver a autenticar tu cuenta", + "title": "Volver a autenticar la integraci\u00f3n" } } }, diff --git a/homeassistant/components/nest/translations/et.json b/homeassistant/components/nest/translations/et.json index 2e58ddeedd..7d22dfd96b 100644 --- a/homeassistant/components/nest/translations/et.json +++ b/homeassistant/components/nest/translations/et.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Tuvastamise URL-i loomise ajal\u00f5pp.", "missing_configuration": "Osis pole seadistatud. Vaata dokumentatsiooni.", "no_url_available": "URL pole saadaval. Selle t\u00f5rke kohta teabe saamiseks vaata [spikrijaotis]({docs_url})", + "reauth_successful": "Taastuvastamine \u00f5nnestus", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "unknown_authorize_url_generation": "Tundmatu viga tuvastamise URL-i loomisel." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Vali tuvastusmeetod" + }, + "reauth_confirm": { + "description": "Nesti sidumine peab konto taastuvastama", + "title": "Taastuvasta sidumine" } } }, diff --git a/homeassistant/components/nest/translations/hu.json b/homeassistant/components/nest/translations/hu.json index d9a216305e..47334c4aa6 100644 --- a/homeassistant/components/nest/translations/hu.json +++ b/homeassistant/components/nest/translations/hu.json @@ -2,13 +2,15 @@ "config": { "abort": { "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", - "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n." + "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", + "reauth_successful": "Az \u00fajb\u00f3li azonos\u00edt\u00e1s sikeres" }, "create_entry": { "default": "Sikeres autentik\u00e1ci\u00f3" }, "error": { "internal_error": "Bels\u0151 hiba t\u00f6rt\u00e9nt a k\u00f3d valid\u00e1l\u00e1s\u00e1n\u00e1l", + "invalid_pin": "\u00c9rv\u00e9nytelen ", "timeout": "Id\u0151t\u00fall\u00e9p\u00e9s a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n.", "unknown": "Ismeretlen hiba t\u00f6rt\u00e9nt a k\u00f3d \u00e9rv\u00e9nyes\u00edt\u00e9se sor\u00e1n" }, @@ -26,7 +28,18 @@ }, "description": "A Nest-fi\u00f3k \u00f6sszekapcsol\u00e1s\u00e1hoz [enged\u00e9lyezze fi\u00f3kj\u00e1t] ( {url} ). \n\n Az enged\u00e9lyez\u00e9s ut\u00e1n m\u00e1solja be az al\u00e1bbi PIN k\u00f3dot.", "title": "Nest fi\u00f3k \u00f6sszekapcsol\u00e1sa" + }, + "reauth_confirm": { + "title": "Integr\u00e1ci\u00f3 \u00fajb\u00f3li azonos\u00edt\u00e1sa" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Mozg\u00e1s \u00e9szlelve", + "camera_person": "Szem\u00e9ly \u00e9szlelve", + "camera_sound": "Hang \u00e9szlelve", + "doorbell_chime": "Cseng\u0151 megnyomva" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 00e949b652..958eaea039 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -5,6 +5,7 @@ "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})", + "reauth_successful": "Riautenticato con successo", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." }, @@ -34,7 +35,19 @@ }, "pick_implementation": { "title": "Scegli il metodo di autenticazione" + }, + "reauth_confirm": { + "description": "L'integrazione di Nest deve autenticare nuovamente il tuo account", + "title": "Autentica nuovamente l'integrazione" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Movimento rilevato", + "camera_person": "Persona rilevata", + "camera_sound": "Suono rilevato", + "doorbell_chime": "Campanello premuto" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/nl.json b/homeassistant/components/nest/translations/nl.json index bd72b659ae..931b8aa770 100644 --- a/homeassistant/components/nest/translations/nl.json +++ b/homeassistant/components/nest/translations/nl.json @@ -25,5 +25,13 @@ "title": "Koppel Nest-account" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Beweging gedetecteerd", + "camera_person": "Persoon gedetecteerd", + "camera_sound": "Geluid gedetecteerd", + "doorbell_chime": "Deurbel is ingedrukt" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/no.json b/homeassistant/components/nest/translations/no.json index 69d67c5b4f..dfaf33b396 100644 --- a/homeassistant/components/nest/translations/no.json +++ b/homeassistant/components/nest/translations/no.json @@ -1,19 +1,20 @@ { "config": { "abort": { - "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." + "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" }, "create_entry": { "default": "Vellykket godkjenning" }, "error": { "internal_error": "Intern feil ved validering av kode", - "invalid_pin": "Ugyldig PIN-kode", + "invalid_pin": "Ugyldig PIN kode", "timeout": "Tidsavbrudd ved validering av kode", "unknown": "Uventet feil" }, @@ -27,13 +28,17 @@ }, "link": { "data": { - "code": "PIN-kode" + "code": "PIN kode" }, - "description": "For \u00e5 koble din Nest-konto, [bekreft kontoen din]({url}). \n\nEtter bekreftelse, kopier og lim inn den oppgitte PIN koden nedenfor.", + "description": "For \u00e5 koble din Nest-konto m\u00e5 du [bekrefte kontoen din]({url}). \n\nEtter bekreftelse, kopier og lim inn den oppgitte PIN koden nedenfor.", "title": "Koble til Nest konto" }, "pick_implementation": { "title": "Velg godkjenningsmetode" + }, + "reauth_confirm": { + "description": "Nest-integrasjonen m\u00e5 godkjenne kontoen din p\u00e5 nytt", + "title": "Godkjenne integrering p\u00e5 nytt" } } }, diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 79c4276c23..6da647ac29 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -2,10 +2,18 @@ "config": { "abort": { "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", - "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o." + "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." + }, + "create_entry": { + "default": "Autenticado com sucesso" }, "error": { "internal_error": "Erro interno ao validar o c\u00f3digo", + "invalid_pin": "C\u00f3digo PIN inv\u00e1lido", "timeout": "Limite temporal ultrapassado ao validar c\u00f3digo", "unknown": "Erro desconhecido ao validar o c\u00f3digo" }, @@ -23,6 +31,9 @@ }, "description": "Para associar \u00e0 sua conta Nest, [autorizar a sua conta]({url}).\n\nAp\u00f3s a autoriza\u00e7\u00e3o, copie e cole o c\u00f3digo pin fornecido abaixo.", "title": "Associar conta Nest" + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } } diff --git a/homeassistant/components/nest/translations/ru.json b/homeassistant/components/nest/translations/ru.json index 4060808c26..4f2e895256 100644 --- a/homeassistant/components/nest/translations/ru.json +++ b/homeassistant/components/nest/translations/ru.json @@ -5,6 +5,7 @@ "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.", + "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.", "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_authorize_url_generation": "\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." }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + }, + "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\u044f \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Nest", + "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" } } }, diff --git a/homeassistant/components/nest/translations/sl.json b/homeassistant/components/nest/translations/sl.json index 4af5404c63..25660b4805 100644 --- a/homeassistant/components/nest/translations/sl.json +++ b/homeassistant/components/nest/translations/sl.json @@ -2,7 +2,9 @@ "config": { "abort": { "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", - "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla." + "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", + "reauth_successful": "Ponovna overitev je uspela.", + "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." }, "error": { "internal_error": "Notranja napaka pri preverjanju kode", @@ -23,7 +25,18 @@ }, "description": "\u010ce \u017eelite povezati svoj ra\u010dun Nest, [pooblastite svoj ra\u010dun]({url}). \n\n Po odobritvi kopirajte in prilepite podano kodo PIN.", "title": "Pove\u017eite Nest ra\u010dun" + }, + "reauth_confirm": { + "description": "Potrebna je ponovna overitev integracije" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Zaznano je gibanje", + "camera_person": "Zaznana je oseba", + "camera_sound": "Zaznan je zvok", + "doorbell_chime": "Zvonec je pritisnjen" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/th.json b/homeassistant/components/nest/translations/th.json index 797aac8240..5f14558e2b 100644 --- a/homeassistant/components/nest/translations/th.json +++ b/homeassistant/components/nest/translations/th.json @@ -5,7 +5,17 @@ "data": { "code": "Pin code" } + }, + "reauth_confirm": { + "title": "\u0e15\u0e23\u0e27\u0e08\u0e2a\u0e2d\u0e1a\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e01\u0e32\u0e23\u0e1a\u0e39\u0e23\u0e13\u0e32\u0e01\u0e32\u0e23\u0e2d\u0e35\u0e01\u0e04\u0e23\u0e31\u0e49\u0e07" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "\u0e15\u0e23\u0e27\u0e08\u0e1e\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e04\u0e25\u0e37\u0e48\u0e2d\u0e19\u0e44\u0e2b\u0e27", + "camera_person": "\u0e15\u0e23\u0e27\u0e08\u0e1e\u0e1a\u0e1a\u0e38\u0e04\u0e04\u0e25", + "camera_sound": "\u0e15\u0e23\u0e27\u0e08\u0e1e\u0e1a\u0e40\u0e2a\u0e35\u0e22\u0e07" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json new file mode 100644 index 0000000000..484cdaff6e --- /dev/null +++ b/homeassistant/components/nest/translations/tr.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "trigger_type": { + "camera_motion": "Hareket alg\u0131land\u0131", + "camera_person": "Ki\u015fi alg\u0131land\u0131", + "camera_sound": "Ses alg\u0131land\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/zh-Hant.json b/homeassistant/components/nest/translations/zh-Hant.json index 80d0f8ee66..a271ca666f 100644 --- a/homeassistant/components/nest/translations/zh-Hant.json +++ b/homeassistant/components/nest/translations/zh-Hant.json @@ -5,7 +5,8 @@ "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", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "create_entry": { @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + }, + "reauth_confirm": { + "description": "Nest \u6574\u5408\u9700\u8981\u91cd\u65b0\u8a8d\u8b49\u60a8\u7684\u5e33\u865f", + "title": "\u91cd\u65b0\u8a8d\u8b49\u6574\u5408" } } }, diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index 590082b826..eab1d9741a 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -15,6 +15,13 @@ }, "options": { "step": { + "public_weather": { + "data": { + "area_name": "Naam van het gebied", + "mode": "Berekening", + "show_on_map": "Toon op kaart" + } + }, "public_weather_areas": { "description": "Configureer openbare weersensoren." } diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index 9f18963dcb..387dbe7b26 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/netatmo/translations/pt.json b/homeassistant/components/netatmo/translations/pt.json index f9199091a8..e39ecffa8a 100644 --- a/homeassistant/components/netatmo/translations/pt.json +++ b/homeassistant/components/netatmo/translations/pt.json @@ -2,10 +2,17 @@ "config": { "abort": { "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "create_entry": { "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } } }, "options": { diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index 588675c670..e396deabb6 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -4,7 +4,7 @@ "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" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/nexia/translations/pt.json b/homeassistant/components/nexia/translations/pt.json index 4a071063d4..7953cf5625 100644 --- a/homeassistant/components/nexia/translations/pt.json +++ b/homeassistant/components/nexia/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/nexia/translations/zh-Hant.json b/homeassistant/components/nexia/translations/zh-Hant.json index 34450afc84..0dc0931afe 100644 --- a/homeassistant/components/nexia/translations/zh-Hant.json +++ b/homeassistant/components/nexia/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json index a7ad0fe1d2..8581b04099 100644 --- a/homeassistant/components/nightscout/translations/de.json +++ b/homeassistant/components/nightscout/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "unknown": "Unerwarteter Fehler" + }, "flow_title": "Nightscout", "step": { "user": { diff --git a/homeassistant/components/nightscout/translations/pt.json b/homeassistant/components/nightscout/translations/pt.json index 657ce03e54..093b777582 100644 --- a/homeassistant/components/nightscout/translations/pt.json +++ b/homeassistant/components/nightscout/translations/pt.json @@ -5,7 +5,16 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "url": "" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/zh-Hant.json b/homeassistant/components/nightscout/translations/zh-Hant.json index 5066f5a2ed..7b480bcc0f 100644 --- a/homeassistant/components/nightscout/translations/zh-Hant.json +++ b/homeassistant/components/nightscout/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -15,7 +15,7 @@ "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", + "description": "- URL\uff1aNightscout \u88dd\u7f6e\u4f4d\u5740\u3002\u4f8b\u5982\uff1ahttps://myhomeassistant.duckdns.org:5423\n- API \u5bc6\u9470\uff08\u9078\u9805\uff09\uff1a\u50c5\u65bc\u88dd\u7f6e\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/notion/translations/nl.json b/homeassistant/components/notion/translations/nl.json index 86e059df4f..4b6597725e 100644 --- a/homeassistant/components/notion/translations/nl.json +++ b/homeassistant/components/notion/translations/nl.json @@ -4,6 +4,7 @@ "already_configured": "Deze gebruikersnaam is al in gebruik." }, "error": { + "invalid_auth": "Ongeldige authenticatie", "no_devices": "Geen apparaten gevonden in account" }, "step": { diff --git a/homeassistant/components/notion/translations/pt.json b/homeassistant/components/notion/translations/pt.json index 24825307e7..e92d51b205 100644 --- a/homeassistant/components/notion/translations/pt.json +++ b/homeassistant/components/notion/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/notion/translations/zh-Hant.json b/homeassistant/components/notion/translations/zh-Hant.json index 12bc209815..865bd1dbd0 100644 --- a/homeassistant/components/notion/translations/zh-Hant.json +++ b/homeassistant/components/notion/translations/zh-Hant.json @@ -5,7 +5,7 @@ }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" + "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u88dd\u7f6e" }, "step": { "user": { diff --git a/homeassistant/components/nuheat/translations/pt.json b/homeassistant/components/nuheat/translations/pt.json index 4a071063d4..7953cf5625 100644 --- a/homeassistant/components/nuheat/translations/pt.json +++ b/homeassistant/components/nuheat/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/nuheat/translations/zh-Hant.json b/homeassistant/components/nuheat/translations/zh-Hant.json index eac51c4cf8..d04a5b165b 100644 --- a/homeassistant/components/nuheat/translations/zh-Hant.json +++ b/homeassistant/components/nuheat/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index d8585ad745..d0b55514a6 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -8,11 +8,8 @@ from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockEntity -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_PORT, CONF_TOKEN -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.service import extract_entity_ids - -from . import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN +from homeassistant.helpers import config_validation as cv, entity_platform _LOGGER = logging.getLogger(__name__) @@ -28,8 +25,6 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) NUKI_DATA = "nuki" -SERVICE_LOCK_N_GO = "lock_n_go" - ERROR_STATES = (0, 254, 255) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -40,47 +35,38 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -LOCK_N_GO_SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_UNLATCH, default=False): cv.boolean, - } -) - -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 the Nuki lock platform.""" - bridge = NukiBridge( - config[CONF_HOST], - config[CONF_TOKEN], - config[CONF_PORT], - True, - DEFAULT_TIMEOUT, + + def get_entities(): + bridge = NukiBridge( + config[CONF_HOST], + config[CONF_TOKEN], + config[CONF_PORT], + True, + DEFAULT_TIMEOUT, + ) + + entities = [NukiLockEntity(lock) for lock in bridge.locks] + entities.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) + return entities + + entities = await hass.async_add_executor_job(get_entities) + + async_add_entities(entities) + + platform = entity_platform.current_platform.get() + assert platform is not None + + platform.async_register_entity_service( + "lock_n_go", + { + vol.Optional(ATTR_UNLATCH, default=False): cv.boolean, + }, + "lock_n_go", ) - devices = [NukiLockEntity(lock) for lock in bridge.locks] - - def service_handler(service): - """Service handler for nuki services.""" - entity_ids = extract_entity_ids(hass, service) - unlatch = service.data[ATTR_UNLATCH] - - for lock in devices: - if lock.entity_id not in entity_ids: - continue - lock.lock_n_go(unlatch=unlatch) - - hass.services.register( - DOMAIN, - SERVICE_LOCK_N_GO, - service_handler, - schema=LOCK_N_GO_SERVICE_SCHEMA, - ) - - devices.extend([NukiOpenerEntity(opener) for opener in bridge.openers]) - - add_entities(devices) - class NukiDeviceEntity(LockEntity, ABC): """Representation of a Nuki device.""" @@ -172,13 +158,13 @@ class NukiLockEntity(NukiDeviceEntity): """Open the door latch.""" self._nuki_device.unlatch() - def lock_n_go(self, unlatch=False, **kwargs): + def lock_n_go(self, unlatch): """Lock and go. This will first unlock the door, then wait for 20 seconds (or another amount of time depending on the lock settings) and relock. """ - self._nuki_device.lock_n_go(unlatch, kwargs) + self._nuki_device.lock_n_go(unlatch) class NukiOpenerEntity(NukiDeviceEntity): @@ -200,3 +186,6 @@ class NukiOpenerEntity(NukiDeviceEntity): def open(self, **kwargs): """Buzz open the door.""" self._nuki_device.electric_strike_actuation() + + def lock_n_go(self, unlatch): + """Stub service.""" diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index 2fd04943e4..31a0bcd776 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -1,4 +1,5 @@ """Component to allow numeric input for platforms.""" +from abc import abstractmethod from datetime import timedelta import logging from typing import Any, Dict @@ -93,6 +94,16 @@ class NumberEntity(Entity): step /= 10.0 return step + @property + def state(self) -> float: + """Return the entity state.""" + return self.value + + @property + @abstractmethod + def value(self) -> float: + """Return the entity value to represent the entity state.""" + def set_value(self, value: float) -> None: """Set new value.""" raise NotImplementedError() diff --git a/homeassistant/components/number/reproduce_state.py b/homeassistant/components/number/reproduce_state.py new file mode 100644 index 0000000000..611744e319 --- /dev/null +++ b/homeassistant/components/number/reproduce_state.py @@ -0,0 +1,65 @@ +"""Reproduce a Number entity state.""" +import asyncio +import logging +from typing import Any, Dict, Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, + state: State, + *, + context: Optional[Context] = None, + reproduce_options: Optional[Dict[str, Any]] = None, +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + try: + float(state.state) + except ValueError: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, + states: Iterable[State], + *, + context: Optional[Context] = None, + reproduce_options: Optional[Dict[str, Any]] = None, +) -> None: + """Reproduce multiple Number states.""" + # Reproduce states in parallel. + await asyncio.gather( + *( + _async_reproduce_state( + hass, state, context=context, reproduce_options=reproduce_options + ) + for state in states + ) + ) diff --git a/homeassistant/components/nut/translations/pt.json b/homeassistant/components/nut/translations/pt.json index 5edf0b18dd..a856ef0aee 100644 --- a/homeassistant/components/nut/translations/pt.json +++ b/homeassistant/components/nut/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/nut/translations/zh-Hant.json b/homeassistant/components/nut/translations/zh-Hant.json index b75bd37958..7c65e836f9 100644 --- a/homeassistant/components/nut/translations/zh-Hant.json +++ b/homeassistant/components/nut/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/nws/strings.json b/homeassistant/components/nws/strings.json index 0f0bdcf4a1..0f119e7c2e 100644 --- a/homeassistant/components/nws/strings.json +++ b/homeassistant/components/nws/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station.", + "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station. For now, an API Key can be anything. It is recommended to use a valid email address.", "title": "Connect to the National Weather Service", "data": { "api_key": "[%key:common::config_flow::data::api_key%]", diff --git a/homeassistant/components/nws/translations/ca.json b/homeassistant/components/nws/translations/ca.json index e012d68a10..4d6e23022a 100644 --- a/homeassistant/components/nws/translations/ca.json +++ b/homeassistant/components/nws/translations/ca.json @@ -15,7 +15,7 @@ "longitude": "Longitud", "station": "Codi d'estaci\u00f3 METAR" }, - "description": "Si no s'especifica un codi d'estaci\u00f3 METAR, la latitud i longitud s'utilitzaran per trobar l'estaci\u00f3 m\u00e9s propera.", + "description": "Si no s'especifica un codi d'estaci\u00f3 METAR, s'utilitzaran la latitud i longitud per trobar l'estaci\u00f3 m\u00e9s propera. De moment, la clau d'API pot ser qualsevol cosa. Es recomana utilitzar una adre\u00e7a de correu electr\u00f2nic v\u00e0lida.", "title": "Connexi\u00f3 amb el Servei Meteorol\u00f2gic Nacional (USA)" } } diff --git a/homeassistant/components/nws/translations/cs.json b/homeassistant/components/nws/translations/cs.json index f2450bf6f6..2c8402a716 100644 --- a/homeassistant/components/nws/translations/cs.json +++ b/homeassistant/components/nws/translations/cs.json @@ -15,7 +15,7 @@ "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka", "station": "K\u00f3d stanice METAR" }, - "description": "Pokud nen\u00ed zad\u00e1n k\u00f3d stanice METAR, pou\u017eije se zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka a d\u00e9lka k vyhled\u00e1n\u00ed nejbli\u017e\u0161\u00ed stanice.", + "description": "Pokud nen\u00ed zad\u00e1n k\u00f3d stanice METAR, pou\u017eije se zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka a d\u00e9lka k vyhled\u00e1n\u00ed nejbli\u017e\u0161\u00ed stanice. Prozat\u00edm m\u016f\u017ee b\u00fdt kl\u00ed\u010d API cokoli. Doporu\u010duje se pou\u017e\u00edt platnou e-mailovou adresu.", "title": "P\u0159ipojen\u00ed k National Weather Service" } } diff --git a/homeassistant/components/nws/translations/en.json b/homeassistant/components/nws/translations/en.json index 04cb13bf5e..211f35d62c 100644 --- a/homeassistant/components/nws/translations/en.json +++ b/homeassistant/components/nws/translations/en.json @@ -15,7 +15,7 @@ "longitude": "Longitude", "station": "METAR station code" }, - "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station.", + "description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station. For now, an API Key can be anything. It is recommended to use a valid email address.", "title": "Connect to the National Weather Service" } } diff --git a/homeassistant/components/nws/translations/et.json b/homeassistant/components/nws/translations/et.json index 4ef73de723..3fe3d33793 100644 --- a/homeassistant/components/nws/translations/et.json +++ b/homeassistant/components/nws/translations/et.json @@ -15,7 +15,7 @@ "longitude": "Pikkuskraad", "station": "METAR jaamakood" }, - "description": "Kui METAR-i jaamakoodi pole m\u00e4\u00e4ratud, kasutatakse l\u00e4hima jaama leidmiseks laius- ja pikkuskraadi.", + "description": "Kui METAR-i jaamakoodi pole m\u00e4\u00e4ratud, kasutatakse l\u00e4hima jaama leidmiseks laius- ja pikkuskraadi. API v\u00f5ti on hetkel suvaline. Sovitatav on kasutada kehtivat e-kirja aadressi.", "title": "\u00dchendu riikliku ilmateenistusega (USA)" } } diff --git a/homeassistant/components/nws/translations/it.json b/homeassistant/components/nws/translations/it.json index 827d8078b5..3b651ce82b 100644 --- a/homeassistant/components/nws/translations/it.json +++ b/homeassistant/components/nws/translations/it.json @@ -15,7 +15,7 @@ "longitude": "Logitudine", "station": "Codice stazione METAR" }, - "description": "Se non viene specificato un codice di stazione METAR, la latitudine e la longitudine verranno utilizzate per trovare la stazione pi\u00f9 vicina.", + "description": "Se non \u00e8 specificato un codice stazione METAR, la latitudine e la longitudine saranno utilizzate per trovare la stazione pi\u00f9 vicina. Per ora, una chiave API pu\u00f2 essere qualsiasi cosa. Si consiglia di utilizzare un indirizzo e-mail valido.", "title": "Collegati al Servizio Meteorologico Nazionale" } } diff --git a/homeassistant/components/nws/translations/no.json b/homeassistant/components/nws/translations/no.json index d9a17545c4..556e9b4136 100644 --- a/homeassistant/components/nws/translations/no.json +++ b/homeassistant/components/nws/translations/no.json @@ -15,7 +15,7 @@ "longitude": "Lengdegrad", "station": "METAR stasjonskode" }, - "description": "Hvis en METAR-stasjonskode ikke er spesifisert, vil breddegrad og lengdegrad brukes til \u00e5 finne den n\u00e6rmeste stasjonen.", + "description": "Hvis en METAR-stasjonskode ikke er spesifisert, vil breddegrad og lengdegrad bli brukt til \u00e5 finne n\u00e6rmeste stasjon. For n\u00e5 kan en API-n\u00f8kkel v\u00e6re hva som helst. Det anbefales \u00e5 bruke en gyldig e-postadresse.", "title": "Koble til National Weather Service" } } diff --git a/homeassistant/components/nws/translations/pl.json b/homeassistant/components/nws/translations/pl.json index 2665a5c5a8..7d0bce9ff1 100644 --- a/homeassistant/components/nws/translations/pl.json +++ b/homeassistant/components/nws/translations/pl.json @@ -15,7 +15,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "station": "Kod stacji METAR" }, - "description": "Je\u015bli nie podasz kodu stacji METAR, do znalezienia najbli\u017cszej stacji zostan\u0105 u\u017cyte wsp\u00f3\u0142rz\u0119dne geograficzne.", + "description": "Je\u015bli nie podasz kodu stacji METAR, do znalezienia najbli\u017cszej stacji zostan\u0105 u\u017cyte wsp\u00f3\u0142rz\u0119dne geograficzne. Na razie, kluczem mo\u017ce by\u0107 cokolwiek. Zaleca si\u0119 u\u017cycie prawid\u0142owego adresu e-mail.", "title": "Po\u0142\u0105czenie z National Weather Service" } } diff --git a/homeassistant/components/nws/translations/ru.json b/homeassistant/components/nws/translations/ru.json index 926e936a59..bc600f8428 100644 --- a/homeassistant/components/nws/translations/ru.json +++ b/homeassistant/components/nws/translations/ru.json @@ -15,7 +15,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "station": "\u041a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 METAR" }, - "description": "\u0415\u0441\u043b\u0438 \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 METAR \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0448\u0438\u0440\u043e\u0442\u0430 \u0438 \u0434\u043e\u043b\u0433\u043e\u0442\u0430.", + "description": "\u0415\u0441\u043b\u0438 \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 METAR \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0434\u043b\u044f \u043f\u043e\u0438\u0441\u043a\u0430 \u0431\u043b\u0438\u0436\u0430\u0439\u0448\u0435\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0448\u0438\u0440\u043e\u0442\u0430 \u0438 \u0434\u043e\u043b\u0433\u043e\u0442\u0430. \u041d\u0430 \u0434\u0430\u043d\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043b\u044e\u0431\u044b\u043c. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b.", "title": "National Weather Service" } } diff --git a/homeassistant/components/nws/translations/zh-Hant.json b/homeassistant/components/nws/translations/zh-Hant.json index 067234c7b5..c3abf6ceba 100644 --- a/homeassistant/components/nws/translations/zh-Hant.json +++ b/homeassistant/components/nws/translations/zh-Hant.json @@ -15,7 +15,7 @@ "longitude": "\u7d93\u5ea6", "station": "METAR \u6a5f\u5834\u4ee3\u78bc" }, - "description": "\u5047\u5982\u672a\u6307\u5b9a METAR \u6a5f\u5834\u4ee3\u78bc\uff0c\u5c07\u6703\u4f7f\u7528\u7d93\u7def\u5ea6\u8cc7\u8a0a\u5c0b\u627e\u6700\u8fd1\u7684\u6a5f\u5834\u3002", + "description": "\u5047\u5982\u672a\u6307\u5b9a METAR \u6a5f\u5834\u4ee3\u78bc\uff0c\u5c07\u6703\u4f7f\u7528\u7d93\u7def\u5ea6\u8cc7\u8a0a\u5c0b\u627e\u6700\u8fd1\u7684\u6a5f\u5834\u3002\u76ee\u524d\uff0cAPI \u5bc6\u9470\u53ef\u8f38\u5165\u4efb\u4f55\u8cc7\u8a0a\uff0c\u5efa\u8b70\u70ba\u6709\u6548\u5730\u96fb\u5b50\u90f5\u4ef6\u4f4d\u5740\u3002", "title": "\u9023\u7dda\u81f3\u7f8e\u570b\u570b\u5bb6\u6c23\u8c61\u5c40\u670d\u52d9" } } diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json index 2ebae1083e..018f3870c5 100644 --- a/homeassistant/components/nzbget/translations/de.json +++ b/homeassistant/components/nzbget/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "unknown": "Unerwarteter Fehler" + }, "flow_title": "NZBGet: {name}", "step": { "user": { diff --git a/homeassistant/components/nzbget/translations/nl.json b/homeassistant/components/nzbget/translations/nl.json index cc7d8071c2..f5f1bfd39e 100644 --- a/homeassistant/components/nzbget/translations/nl.json +++ b/homeassistant/components/nzbget/translations/nl.json @@ -21,5 +21,14 @@ "title": "Maak verbinding met NZBGet" } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie (seconden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/zh-Hant.json b/homeassistant/components/nzbget/translations/zh-Hant.json index 7858092db4..26fd5f3411 100644 --- a/homeassistant/components/nzbget/translations/zh-Hant.json +++ b/homeassistant/components/nzbget/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 6f39806287..3821567570 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/omnilogic/translations/nl.json b/homeassistant/components/omnilogic/translations/nl.json index 1127dc941e..5189795ec9 100644 --- a/homeassistant/components/omnilogic/translations/nl.json +++ b/homeassistant/components/omnilogic/translations/nl.json @@ -16,5 +16,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "Polling-interval (in seconden)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/pt.json b/homeassistant/components/omnilogic/translations/pt.json new file mode 100644 index 0000000000..3e10b97777 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/zh-Hant.json b/homeassistant/components/omnilogic/translations/zh-Hant.json index 99b5a46570..c2c39e00d6 100644 --- a/homeassistant/components/omnilogic/translations/zh-Hant.json +++ b/homeassistant/components/omnilogic/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/onewire/onewirehub.py b/homeassistant/components/onewire/onewirehub.py index 3b715eed0d..09a3235377 100644 --- a/homeassistant/components/onewire/onewirehub.py +++ b/homeassistant/components/onewire/onewirehub.py @@ -11,6 +11,11 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import CONF_MOUNT_DIR, CONF_TYPE_OWSERVER, CONF_TYPE_SYSBUS +DEVICE_COUPLERS = { + # Family : [branches] + "1F": ["aux", "main"] +} + class OneWireHub: """Hub to communicate with SysBus or OWServer.""" @@ -62,17 +67,24 @@ class OneWireHub: ) return self.devices - def _discover_devices_owserver(self): + def _discover_devices_owserver(self, path="/"): """Discover all owserver devices.""" devices = [] - for device_path in self.owproxy.dir(): - devices.append( - { - "path": device_path, - "family": self.owproxy.read(f"{device_path}family").decode(), - "type": self.owproxy.read(f"{device_path}type").decode(), - } - ) + for device_path in self.owproxy.dir(path): + device_family = self.owproxy.read(f"{device_path}family").decode() + device_type = self.owproxy.read(f"{device_path}type").decode() + device_branches = DEVICE_COUPLERS.get(device_family) + if device_branches: + for branch in device_branches: + devices += self._discover_devices_owserver(f"{device_path}{branch}") + else: + devices.append( + { + "path": device_path, + "family": device_family, + "type": device_type, + } + ) return devices diff --git a/homeassistant/components/onewire/translations/nl.json b/homeassistant/components/onewire/translations/nl.json index 8b2702b670..ae155ccf2c 100644 --- a/homeassistant/components/onewire/translations/nl.json +++ b/homeassistant/components/onewire/translations/nl.json @@ -2,6 +2,20 @@ "config": { "abort": { "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "invalid_path": "Directory niet gevonden." + }, + "step": { + "owserver": { + "title": "Owserver-details instellen" + }, + "user": { + "data": { + "type": "Verbindingstype" + }, + "title": "Stel 1-Wire in" + } } } } \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/pt.json b/homeassistant/components/onewire/translations/pt.json new file mode 100644 index 0000000000..bd1e14729e --- /dev/null +++ b/homeassistant/components/onewire/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "owserver": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/zh-Hant.json b/homeassistant/components/onewire/translations/zh-Hant.json index acafb6eee4..9c606534a5 100644 --- a/homeassistant/components/onewire/translations/zh-Hant.json +++ b/homeassistant/components/onewire/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_path": "\u672a\u627e\u5230\u8a2d\u5099\u3002" + "invalid_path": "\u672a\u627e\u5230\u88dd\u7f6e\u3002" }, "step": { "owserver": { diff --git a/homeassistant/components/onvif/translations/pt.json b/homeassistant/components/onvif/translations/pt.json index cfc92a512d..c3662a032a 100644 --- a/homeassistant/components/onvif/translations/pt.json +++ b/homeassistant/components/onvif/translations/pt.json @@ -7,6 +7,9 @@ "no_mac": "N\u00e3o foi poss\u00edvel configurar o ID unico para o dispositivo ONVIF.", "onvif_error": "Erro ao configurar o dispositivo ONVIF. Verifique os logs para obter mais informa\u00e7\u00f5es." }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/onvif/translations/zh-Hant.json b/homeassistant/components/onvif/translations/zh-Hant.json index 6541d8accd..b21982fede 100644 --- a/homeassistant/components/onvif/translations/zh-Hant.json +++ b/homeassistant/components/onvif/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "no_h264": "\u8a72\u8a2d\u5099\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5099\u8a2d\u5b9a\u3002", - "no_mac": "\u7121\u6cd5\u70ba ONVIF \u8a2d\u5099\u8a2d\u5b9a\u552f\u4e00 ID\u3002", - "onvif_error": "\u8a2d\u5b9a ONVIF \u8a2d\u5099\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" + "no_h264": "\u8a72\u88dd\u7f6e\u4e0d\u652f\u63f4 H264 \u4e32\u6d41\uff0c\u8acb\u6aa2\u67e5\u88dd\u7f6e\u8a2d\u5b9a\u3002", + "no_mac": "\u7121\u6cd5\u70ba ONVIF \u88dd\u7f6e\u8a2d\u5b9a\u552f\u4e00 ID\u3002", + "onvif_error": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e\u932f\u8aa4\uff0c\u8acb\u53c3\u95b1\u65e5\u8a8c\u4ee5\u7372\u5f97\u66f4\u8a73\u7d30\u8cc7\u8a0a\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -27,9 +27,9 @@ }, "device": { "data": { - "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 ONVIF \u8a2d\u5099" + "host": "\u9078\u64c7\u6240\u63a2\u7d22\u5230\u7684 ONVIF \u88dd\u7f6e" }, - "title": "\u9078\u64c7 ONVIF \u8a2d\u5099" + "title": "\u9078\u64c7 ONVIF \u88dd\u7f6e" }, "manual_input": { "data": { @@ -37,11 +37,11 @@ "name": "\u540d\u7a31", "port": "\u901a\u8a0a\u57e0" }, - "title": "\u8a2d\u5b9a ONVIF \u8a2d\u5099" + "title": "\u8a2d\u5b9a ONVIF \u88dd\u7f6e" }, "user": { - "description": "\u9ede\u4e0b\u50b3\u9001\u5f8c\u3001\u5c07\u6703\u641c\u5c0b\u7db2\u8def\u4e2d\u652f\u63f4 Profile S \u7684 ONVIF \u8a2d\u5099\u3002\n\n\u67d0\u4e9b\u5ee0\u5546\u9810\u8a2d\u7684\u6a21\u5f0f\u70ba ONVIF \u95dc\u9589\u6a21\u5f0f\uff0c\u8acb\u518d\u6b21\u78ba\u8a8d\u651d\u5f71\u6a5f\u5df2\u7d93\u958b\u555f ONVIF\u3002", - "title": "ONVIF \u8a2d\u5099\u8a2d\u5b9a" + "description": "\u9ede\u4e0b\u50b3\u9001\u5f8c\u3001\u5c07\u6703\u641c\u5c0b\u7db2\u8def\u4e2d\u652f\u63f4 Profile S \u7684 ONVIF \u88dd\u7f6e\u3002\n\n\u67d0\u4e9b\u5ee0\u5546\u9810\u8a2d\u7684\u6a21\u5f0f\u70ba ONVIF \u95dc\u9589\u6a21\u5f0f\uff0c\u8acb\u518d\u6b21\u78ba\u8a8d\u651d\u5f71\u6a5f\u5df2\u7d93\u958b\u555f ONVIF\u3002", + "title": "ONVIF \u88dd\u7f6e\u8a2d\u5b9a" } } }, @@ -52,7 +52,7 @@ "extra_arguments": "\u984d\u5916 FFMPEG \u53c3\u6578", "rtsp_transport": "RTSP \u50b3\u8f38\u5354\u5b9a" }, - "title": "ONVIF \u8a2d\u5099\u9078\u9805" + "title": "ONVIF \u88dd\u7f6e\u9078\u9805" } } } diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 3a94215a7b..98fbf2d961 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -3,5 +3,5 @@ "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": ["openhomedevice==0.7.2"], - "codeowners": [] + "codeowners": ["@bazwilliams"] } diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 49edf8e7d0..06132e83e8 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -117,14 +117,20 @@ class OpenSkySensor(Entity): for flight in flights: if flight in metadata: altitude = metadata[flight].get(ATTR_ALTITUDE) + longitude = metadata[flight].get(ATTR_LONGITUDE) + latitude = metadata[flight].get(ATTR_LATITUDE) else: # Assume Flight has landed if missing. altitude = 0 + longitude = None + latitude = None data = { ATTR_CALLSIGN: flight, ATTR_ALTITUDE: altitude, ATTR_SENSOR: self._name, + ATTR_LONGITUDE: longitude, + ATTR_LATITUDE: latitude, } self._hass.bus.fire(event, data) diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 9e3c4d4122..a896b37a26 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -1,14 +1,22 @@ """Support for OpenTherm Gateway binary sensors.""" import logging +from pprint import pformat from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN -from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW +from .const import ( + BINARY_SENSOR_INFO, + DATA_GATEWAYS, + DATA_OPENTHERM_GW, + DEPRECATED_BINARY_SENSOR_SOURCE_LOOKUP, + TRANSLATE_SOURCE, +) _LOGGER = logging.getLogger(__name__) @@ -16,16 +24,51 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway binary sensors.""" sensors = [] + deprecated_sensors = [] + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + ent_reg = await async_get_registry(hass) for var, info in BINARY_SENSOR_INFO.items(): device_class = info[0] friendly_name_format = info[1] - sensors.append( - OpenThermBinarySensor( - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], - var, - device_class, - friendly_name_format, + status_sources = info[2] + + for source in status_sources: + sensors.append( + OpenThermBinarySensor( + gw_dev, + var, + source, + device_class, + friendly_name_format, + ) ) + + old_style_entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ) + old_ent = ent_reg.async_get(old_style_entity_id) + if old_ent and old_ent.config_entry_id == config_entry.entry_id: + if old_ent.disabled: + ent_reg.async_remove(old_style_entity_id) + else: + deprecated_sensors.append( + DeprecatedOpenThermBinarySensor( + gw_dev, + var, + device_class, + friendly_name_format, + ) + ) + + sensors.extend(deprecated_sensors) + + if deprecated_sensors: + _LOGGER.warning( + "The following binary_sensor entities are deprecated and may " + "no longer behave as expected. They will be removed in a " + "future version. You can force removal of these entities by " + "disabling them and restarting Home Assistant.\n%s", + pformat([s.entity_id for s in deprecated_sensors]), ) async_add_entities(sensors) @@ -34,15 +77,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class OpenThermBinarySensor(BinarySensorEntity): """Represent an OpenTherm Gateway binary sensor.""" - def __init__(self, gw_dev, var, device_class, friendly_name_format): + def __init__(self, gw_dev, var, source, device_class, friendly_name_format): """Initialize the binary sensor.""" self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ENTITY_ID_FORMAT, f"{var}_{source}_{gw_dev.gw_id}", hass=gw_dev.hass ) self._gateway = gw_dev self._var = var + self._source = source self._state = None self._device_class = device_class + if TRANSLATE_SOURCE[source] is not None: + friendly_name_format = ( + f"{friendly_name_format} ({TRANSLATE_SOURCE[source]})" + ) self._friendly_name = friendly_name_format.format(gw_dev.name) self._unsub_updates = None @@ -73,7 +121,7 @@ class OpenThermBinarySensor(BinarySensorEntity): @callback def receive_report(self, status): """Handle status updates from the component.""" - state = status.get(self._var) + state = status[self._source].get(self._var) self._state = None if state is None else bool(state) self.async_write_ha_state() @@ -96,7 +144,7 @@ class OpenThermBinarySensor(BinarySensorEntity): @property def unique_id(self): """Return a unique ID.""" - return f"{self._gateway.gw_id}-{self._var}" + return f"{self._gateway.gw_id}-{self._source}-{self._var}" @property def is_on(self): @@ -112,3 +160,26 @@ class OpenThermBinarySensor(BinarySensorEntity): def should_poll(self): """Return False because entity pushes its state.""" return False + + +class DeprecatedOpenThermBinarySensor(OpenThermBinarySensor): + """Represent a deprecated OpenTherm Gateway Binary Sensor.""" + + # pylint: disable=super-init-not-called + def __init__(self, gw_dev, var, device_class, friendly_name_format): + """Initialize the binary sensor.""" + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ) + self._gateway = gw_dev + self._var = var + self._source = DEPRECATED_BINARY_SENSOR_SOURCE_LOOKUP[var] + self._state = None + self._device_class = device_class + self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 237733e687..8ec536e733 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -101,10 +101,10 @@ class OpenThermClimate(ClimateEntity): @callback def receive_report(self, status): """Receive and handle a new report from the Gateway.""" - self._available = bool(status) - ch_active = status.get(gw_vars.DATA_SLAVE_CH_ACTIVE) - flame_on = status.get(gw_vars.DATA_SLAVE_FLAME_ON) - cooling_active = status.get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) + self._available = status != gw_vars.DEFAULT_STATUS + ch_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_CH_ACTIVE) + flame_on = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_FLAME_ON) + cooling_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_COOLING_ACTIVE) if ch_active and flame_on: self._current_operation = CURRENT_HVAC_HEAT self._hvac_mode = HVAC_MODE_HEAT @@ -114,8 +114,10 @@ class OpenThermClimate(ClimateEntity): else: self._current_operation = CURRENT_HVAC_IDLE - self._current_temperature = status.get(gw_vars.DATA_ROOM_TEMP) - temp_upd = status.get(gw_vars.DATA_ROOM_SETPOINT) + self._current_temperature = status[gw_vars.THERMOSTAT].get( + gw_vars.DATA_ROOM_TEMP + ) + temp_upd = status[gw_vars.THERMOSTAT].get(gw_vars.DATA_ROOM_SETPOINT) if self._target_temperature != temp_upd: self._new_target_temperature = None @@ -123,14 +125,14 @@ class OpenThermClimate(ClimateEntity): # GPIO mode 5: 0 == Away # GPIO mode 6: 1 == Away - gpio_a_state = status.get(gw_vars.OTGW_GPIO_A) + gpio_a_state = status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_A) if gpio_a_state == 5: self._away_mode_a = 0 elif gpio_a_state == 6: self._away_mode_a = 1 else: self._away_mode_a = None - gpio_b_state = status.get(gw_vars.OTGW_GPIO_B) + gpio_b_state = status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_B) if gpio_b_state == 5: self._away_mode_b = 0 elif gpio_b_state == 6: @@ -139,11 +141,11 @@ class OpenThermClimate(ClimateEntity): self._away_mode_b = None if self._away_mode_a is not None: self._away_state_a = ( - status.get(gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a + status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_A_STATE) == self._away_mode_a ) if self._away_mode_b is not None: self._away_state_b = ( - status.get(gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b + status[gw_vars.OTGW].get(gw_vars.OTGW_GPIO_B_STATE) == self._away_mode_b ) self.async_write_ha_state() diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 59f14ab2ee..8da530bebd 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -54,7 +54,7 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): otgw = pyotgw.pyotgw() status = await otgw.connect(self.hass.loop, device) await otgw.disconnect() - return status.get(gw_vars.OTGW_ABOUT) + return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) try: res = await asyncio.wait_for(test_connection(), timeout=10) diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 3ff1577c43..2c3e2f7071 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -40,244 +40,599 @@ SERVICE_SET_MAX_MOD = "set_max_modulation" SERVICE_SET_OAT = "set_outside_temperature" SERVICE_SET_SB_TEMP = "set_setback_temperature" +TRANSLATE_SOURCE = { + gw_vars.BOILER: "Boiler", + gw_vars.OTGW: None, + gw_vars.THERMOSTAT: "Thermostat", +} + UNIT_KW = "kW" UNIT_L_MIN = f"L/{TIME_MINUTES}" BINARY_SENSOR_INFO = { - # [device_class, friendly_name format] - gw_vars.DATA_MASTER_CH_ENABLED: [None, "Thermostat Central Heating Enabled {}"], - gw_vars.DATA_MASTER_DHW_ENABLED: [None, "Thermostat Hot Water Enabled {}"], - gw_vars.DATA_MASTER_COOLING_ENABLED: [None, "Thermostat Cooling Enabled {}"], + # [device_class, friendly_name format, [status source, ...]] + gw_vars.DATA_MASTER_CH_ENABLED: [ + None, + "Thermostat Central Heating {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_DHW_ENABLED: [ + None, + "Thermostat Hot Water {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_COOLING_ENABLED: [ + None, + "Thermostat Cooling {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], gw_vars.DATA_MASTER_OTC_ENABLED: [ None, - "Thermostat Outside Temperature Correction Enabled {}", + "Thermostat Outside Temperature Correction {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_CH2_ENABLED: [ + None, + "Thermostat Central Heating 2 {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_FAULT_IND: [ + DEVICE_CLASS_PROBLEM, + "Boiler Fault {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_MASTER_CH2_ENABLED: [None, "Thermostat Central Heating 2 Enabled {}"], - gw_vars.DATA_SLAVE_FAULT_IND: [DEVICE_CLASS_PROBLEM, "Boiler Fault Indication {}"], gw_vars.DATA_SLAVE_CH_ACTIVE: [ DEVICE_CLASS_HEAT, - "Boiler Central Heating Status {}", + "Boiler Central Heating {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_DHW_ACTIVE: [ + DEVICE_CLASS_HEAT, + "Boiler Hot Water {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_FLAME_ON: [ + DEVICE_CLASS_HEAT, + "Boiler Flame {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_COOLING_ACTIVE: [ + DEVICE_CLASS_COLD, + "Boiler Cooling {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_DHW_ACTIVE: [DEVICE_CLASS_HEAT, "Boiler Hot Water Status {}"], - gw_vars.DATA_SLAVE_FLAME_ON: [DEVICE_CLASS_HEAT, "Boiler Flame Status {}"], - gw_vars.DATA_SLAVE_COOLING_ACTIVE: [DEVICE_CLASS_COLD, "Boiler Cooling Status {}"], gw_vars.DATA_SLAVE_CH2_ACTIVE: [ DEVICE_CLASS_HEAT, - "Boiler Central Heating 2 Status {}", + "Boiler Central Heating 2 {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DIAG_IND: [ DEVICE_CLASS_PROBLEM, - "Boiler Diagnostics Indication {}", + "Boiler Diagnostics {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_DHW_PRESENT: [ + None, + "Boiler Hot Water Present {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_CONTROL_TYPE: [ + None, + "Boiler Control Type {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [ + None, + "Boiler Cooling Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_DHW_CONFIG: [ + None, + "Boiler Hot Water Configuration {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [ + None, + "Boiler Pump Commands Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_CH2_PRESENT: [ + None, + "Boiler Central Heating 2 Present {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_DHW_PRESENT: [None, "Boiler Hot Water Present {}"], - gw_vars.DATA_SLAVE_CONTROL_TYPE: [None, "Boiler Control Type {}"], - gw_vars.DATA_SLAVE_COOLING_SUPPORTED: [None, "Boiler Cooling Support {}"], - gw_vars.DATA_SLAVE_DHW_CONFIG: [None, "Boiler Hot Water Configuration {}"], - gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: [None, "Boiler Pump Commands Support {}"], - gw_vars.DATA_SLAVE_CH2_PRESENT: [None, "Boiler Central Heating 2 Present {}"], gw_vars.DATA_SLAVE_SERVICE_REQ: [ DEVICE_CLASS_PROBLEM, "Boiler Service Required {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_REMOTE_RESET: [ + None, + "Boiler Remote Reset Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_REMOTE_RESET: [None, "Boiler Remote Reset Support {}"], gw_vars.DATA_SLAVE_LOW_WATER_PRESS: [ DEVICE_CLASS_PROBLEM, "Boiler Low Water Pressure {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_GAS_FAULT: [ + DEVICE_CLASS_PROBLEM, + "Boiler Gas Fault {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_GAS_FAULT: [DEVICE_CLASS_PROBLEM, "Boiler Gas Fault {}"], gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: [ DEVICE_CLASS_PROBLEM, "Boiler Air Pressure Fault {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_WATER_OVERTEMP: [ DEVICE_CLASS_PROBLEM, "Boiler Water Overtemperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_REMOTE_TRANSFER_DHW: [ None, "Remote Hot Water Setpoint Transfer Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: [ None, "Remote Maximum Central Heating Setpoint Write Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_REMOTE_RW_DHW: [ + None, + "Remote Hot Water Setpoint Write Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_REMOTE_RW_DHW: [None, "Remote Hot Water Setpoint Write Support {}"], gw_vars.DATA_REMOTE_RW_MAX_CH: [ None, "Remote Central Heating Setpoint Write Support {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_ROVRD_MAN_PRIO: [None, "Remote Override Manual Change Priority {}"], - gw_vars.DATA_ROVRD_AUTO_PRIO: [None, "Remote Override Program Change Priority {}"], - gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A State {}"], - gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B State {}"], - gw_vars.OTGW_IGNORE_TRANSITIONS: [None, "Gateway Ignore Transitions {}"], - gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte {}"], + gw_vars.DATA_ROVRD_MAN_PRIO: [ + None, + "Remote Override Manual Change Priority {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_ROVRD_AUTO_PRIO: [ + None, + "Remote Override Program Change Priority {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.OTGW_GPIO_A_STATE: [None, "Gateway GPIO A {}", [gw_vars.OTGW]], + gw_vars.OTGW_GPIO_B_STATE: [None, "Gateway GPIO B {}", [gw_vars.OTGW]], + gw_vars.OTGW_IGNORE_TRANSITIONS: [ + None, + "Gateway Ignore Transitions {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_OVRD_HB: [None, "Gateway Override High Byte {}", [gw_vars.OTGW]], } SENSOR_INFO = { - # [device_class, unit, friendly_name] + # [device_class, unit, friendly_name, [status source, ...]] gw_vars.DATA_CONTROL_SETPOINT: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_MEMBERID: [ + None, + None, + "Thermostat Member ID {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_MEMBERID: [ + None, + None, + "Boiler Member ID {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_OEM_FAULT: [ + None, + None, + "Boiler OEM Fault Code {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_COOLING_CONTROL: [ + None, + PERCENTAGE, + "Cooling Control Signal {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_MASTER_MEMBERID: [None, None, "Thermostat Member ID {}"], - gw_vars.DATA_SLAVE_MEMBERID: [None, None, "Boiler Member ID {}"], - gw_vars.DATA_SLAVE_OEM_FAULT: [None, None, "Boiler OEM Fault Code {}"], - gw_vars.DATA_COOLING_CONTROL: [None, PERCENTAGE, "Cooling Control Signal {}"], gw_vars.DATA_CONTROL_SETPOINT_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Control Setpoint 2 {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_SETPOINT_OVRD: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint Override {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD: [ None, PERCENTAGE, "Boiler Maximum Relative Modulation {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_MAX_CAPACITY: [ + None, + UNIT_KW, + "Boiler Maximum Capacity {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_SLAVE_MAX_CAPACITY: [None, UNIT_KW, "Boiler Maximum Capacity {}"], gw_vars.DATA_SLAVE_MIN_MOD_LEVEL: [ None, PERCENTAGE, "Boiler Minimum Modulation Level {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_SETPOINT: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_REL_MOD_LEVEL: [ + None, + PERCENTAGE, + "Relative Modulation Level {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_REL_MOD_LEVEL: [None, PERCENTAGE, "Relative Modulation Level {}"], gw_vars.DATA_CH_WATER_PRESS: [ None, PRESSURE_BAR, "Central Heating Water Pressure {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_FLOW_RATE: [ + None, + UNIT_L_MIN, + "Hot Water Flow Rate {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_DHW_FLOW_RATE: [None, UNIT_L_MIN, "Hot Water Flow Rate {}"], gw_vars.DATA_ROOM_SETPOINT_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Setpoint 2 {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_ROOM_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Room Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_CH_WATER_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Central Heating Water Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_OUTSIDE_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Outside Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_RETURN_WATER_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Return Water Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SOLAR_STORAGE_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Solar Storage Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SOLAR_COLL_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Solar Collector Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_CH_WATER_TEMP_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Central Heating 2 Water Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_TEMP_2: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water 2 Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_EXHAUST_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Exhaust Temperature {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DHW_MAX_SETP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Maximum Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_DHW_MIN_SETP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Minimum Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_CH_MAX_SETP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Boiler Maximum Central Heating Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_SLAVE_CH_MIN_SETP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Boiler Minimum Central Heating Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_DHW_SETPOINT: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Hot Water Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], gw_vars.DATA_MAX_CH_SETPOINT: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Maximum Central Heating Setpoint {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], ], - gw_vars.DATA_OEM_DIAG: [None, None, "OEM Diagnostic Code {}"], - gw_vars.DATA_TOTAL_BURNER_STARTS: [None, None, "Total Burner Starts {}"], - gw_vars.DATA_CH_PUMP_STARTS: [None, None, "Central Heating Pump Starts {}"], - gw_vars.DATA_DHW_PUMP_STARTS: [None, None, "Hot Water Pump Starts {}"], - gw_vars.DATA_DHW_BURNER_STARTS: [None, None, "Hot Water Burner Starts {}"], - gw_vars.DATA_TOTAL_BURNER_HOURS: [None, TIME_HOURS, "Total Burner Hours {}"], - gw_vars.DATA_CH_PUMP_HOURS: [None, TIME_HOURS, "Central Heating Pump Hours {}"], - gw_vars.DATA_DHW_PUMP_HOURS: [None, TIME_HOURS, "Hot Water Pump Hours {}"], - gw_vars.DATA_DHW_BURNER_HOURS: [None, TIME_HOURS, "Hot Water Burner Hours {}"], - gw_vars.DATA_MASTER_OT_VERSION: [None, None, "Thermostat OpenTherm Version {}"], - gw_vars.DATA_SLAVE_OT_VERSION: [None, None, "Boiler OpenTherm Version {}"], - gw_vars.DATA_MASTER_PRODUCT_TYPE: [None, None, "Thermostat Product Type {}"], - gw_vars.DATA_MASTER_PRODUCT_VERSION: [None, None, "Thermostat Product Version {}"], - gw_vars.DATA_SLAVE_PRODUCT_TYPE: [None, None, "Boiler Product Type {}"], - gw_vars.DATA_SLAVE_PRODUCT_VERSION: [None, None, "Boiler Product Version {}"], - gw_vars.OTGW_MODE: [None, None, "Gateway/Monitor Mode {}"], - gw_vars.OTGW_DHW_OVRD: [None, None, "Gateway Hot Water Override Mode {}"], - gw_vars.OTGW_ABOUT: [None, None, "Gateway Firmware Version {}"], - gw_vars.OTGW_BUILD: [None, None, "Gateway Firmware Build {}"], - gw_vars.OTGW_CLOCKMHZ: [None, None, "Gateway Clock Speed {}"], - gw_vars.OTGW_LED_A: [None, None, "Gateway LED A Mode {}"], - gw_vars.OTGW_LED_B: [None, None, "Gateway LED B Mode {}"], - gw_vars.OTGW_LED_C: [None, None, "Gateway LED C Mode {}"], - gw_vars.OTGW_LED_D: [None, None, "Gateway LED D Mode {}"], - gw_vars.OTGW_LED_E: [None, None, "Gateway LED E Mode {}"], - gw_vars.OTGW_LED_F: [None, None, "Gateway LED F Mode {}"], - gw_vars.OTGW_GPIO_A: [None, None, "Gateway GPIO A Mode {}"], - gw_vars.OTGW_GPIO_B: [None, None, "Gateway GPIO B Mode {}"], + gw_vars.DATA_OEM_DIAG: [ + None, + None, + "OEM Diagnostic Code {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_TOTAL_BURNER_STARTS: [ + None, + None, + "Total Burner Starts {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_CH_PUMP_STARTS: [ + None, + None, + "Central Heating Pump Starts {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_PUMP_STARTS: [ + None, + None, + "Hot Water Pump Starts {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_BURNER_STARTS: [ + None, + None, + "Hot Water Burner Starts {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_TOTAL_BURNER_HOURS: [ + None, + TIME_HOURS, + "Total Burner Hours {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_CH_PUMP_HOURS: [ + None, + TIME_HOURS, + "Central Heating Pump Hours {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_PUMP_HOURS: [ + None, + TIME_HOURS, + "Hot Water Pump Hours {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_DHW_BURNER_HOURS: [ + None, + TIME_HOURS, + "Hot Water Burner Hours {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_OT_VERSION: [ + None, + None, + "Thermostat OpenTherm Version {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_OT_VERSION: [ + None, + None, + "Boiler OpenTherm Version {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_PRODUCT_TYPE: [ + None, + None, + "Thermostat Product Type {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_MASTER_PRODUCT_VERSION: [ + None, + None, + "Thermostat Product Version {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_PRODUCT_TYPE: [ + None, + None, + "Boiler Product Type {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.DATA_SLAVE_PRODUCT_VERSION: [ + None, + None, + "Boiler Product Version {}", + [gw_vars.BOILER, gw_vars.THERMOSTAT], + ], + gw_vars.OTGW_MODE: [None, None, "Gateway/Monitor Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_DHW_OVRD: [ + None, + None, + "Gateway Hot Water Override Mode {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_ABOUT: [None, None, "Gateway Firmware Version {}", [gw_vars.OTGW]], + gw_vars.OTGW_BUILD: [None, None, "Gateway Firmware Build {}", [gw_vars.OTGW]], + gw_vars.OTGW_CLOCKMHZ: [None, None, "Gateway Clock Speed {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_A: [None, None, "Gateway LED A Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_B: [None, None, "Gateway LED B Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_C: [None, None, "Gateway LED C Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_D: [None, None, "Gateway LED D Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_E: [None, None, "Gateway LED E Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_LED_F: [None, None, "Gateway LED F Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_GPIO_A: [None, None, "Gateway GPIO A Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_GPIO_B: [None, None, "Gateway GPIO B Mode {}", [gw_vars.OTGW]], gw_vars.OTGW_SB_TEMP: [ DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, "Gateway Setback Temperature {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_SETP_OVRD_MODE: [ + None, + None, + "Gateway Room Setpoint Override Mode {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_SMART_PWR: [None, None, "Gateway Smart Power Mode {}", [gw_vars.OTGW]], + gw_vars.OTGW_THRM_DETECT: [ + None, + None, + "Gateway Thermostat Detection {}", + [gw_vars.OTGW], + ], + gw_vars.OTGW_VREF: [ + None, + None, + "Gateway Reference Voltage Setting {}", + [gw_vars.OTGW], ], - gw_vars.OTGW_SETP_OVRD_MODE: [None, None, "Gateway Room Setpoint Override Mode {}"], - gw_vars.OTGW_SMART_PWR: [None, None, "Gateway Smart Power Mode {}"], - gw_vars.OTGW_THRM_DETECT: [None, None, "Gateway Thermostat Detection {}"], - gw_vars.OTGW_VREF: [None, None, "Gateway Reference Voltage Setting {}"], +} + +DEPRECATED_BINARY_SENSOR_SOURCE_LOOKUP = { + gw_vars.DATA_MASTER_CH_ENABLED: gw_vars.THERMOSTAT, + gw_vars.DATA_MASTER_DHW_ENABLED: gw_vars.THERMOSTAT, + gw_vars.DATA_MASTER_OTC_ENABLED: gw_vars.THERMOSTAT, + gw_vars.DATA_MASTER_CH2_ENABLED: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_FAULT_IND: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH_ACTIVE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_ACTIVE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_FLAME_ON: gw_vars.BOILER, + gw_vars.DATA_SLAVE_COOLING_ACTIVE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH2_ACTIVE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DIAG_IND: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_PRESENT: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CONTROL_TYPE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_COOLING_SUPPORTED: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_CONFIG: gw_vars.BOILER, + gw_vars.DATA_SLAVE_MASTER_LOW_OFF_PUMP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH2_PRESENT: gw_vars.BOILER, + gw_vars.DATA_SLAVE_SERVICE_REQ: gw_vars.BOILER, + gw_vars.DATA_SLAVE_REMOTE_RESET: gw_vars.BOILER, + gw_vars.DATA_SLAVE_LOW_WATER_PRESS: gw_vars.BOILER, + gw_vars.DATA_SLAVE_GAS_FAULT: gw_vars.BOILER, + gw_vars.DATA_SLAVE_AIR_PRESS_FAULT: gw_vars.BOILER, + gw_vars.DATA_SLAVE_WATER_OVERTEMP: gw_vars.BOILER, + gw_vars.DATA_REMOTE_TRANSFER_DHW: gw_vars.BOILER, + gw_vars.DATA_REMOTE_TRANSFER_MAX_CH: gw_vars.BOILER, + gw_vars.DATA_REMOTE_RW_DHW: gw_vars.BOILER, + gw_vars.DATA_REMOTE_RW_MAX_CH: gw_vars.BOILER, + gw_vars.DATA_ROVRD_MAN_PRIO: gw_vars.THERMOSTAT, + gw_vars.DATA_ROVRD_AUTO_PRIO: gw_vars.THERMOSTAT, + gw_vars.OTGW_GPIO_A_STATE: gw_vars.OTGW, + gw_vars.OTGW_GPIO_B_STATE: gw_vars.OTGW, + gw_vars.OTGW_IGNORE_TRANSITIONS: gw_vars.OTGW, + gw_vars.OTGW_OVRD_HB: gw_vars.OTGW, +} + +DEPRECATED_SENSOR_SOURCE_LOOKUP = { + gw_vars.DATA_CONTROL_SETPOINT: gw_vars.BOILER, + gw_vars.DATA_MASTER_MEMBERID: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_MEMBERID: gw_vars.BOILER, + gw_vars.DATA_SLAVE_OEM_FAULT: gw_vars.BOILER, + gw_vars.DATA_COOLING_CONTROL: gw_vars.BOILER, + gw_vars.DATA_CONTROL_SETPOINT_2: gw_vars.BOILER, + gw_vars.DATA_ROOM_SETPOINT_OVRD: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD: gw_vars.BOILER, + gw_vars.DATA_SLAVE_MAX_CAPACITY: gw_vars.BOILER, + gw_vars.DATA_SLAVE_MIN_MOD_LEVEL: gw_vars.BOILER, + gw_vars.DATA_ROOM_SETPOINT: gw_vars.THERMOSTAT, + gw_vars.DATA_REL_MOD_LEVEL: gw_vars.BOILER, + gw_vars.DATA_CH_WATER_PRESS: gw_vars.BOILER, + gw_vars.DATA_DHW_FLOW_RATE: gw_vars.BOILER, + gw_vars.DATA_ROOM_SETPOINT_2: gw_vars.THERMOSTAT, + gw_vars.DATA_ROOM_TEMP: gw_vars.THERMOSTAT, + gw_vars.DATA_CH_WATER_TEMP: gw_vars.BOILER, + gw_vars.DATA_DHW_TEMP: gw_vars.BOILER, + gw_vars.DATA_OUTSIDE_TEMP: gw_vars.THERMOSTAT, + gw_vars.DATA_RETURN_WATER_TEMP: gw_vars.BOILER, + gw_vars.DATA_SOLAR_STORAGE_TEMP: gw_vars.BOILER, + gw_vars.DATA_SOLAR_COLL_TEMP: gw_vars.BOILER, + gw_vars.DATA_CH_WATER_TEMP_2: gw_vars.BOILER, + gw_vars.DATA_DHW_TEMP_2: gw_vars.BOILER, + gw_vars.DATA_EXHAUST_TEMP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_MAX_SETP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_DHW_MIN_SETP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH_MAX_SETP: gw_vars.BOILER, + gw_vars.DATA_SLAVE_CH_MIN_SETP: gw_vars.BOILER, + gw_vars.DATA_DHW_SETPOINT: gw_vars.BOILER, + gw_vars.DATA_MAX_CH_SETPOINT: gw_vars.BOILER, + gw_vars.DATA_OEM_DIAG: gw_vars.BOILER, + gw_vars.DATA_TOTAL_BURNER_STARTS: gw_vars.BOILER, + gw_vars.DATA_CH_PUMP_STARTS: gw_vars.BOILER, + gw_vars.DATA_DHW_PUMP_STARTS: gw_vars.BOILER, + gw_vars.DATA_DHW_BURNER_STARTS: gw_vars.BOILER, + gw_vars.DATA_TOTAL_BURNER_HOURS: gw_vars.BOILER, + gw_vars.DATA_CH_PUMP_HOURS: gw_vars.BOILER, + gw_vars.DATA_DHW_PUMP_HOURS: gw_vars.BOILER, + gw_vars.DATA_DHW_BURNER_HOURS: gw_vars.BOILER, + gw_vars.DATA_MASTER_OT_VERSION: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_OT_VERSION: gw_vars.BOILER, + gw_vars.DATA_MASTER_PRODUCT_TYPE: gw_vars.THERMOSTAT, + gw_vars.DATA_MASTER_PRODUCT_VERSION: gw_vars.THERMOSTAT, + gw_vars.DATA_SLAVE_PRODUCT_TYPE: gw_vars.BOILER, + gw_vars.DATA_SLAVE_PRODUCT_VERSION: gw_vars.BOILER, + gw_vars.OTGW_MODE: gw_vars.OTGW, + gw_vars.OTGW_DHW_OVRD: gw_vars.OTGW, + gw_vars.OTGW_ABOUT: gw_vars.OTGW, + gw_vars.OTGW_BUILD: gw_vars.OTGW, + gw_vars.OTGW_CLOCKMHZ: gw_vars.OTGW, + gw_vars.OTGW_LED_A: gw_vars.OTGW, + gw_vars.OTGW_LED_B: gw_vars.OTGW, + gw_vars.OTGW_LED_C: gw_vars.OTGW, + gw_vars.OTGW_LED_D: gw_vars.OTGW, + gw_vars.OTGW_LED_E: gw_vars.OTGW, + gw_vars.OTGW_LED_F: gw_vars.OTGW, + gw_vars.OTGW_GPIO_A: gw_vars.OTGW, + gw_vars.OTGW_GPIO_B: gw_vars.OTGW, + gw_vars.OTGW_SB_TEMP: gw_vars.OTGW, + gw_vars.OTGW_SETP_OVRD_MODE: gw_vars.OTGW, + gw_vars.OTGW_SMART_PWR: gw_vars.OTGW, + gw_vars.OTGW_THRM_DETECT: gw_vars.OTGW, + gw_vars.OTGW_VREF: gw_vars.OTGW, } diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 558f4adced..066cee61c0 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -2,7 +2,7 @@ "domain": "opentherm_gw", "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": ["pyotgw==0.6b1"], + "requirements": ["pyotgw==1.0b1"], "codeowners": ["@mvn23"], "config_flow": true } diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index b2f8e27298..4a20aa651c 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -1,14 +1,22 @@ """Support for OpenTherm Gateway sensors.""" import logging +from pprint import pformat from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id +from homeassistant.helpers.entity_registry import async_get_registry from . import DOMAIN -from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO +from .const import ( + DATA_GATEWAYS, + DATA_OPENTHERM_GW, + DEPRECATED_SENSOR_SOURCE_LOOKUP, + SENSOR_INFO, + TRANSLATE_SOURCE, +) _LOGGER = logging.getLogger(__name__) @@ -16,18 +24,54 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway sensors.""" sensors = [] + deprecated_sensors = [] + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + ent_reg = await async_get_registry(hass) for var, info in SENSOR_INFO.items(): device_class = info[0] unit = info[1] friendly_name_format = info[2] - sensors.append( - OpenThermSensor( - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], - var, - device_class, - unit, - friendly_name_format, + status_sources = info[3] + + for source in status_sources: + sensors.append( + OpenThermSensor( + gw_dev, + var, + source, + device_class, + unit, + friendly_name_format, + ) ) + + old_style_entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ) + old_ent = ent_reg.async_get(old_style_entity_id) + if old_ent and old_ent.config_entry_id == config_entry.entry_id: + if old_ent.disabled: + ent_reg.async_remove(old_style_entity_id) + else: + deprecated_sensors.append( + DeprecatedOpenThermSensor( + gw_dev, + var, + device_class, + unit, + friendly_name_format, + ) + ) + + sensors.extend(deprecated_sensors) + + if deprecated_sensors: + _LOGGER.warning( + "The following sensor entities are deprecated and may no " + "longer behave as expected. They will be removed in a future " + "version. You can force removal of these entities by disabling " + "them and restarting Home Assistant.\n%s", + pformat([s.entity_id for s in deprecated_sensors]), ) async_add_entities(sensors) @@ -36,16 +80,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class OpenThermSensor(Entity): """Representation of an OpenTherm Gateway sensor.""" - def __init__(self, gw_dev, var, device_class, unit, friendly_name_format): + def __init__(self, gw_dev, var, source, device_class, unit, friendly_name_format): """Initialize the OpenTherm Gateway sensor.""" self.entity_id = async_generate_entity_id( - ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ENTITY_ID_FORMAT, f"{var}_{source}_{gw_dev.gw_id}", hass=gw_dev.hass ) self._gateway = gw_dev self._var = var + self._source = source self._value = None self._device_class = device_class self._unit = unit + if TRANSLATE_SOURCE[source] is not None: + friendly_name_format = ( + f"{friendly_name_format} ({TRANSLATE_SOURCE[source]})" + ) self._friendly_name = friendly_name_format.format(gw_dev.name) self._unsub_updates = None @@ -74,7 +123,7 @@ class OpenThermSensor(Entity): @callback def receive_report(self, status): """Handle status updates from the component.""" - value = status.get(self._var) + value = status[self._source].get(self._var) if isinstance(value, float): value = f"{value:2.1f}" self._value = value @@ -99,7 +148,7 @@ class OpenThermSensor(Entity): @property def unique_id(self): """Return a unique ID.""" - return f"{self._gateway.gw_id}-{self._var}" + return f"{self._gateway.gw_id}-{self._source}-{self._var}" @property def device_class(self): @@ -120,3 +169,27 @@ class OpenThermSensor(Entity): def should_poll(self): """Return False because entity pushes its state.""" return False + + +class DeprecatedOpenThermSensor(OpenThermSensor): + """Represent a deprecated OpenTherm Gateway Sensor.""" + + # pylint: disable=super-init-not-called + def __init__(self, gw_dev, var, device_class, unit, friendly_name_format): + """Initialize the OpenTherm Gateway sensor.""" + self.entity_id = async_generate_entity_id( + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass + ) + self._gateway = gw_dev + self._var = var + self._source = DEPRECATED_SENSOR_SOURCE_LOOKUP[var] + self._value = None + self._device_class = device_class + self._unit = unit + self._friendly_name = friendly_name_format.format(gw_dev.name) + self._unsub_updates = None + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._gateway.gw_id}-{self._var}" diff --git a/homeassistant/components/opentherm_gw/translations/pt.json b/homeassistant/components/opentherm_gw/translations/pt.json index 960e3a9cf5..85b6e61796 100644 --- a/homeassistant/components/opentherm_gw/translations/pt.json +++ b/homeassistant/components/opentherm_gw/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "init": { "data": { @@ -8,5 +12,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "precision": "Precis\u00e3o" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/zh-Hant.json b/homeassistant/components/opentherm_gw/translations/zh-Hant.json index 35099f2a59..ea138287c7 100644 --- a/homeassistant/components/opentherm_gw/translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728" }, diff --git a/homeassistant/components/openuv/translations/pt.json b/homeassistant/components/openuv/translations/pt.json index c408b4bec3..6433111fe8 100644 --- a/homeassistant/components/openuv/translations/pt.json +++ b/homeassistant/components/openuv/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, "error": { "invalid_api_key": "Chave de API inv\u00e1lida" }, diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index 35232fe04d..239b47e2d3 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/openweathermap/translations/pt.json b/homeassistant/components/openweathermap/translations/pt.json index f736ec7e3c..aabac4f4cf 100644 --- a/homeassistant/components/openweathermap/translations/pt.json +++ b/homeassistant/components/openweathermap/translations/pt.json @@ -1,8 +1,16 @@ { "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { "user": { "data": { + "api_key": "API Key", "language": "Idioma", "latitude": "Latitude", "longitude": "Longitude", diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index 6f39806287..3bd083e483 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -1,6 +1,14 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { + "reauth": { + "data": { + "password": "Passwort" + } + }, "user": { "data": { "password": "Passwort", diff --git a/homeassistant/components/ovo_energy/translations/hu.json b/homeassistant/components/ovo_energy/translations/hu.json index c4b70e9007..c4091388b3 100644 --- a/homeassistant/components/ovo_energy/translations/hu.json +++ b/homeassistant/components/ovo_energy/translations/hu.json @@ -3,6 +3,11 @@ "error": { "already_configured": "A fi\u00f3k m\u00e1r konfigur\u00e1lva van", "cannot_connect": "Sikertelen csatlakoz\u00e1s" + }, + "step": { + "reauth": { + "title": "\u00dajrahiteles\u00edt\u00e9s" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/nl.json b/homeassistant/components/ovo_energy/translations/nl.json index cf9f264a61..daa12f9e56 100644 --- a/homeassistant/components/ovo_energy/translations/nl.json +++ b/homeassistant/components/ovo_energy/translations/nl.json @@ -5,6 +5,9 @@ "invalid_auth": "Ongeldige authenticatie" }, "step": { + "reauth": { + "title": "Opnieuw verifi\u00ebren" + }, "user": { "data": { "password": "Wachtwoord", diff --git a/homeassistant/components/ovo_energy/translations/no.json b/homeassistant/components/ovo_energy/translations/no.json index 5e0537b663..97bf9fc498 100644 --- a/homeassistant/components/ovo_energy/translations/no.json +++ b/homeassistant/components/ovo_energy/translations/no.json @@ -11,8 +11,8 @@ "data": { "password": "Passord" }, - "description": "Autentisering mislyktes for OVO Energy. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.", - "title": "Reautorisasjon" + "description": "Godkjenning mislyktes for OVO Energy. Vennligst skriv inn din n\u00e5v\u00e6rende legitimasjon.", + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/ovo_energy/translations/pt.json b/homeassistant/components/ovo_energy/translations/pt.json index 3fbf1797b3..7015a44b5f 100644 --- a/homeassistant/components/ovo_energy/translations/pt.json +++ b/homeassistant/components/ovo_energy/translations/pt.json @@ -1,9 +1,16 @@ { "config": { "error": { - "already_configured": "Conta j\u00e1 configurada" + "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { + "reauth": { + "data": { + "password": "Palavra-passe" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json new file mode 100644 index 0000000000..f3784f6de8 --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "flow_title": "OVO Enerji: {username}", + "step": { + "reauth": { + "data": { + "password": "\u015eifre" + }, + "description": "OVO Energy i\u00e7in kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu. L\u00fctfen mevcut kimlik bilgilerinizi girin.", + "title": "Yeniden kimlik do\u011frulama" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ovo_energy/translations/zh-Hant.json b/homeassistant/components/ovo_energy/translations/zh-Hant.json index f557a83009..43f456f757 100644 --- a/homeassistant/components/ovo_energy/translations/zh-Hant.json +++ b/homeassistant/components/ovo_energy/translations/zh-Hant.json @@ -19,7 +19,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8a2d\u5b9a OVO Energy \u8a2d\u5099\u4ee5\u76e3\u63a7\u80fd\u6e90\u4f7f\u7528\u72c0\u6cc1\u3002", + "description": "\u8a2d\u5b9a OVO Energy \u88dd\u7f6e\u4ee5\u76e3\u63a7\u80fd\u6e90\u4f7f\u7528\u72c0\u6cc1\u3002", "title": "\u65b0\u589e OVO Energy \u5e33\u865f" } } diff --git a/homeassistant/components/owntracks/translations/no.json b/homeassistant/components/owntracks/translations/no.json index 923fbab244..db992a5630 100644 --- a/homeassistant/components/owntracks/translations/no.json +++ b/homeassistant/components/owntracks/translations/no.json @@ -4,7 +4,7 @@ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { - "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url}\n - Identificasjon:\n - Brukernavn: ''\n - Enhets ID: ''\n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP\n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autensiering\n - BrukerID: ''\n\n{secret}\n \n Se [dokumentasjonen]({docs_url}) for mer informasjon." + "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url}\n - Identifikasjon:\n - Brukernavn: ''\n - Enhets ID: ''\n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP\n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 godkjenning\n - BrukerID: ''\n\n{secret}\n \n Se [dokumentasjonen]({docs_url}) for mer informasjon" }, "step": { "user": { diff --git a/homeassistant/components/owntracks/translations/pt.json b/homeassistant/components/owntracks/translations/pt.json index dbc8db55d6..ebc78e1346 100644 --- a/homeassistant/components/owntracks/translations/pt.json +++ b/homeassistant/components/owntracks/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, "create_entry": { "default": "\n\n No Android, abra [o aplicativo OwnTracks] ( {android_url} ), v\u00e1 para prefer\u00eancias - > conex\u00e3o. Altere as seguintes configura\u00e7\u00f5es: \n - Modo: HTTP privado \n - Anfitri\u00e3o: {webhook_url} \n - Identifica\u00e7\u00e3o: \n - Nome de usu\u00e1rio: ` \n - ID do dispositivo: ` ` \n\n No iOS, abra [o aplicativo OwnTracks] ( {ios_url} ), toque no \u00edcone (i) no canto superior esquerdo - > configura\u00e7\u00f5es. Altere as seguintes configura\u00e7\u00f5es: \n - Modo: HTTP \n - URL: {webhook_url} \n - Ativar autentica\u00e7\u00e3o \n - UserID: ` ` \n\n {secret} \n \n Veja [a documenta\u00e7\u00e3o] ( {docs_url} ) para mais informa\u00e7\u00f5es." }, diff --git a/homeassistant/components/owntracks/translations/zh-Hant.json b/homeassistant/components/owntracks/translations/zh-Hant.json index b89f147247..6c92b55779 100644 --- a/homeassistant/components/owntracks/translations/zh-Hant.json +++ b/homeassistant/components/owntracks/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\n\n\u65bc Android \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({android_url})\u3001\u9ede\u9078\u8a2d\u5b9a\uff08preferences\uff09 -> \u9023\u7dda\uff08connection\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aPrivate HTTP\n - \u4e3b\u6a5f\u7aef\uff08Host\uff09\uff1a{webhook_url}\n - Identification\uff1a\n - Username\uff1a ``\n - Device ID\uff1a``\n\n\u65bc iOS \u8a2d\u5099\uff0c\u6253\u958b [OwnTracks app]({ios_url})\u3001\u9ede\u9078\u5de6\u4e0a\u65b9\u7684 (i) \u5716\u793a -> \u8a2d\u5b9a\uff08settings\uff09\u3002\u8b8a\u66f4\u4ee5\u4e0b\u8a2d\u5b9a\uff1a\n - \u6a21\u5f0f\uff08Mode\uff09\uff1aHTTP\n - URL: {webhook_url}\n - \u958b\u555f authentication\n - UserID: ``\n\n{secret}\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json index 6010da17b7..4553589e36 100644 --- a/homeassistant/components/ozw/translations/ca.json +++ b/homeassistant/components/ozw/translations/ca.json @@ -4,13 +4,24 @@ "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement OpenZWave.", "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement OpenZWave.", "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 d'OpenZWave.", + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "mqtt_required": "La integraci\u00f3 MQTT no est\u00e0 configurada", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." }, "error": { "addon_start_failed": "No s'ha pogut iniciar el complement OpenZWave. Comprova la configuraci\u00f3." }, + "progress": { + "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement OpenZWave. Pot tardar uns quants minuts." + }, "step": { + "hassio_confirm": { + "title": "Configuraci\u00f3 de la integraci\u00f3 d'OpenZWave amb el complement OpenZWave" + }, + "install_addon": { + "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement OpenZWave" + }, "on_supervisor": { "data": { "use_addon": "Utilitza el complement OpenZWave Supervisor" diff --git a/homeassistant/components/ozw/translations/cs.json b/homeassistant/components/ozw/translations/cs.json index 621e48bab7..d479efdf95 100644 --- a/homeassistant/components/ozw/translations/cs.json +++ b/homeassistant/components/ozw/translations/cs.json @@ -4,6 +4,8 @@ "addon_info_failed": "Nepoda\u0159ilo se z\u00edskat informace o dopl\u0148ku OpenZWave.", "addon_install_failed": "Instalace dopl\u0148ku OpenZWave se nezda\u0159ila.", "addon_set_config_failed": "Nepoda\u0159ilo se nastavit OpenZWave.", + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", "mqtt_required": "Integrace MQTT nen\u00ed nastavena", "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." }, @@ -11,6 +13,12 @@ "addon_start_failed": "Spu\u0161t\u011bn\u00ed dopl\u0148ku OpenZWave se nezda\u0159ilo. Zkontrolujte konfiguraci." }, "step": { + "hassio_confirm": { + "title": "Nastaven\u00ed integrace OpenZWave s dopl\u0148kem OpenZWave" + }, + "install_addon": { + "title": "Instalace dopl\u0148ku OpenZWave byla zah\u00e1jena." + }, "on_supervisor": { "data": { "use_addon": "Pou\u017e\u00edt dopln\u011bk OpenZWave pro Supervisor" diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json index 81a2390cc8..70eaaaf18d 100644 --- a/homeassistant/components/ozw/translations/de.json +++ b/homeassistant/components/ozw/translations/de.json @@ -1,7 +1,20 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet" + }, + "progress": { + "install_addon": "Bitte warten, bis die Installation des OpenZWave-Add-Ons abgeschlossen ist. Dies kann einige Minuten dauern." + }, + "step": { + "hassio_confirm": { + "title": "Richte die OpenZWave Integration mit dem OpenZWave Add-On ein" + }, + "install_addon": { + "title": "Die Installation des OpenZWave-Add-On wurde gestartet" + } } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/es.json b/homeassistant/components/ozw/translations/es.json index 60d64f9afb..f06c2896bc 100644 --- a/homeassistant/components/ozw/translations/es.json +++ b/homeassistant/components/ozw/translations/es.json @@ -4,6 +4,8 @@ "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento de OpenZWave.", "addon_install_failed": "No se pudo instalar el complemento de OpenZWave.", "addon_set_config_failed": "No se pudo establecer la configuraci\u00f3n de OpenZWave.", + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "mqtt_required": "La integraci\u00f3n de MQTT no est\u00e1 configurada", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, @@ -14,6 +16,9 @@ "install_addon": "Espera mientras finaliza la instalaci\u00f3n del complemento OpenZWave. Esto puede tardar varios minutos." }, "step": { + "hassio_confirm": { + "title": "Configurar la integraci\u00f3n de OpenZWave con el complemento OpenZWave" + }, "install_addon": { "title": "La instalaci\u00f3n del complemento OpenZWave se ha iniciado" }, diff --git a/homeassistant/components/ozw/translations/fr.json b/homeassistant/components/ozw/translations/fr.json index cbf9bfb6bd..c4ea835d86 100644 --- a/homeassistant/components/ozw/translations/fr.json +++ b/homeassistant/components/ozw/translations/fr.json @@ -4,13 +4,20 @@ "addon_info_failed": "Impossible d\u2019obtenir des informations de l'add-on OpenZWave.", "addon_install_failed": "\u00c9chec de l\u2019installation de l'add-on OpenZWave.", "addon_set_config_failed": "\u00c9chec de la configuration OpenZWave.", + "already_configured": "Cet appareil est d\u00e9j\u00e0 configur\u00e9", "mqtt_required": "L'int\u00e9gration MQTT n'est pas configur\u00e9e", "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." }, "error": { "addon_start_failed": "\u00c9chec du d\u00e9marrage de l'add-on OpenZWave. V\u00e9rifiez la configuration." }, + "progress": { + "install_addon": "Veuillez patienter pendant que l'installation du module OpenZWave se termine. Cela peut prendre plusieurs minutes." + }, "step": { + "hassio_confirm": { + "title": "Configurer l\u2019int\u00e9gration OpenZWave avec l\u2019add-on OpenZWave" + }, "on_supervisor": { "data": { "use_addon": "Utiliser l'add-on OpenZWave Supervisor" diff --git a/homeassistant/components/ozw/translations/hu.json b/homeassistant/components/ozw/translations/hu.json index 9729938035..e4c864d9fd 100644 --- a/homeassistant/components/ozw/translations/hu.json +++ b/homeassistant/components/ozw/translations/hu.json @@ -3,7 +3,8 @@ "abort": { "addon_info_failed": "Nem siker\u00fclt bet\u00f6lteni az OpenZWave kieg\u00e9sz\u00edt\u0151 inform\u00e1ci\u00f3kat.", "addon_install_failed": "Nem siker\u00fclt telep\u00edteni az OpenZWave b\u0151v\u00edtm\u00e9nyt.", - "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t." + "addon_set_config_failed": "Nem siker\u00fclt be\u00e1ll\u00edtani az OpenZWave konfigur\u00e1ci\u00f3t.", + "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" }, "error": { "addon_start_failed": "Nem siker\u00fclt elind\u00edtani az OpenZWave b\u0151v\u00edtm\u00e9nyt. Ellen\u0151rizze a konfigur\u00e1ci\u00f3t." diff --git a/homeassistant/components/ozw/translations/it.json b/homeassistant/components/ozw/translations/it.json index e03c71ae70..ff3e0a711c 100644 --- a/homeassistant/components/ozw/translations/it.json +++ b/homeassistant/components/ozw/translations/it.json @@ -4,13 +4,24 @@ "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo OpenZWave.", "addon_install_failed": "Impossibile installare il componente aggiuntivo OpenZWave.", "addon_set_config_failed": "Impossibile impostare la configurazione di OpenZWave.", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "mqtt_required": "L'integrazione MQTT non \u00e8 impostata", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." }, "error": { "addon_start_failed": "Impossibile avviare il componente aggiuntivo OpenZWave. Controlla la configurazione." }, + "progress": { + "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo OpenZWave. Questa operazione pu\u00f2 richiedere diversi minuti." + }, "step": { + "hassio_confirm": { + "title": "Configura l'integrazione di OpenZWave con il componente aggiuntivo OpenZWave" + }, + "install_addon": { + "title": "L'installazione del componente aggiuntivo OpenZWave \u00e8 iniziata" + }, "on_supervisor": { "data": { "use_addon": "Usa il componente aggiuntivo OpenZWave Supervisor" diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json index 966e1a4065..89563ff353 100644 --- a/homeassistant/components/ozw/translations/no.json +++ b/homeassistant/components/ozw/translations/no.json @@ -4,6 +4,8 @@ "addon_info_failed": "Kunne ikke hente OpenZWave-tilleggsinfo", "addon_install_failed": "Kunne ikke installere OpenZWave-tillegget", "addon_set_config_failed": "Kunne ikke angi OpenZWave-konfigurasjon", + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "mqtt_required": "MQTT-integrasjonen er ikke satt opp", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, @@ -14,6 +16,9 @@ "install_addon": "Vent mens OpenZWave-tilleggsinstallasjonen er ferdig. Dette kan ta flere minutter." }, "step": { + "hassio_confirm": { + "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegget" + }, "install_addon": { "title": "Installasjonen av tilleggsprogrammet OpenZWave har startet" }, diff --git a/homeassistant/components/ozw/translations/pl.json b/homeassistant/components/ozw/translations/pl.json index a143163ca1..c9fd17c59b 100644 --- a/homeassistant/components/ozw/translations/pl.json +++ b/homeassistant/components/ozw/translations/pl.json @@ -4,6 +4,8 @@ "addon_info_failed": "Nie uda\u0142o si\u0119 pobra\u0107 informacji o dodatku OpenZWave", "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku OpenZWave", "addon_set_config_failed": "Nie uda\u0142o si\u0119 ustawi\u0107 konfiguracji OpenZWave", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", "mqtt_required": "Integracja MQTT nie jest skonfigurowana", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." }, @@ -14,6 +16,9 @@ "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku OpenZWave. Mo\u017ce to potrwa\u0107 kilka minut." }, "step": { + "hassio_confirm": { + "title": "Konfiguracja integracji OpenZWave z dodatkiem OpenZWave" + }, "install_addon": { "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku OpenZWave" }, diff --git a/homeassistant/components/ozw/translations/pt.json b/homeassistant/components/ozw/translations/pt.json new file mode 100644 index 0000000000..75d8509787 --- /dev/null +++ b/homeassistant/components/ozw/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "start_addon": { + "data": { + "usb_path": "Caminho do Dispositivo USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/ru.json b/homeassistant/components/ozw/translations/ru.json index b2f5ebd6e8..07dc84eae0 100644 --- a/homeassistant/components/ozw/translations/ru.json +++ b/homeassistant/components/ozw/translations/ru.json @@ -4,6 +4,8 @@ "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 OpenZWave.", "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c OpenZWave.", "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e OpenZWave.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "mqtt_required": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f MQTT \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, @@ -14,6 +16,9 @@ "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442." }, "step": { + "hassio_confirm": { + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" + }, "install_addon": { "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f OpenZWave" }, diff --git a/homeassistant/components/ozw/translations/sl.json b/homeassistant/components/ozw/translations/sl.json index c4d67feb5b..8da77910c3 100644 --- a/homeassistant/components/ozw/translations/sl.json +++ b/homeassistant/components/ozw/translations/sl.json @@ -1,9 +1,20 @@ { "config": { "abort": { + "already_configured": "Naprava je \u017ee name\u0161\u010dena", + "already_in_progress": "Name\u0161\u010danje se \u017ee izvaja", "mqtt_required": "Integracija MQTT ni nastavljena" }, + "progress": { + "install_addon": "Po\u010dakajte, da se namestitev dodatka OpenZWave zaklju\u010di. To lahko traja ve\u010d minut." + }, "step": { + "hassio_confirm": { + "title": "Namestite OpenZWave integracijo z OpenZWave dodatkom." + }, + "install_addon": { + "title": "Namestitev dodatka OpenZWave se je za\u010dela" + }, "on_supervisor": { "title": "Izberite na\u010din povezave" } diff --git a/homeassistant/components/ozw/translations/tr.json b/homeassistant/components/ozw/translations/tr.json new file mode 100644 index 0000000000..d0a70d5775 --- /dev/null +++ b/homeassistant/components/ozw/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "addon_info_failed": "OpenZWave eklenti bilgileri al\u0131namad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "progress": { + "install_addon": "OpenZWave eklenti kurulumu bitene kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." + }, + "step": { + "install_addon": { + "title": "OpenZWave eklenti kurulumu ba\u015flad\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/zh-Hant.json b/homeassistant/components/ozw/translations/zh-Hant.json index f4334e1d63..f9ed5469da 100644 --- a/homeassistant/components/ozw/translations/zh-Hant.json +++ b/homeassistant/components/ozw/translations/zh-Hant.json @@ -4,8 +4,10 @@ "addon_info_failed": "\u53d6\u5f97 OpenZWave add-on \u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "OpenZWave add-on \u5b89\u88dd\u5931\u6557\u3002", "addon_set_config_failed": "OpenZWave add-on \u8a2d\u5b9a\u5931\u6557\u3002", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "mqtt_required": "MQTT \u6574\u5408\u5c1a\u672a\u8a2d\u5b9a", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "addon_start_failed": "OpenZWave add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002" @@ -14,6 +16,9 @@ "install_addon": "\u8acb\u7a0d\u7b49 OpenZWave add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" }, "step": { + "hassio_confirm": { + "title": "\u4ee5 OpenZWave add-on \u8a2d\u5b9a OpenZwave \u6574\u5408" + }, "install_addon": { "title": "OpenZWave add-on \u5b89\u88dd\u5df2\u555f\u52d5" }, @@ -27,7 +32,7 @@ "start_addon": { "data": { "network_key": "\u7db2\u8def\u5bc6\u9470", - "usb_path": "USB \u8a2d\u5099\u8def\u5f91" + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, "title": "\u8acb\u8f38\u5165 OpenZWave \u8a2d\u5b9a\u3002" } diff --git a/homeassistant/components/panasonic_viera/translations/de.json b/homeassistant/components/panasonic_viera/translations/de.json index cac04acb87..4b2c14be9d 100644 --- a/homeassistant/components/panasonic_viera/translations/de.json +++ b/homeassistant/components/panasonic_viera/translations/de.json @@ -2,9 +2,11 @@ "config": { "abort": { "already_configured": "Dieser Panasonic Viera TV ist bereits konfiguriert.", + "cannot_connect": "Verbindungsfehler", "unknown": "Ein unbekannter Fehler ist aufgetreten. Weitere Informationen finden Sie in den Logs." }, "error": { + "cannot_connect": "Verbindungsfehler", "invalid_pin_code": "Der von Ihnen eingegebene PIN-Code war ung\u00fcltig" }, "step": { diff --git a/homeassistant/components/panasonic_viera/translations/no.json b/homeassistant/components/panasonic_viera/translations/no.json index 7efa1e3176..5c0105a762 100644 --- a/homeassistant/components/panasonic_viera/translations/no.json +++ b/homeassistant/components/panasonic_viera/translations/no.json @@ -7,14 +7,14 @@ }, "error": { "cannot_connect": "Tilkobling mislyktes", - "invalid_pin_code": "PIN-kode du skrev inn var ugyldig" + "invalid_pin_code": "PIN kode du skrev inn var ugyldig" }, "step": { "pairing": { "data": { - "pin": "PIN-kode" + "pin": "PIN kode" }, - "description": "Skriv inn PIN-kode som vises p\u00e5 TV-en", + "description": "Skriv inn PIN kode som vises p\u00e5 TV-en", "title": "Sammenkobling" }, "user": { diff --git a/homeassistant/components/panasonic_viera/translations/pt.json b/homeassistant/components/panasonic_viera/translations/pt.json index 1e4f4cadc2..411d3a8610 100644 --- a/homeassistant/components/panasonic_viera/translations/pt.json +++ b/homeassistant/components/panasonic_viera/translations/pt.json @@ -1,10 +1,20 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_pin_code": "O C\u00f3digo PIN digitado \u00e9 inv\u00e1lido" + }, "step": { "pairing": { "data": { "pin": "PIN" }, + "description": "Digite o C\u00f3digo PIN exibido na sua TV", "title": "Emparelhamento" }, "user": { @@ -12,6 +22,7 @@ "host": "Endere\u00e7o IP", "name": "Nome" }, + "description": "Introduza o Endere\u00e7o IP da sua TV Panasonic Viera", "title": "Configure a sua TV" } } diff --git a/homeassistant/components/panasonic_viera/translations/zh-Hant.json b/homeassistant/components/panasonic_viera/translations/zh-Hant.json index 5ac554d269..1b39556f45 100644 --- a/homeassistant/components/panasonic_viera/translations/zh-Hant.json +++ b/homeassistant/components/panasonic_viera/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 584ce708d1..d0c0e9eccc 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -87,8 +87,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -_UNDEF = object() - @bind_hass async def async_create_person(hass, name, *, user_id=None, device_trackers=None): diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 451e8d6a3c..7ccec14406 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -313,10 +313,11 @@ class PhilipsTVMediaPlayer(MediaPlayerEntity): self._tv.update() self._sources = { - srcid: source["name"] or f"Source {srcid}" + srcid: source.get("name") or f"Source {srcid}" for srcid, source in (self._tv.sources or {}).items() } self._channels = { - chid: channel["name"] for chid, channel in (self._tv.channels or {}).items() + chid: channel.get("name") or f"Channel {chid}" + for chid, channel in (self._tv.channels or {}).items() } diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index f681da4210..e56597a400 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { + "api_key": "API Key", "host": "Servidor", - "port": "Porta" + "location": "Localiza\u00e7\u00e3o", + "name": "Nome", + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index 33788960de..258a75caa0 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -3,6 +3,6 @@ "name": "Ping (ICMP)", "documentation": "https://www.home-assistant.io/integrations/ping", "codeowners": [], - "requirements": ["icmplib==1.2.2"], + "requirements": ["icmplib==2.0"], "quality_scale": "internal" } diff --git a/homeassistant/components/plaato/translations/pt.json b/homeassistant/components/plaato/translations/pt.json new file mode 100644 index 0000000000..e9890abba2 --- /dev/null +++ b/homeassistant/components/plaato/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index dbfe2075e2..aec745ea38 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4d765cc050..24e37216b7 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -518,7 +518,7 @@ class PlexMediaPlayer(MediaPlayerEntity): "media_content_rating", "media_library_title", "player_source", - "summary", + "media_summary", "username", ]: value = getattr(self, attr, None) diff --git a/homeassistant/components/plex/translations/no.json b/homeassistant/components/plex/translations/no.json index c5368b675b..ddd6ef4cdb 100644 --- a/homeassistant/components/plex/translations/no.json +++ b/homeassistant/components/plex/translations/no.json @@ -4,7 +4,7 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", - "reauth_successful": "Reautentisering var vellykket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Uventet feil" }, diff --git a/homeassistant/components/plex/translations/pt.json b/homeassistant/components/plex/translations/pt.json index 81b70bcd08..3b63ab169e 100644 --- a/homeassistant/components/plex/translations/pt.json +++ b/homeassistant/components/plex/translations/pt.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Este servidor Plex j\u00e1 est\u00e1 configurado", "already_in_progress": "Plex est\u00e1 a ser configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", "unknown": "Falha por motivo desconhecido" }, "error": { @@ -12,7 +13,9 @@ "manual_setup": { "data": { "host": "Servidor", - "port": "Porta" + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "verify_ssl": "Verificar o certificado SSL" } }, "select_server": { diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index e2e01eb1df..2282e3584f 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -18,7 +18,8 @@ "user_gateway": { "data": { "port": "Port" - } + }, + "description": "Bitte eingeben" } } }, diff --git a/homeassistant/components/plugwise/translations/hu.json b/homeassistant/components/plugwise/translations/hu.json new file mode 100644 index 0000000000..1dcdb7fe5a --- /dev/null +++ b/homeassistant/components/plugwise/translations/hu.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "flow_type": "Kapcsolat t\u00edpusa" + } + }, + "user_gateway": { + "description": "K\u00e9rj\u00fck, adja meg" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/translations/pt.json b/homeassistant/components/plugwise/translations/pt.json index 808e3f3f7e..dd40927a7c 100644 --- a/homeassistant/components/plugwise/translations/pt.json +++ b/homeassistant/components/plugwise/translations/pt.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" + }, + "step": { + "user_gateway": { + "data": { + "host": "Endere\u00e7o IP", + "port": "Porta" + } + } } }, "options": { diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json new file mode 100644 index 0000000000..d25f1975cf --- /dev/null +++ b/homeassistant/components/plugwise/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user_gateway": { + "data": { + "username": "Smile Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/de.json b/homeassistant/components/plum_lightpad/translations/de.json index f55df964f8..accee16a6f 100644 --- a/homeassistant/components/plum_lightpad/translations/de.json +++ b/homeassistant/components/plum_lightpad/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index 41b46ae5cb..ad95609bd9 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -2,6 +2,6 @@ "domain": "pocketcasts", "name": "Pocket Casts", "documentation": "https://www.home-assistant.io/integrations/pocketcasts", - "requirements": ["pocketcasts==0.1"], + "requirements": ["pycketcasts==1.0.0"], "codeowners": [] } diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 05a8f96bda..19f7e26543 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -import pocketcasts +from pycketcasts import pocketcasts import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -29,8 +29,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) try: - api = pocketcasts.Api(username, password) - _LOGGER.debug("Found %d podcasts", len(api.my_podcasts())) + api = pocketcasts.PocketCast(email=username, password=password) + _LOGGER.debug("Found %d podcasts", len(api.subscriptions)) add_entities([PocketCastsSensor(api)], True) except OSError as err: _LOGGER.error("Connection to server failed: %s", err) @@ -63,7 +63,7 @@ class PocketCastsSensor(Entity): def update(self): """Update sensor values.""" try: - self._state = len(self._api.new_episodes_released()) + self._state = len(self._api.new_releases) _LOGGER.debug("Found %d new episodes", self._state) except OSError as err: _LOGGER.warning("Failed to contact server: %s", err) diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index 1e224e5ac5..8ee83eab72 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.", - "no_flows": "Du m\u00fcsst Point konfigurieren, bevor du dich damit authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/point/)." + "no_flows": "Du m\u00fcsst Point konfigurieren, bevor du dich damit authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, "create_entry": { "default": "Erfolgreich authentifiziert" diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index 59dff606f8..d0d0b9114f 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -2,22 +2,22 @@ "config": { "abort": { "already_setup": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", - "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", + "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "external_setup": "Punktet er konfigurert fra en annen flyt.", - "no_flows": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", - "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." + "no_flows": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", + "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" }, "create_entry": { "default": "Vellykket godkjenning" }, "error": { - "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker p\u00e5 Send", + "follow_link": "Vennligst f\u00f8lg lenken og godkjenn f\u00f8r du trykker send", "no_token": "Ugyldig tilgangstoken" }, "step": { "auth": { - "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Minut-kontoen din, kom tilbake og trykk **Send inn** nedenfor. \n\n [Link]({authorization_url})", + "description": "Vennligst f\u00f8lg lenken nedenfor og **Godta** tilgang til Minut-kontoen din, kom tilbake og trykk **Send inn** nedenfor\n\n [Link]({authorization_url})", "title": "Godkjenn Point" }, "user": { diff --git a/homeassistant/components/point/translations/pt.json b/homeassistant/components/point/translations/pt.json index 1ccfdddc1d..401e10c256 100644 --- a/homeassistant/components/point/translations/pt.json +++ b/homeassistant/components/point/translations/pt.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", - "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/)." + "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "create_entry": { "default": "Autenticado com sucesso com Minut para o(s) seu(s) dispositivo (s) Point" diff --git a/homeassistant/components/point/translations/sl.json b/homeassistant/components/point/translations/sl.json index 2fd65bb972..3c928935cc 100644 --- a/homeassistant/components/point/translations/sl.json +++ b/homeassistant/components/point/translations/sl.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", "external_setup": "To\u010dka uspe\u0161no konfigurirana iz drugega toka.", - "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Point. [Preberite navodila](https://www.home-assistant.io/components/point/)." + "no_flows": "Preden lahko preverite pristnost, morate konfigurirati Point. [Preberite navodila](https://www.home-assistant.io/components/point/).", + "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." }, "create_entry": { "default": "Uspe\u0161no overjen z Minut-om za va\u0161e Point naprave" diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index bbab02f959..710d363f77 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "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", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index 8fc660e812..c7dfe6d02b 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -8,7 +8,8 @@ "data": { "email": "E-Mail", "password": "Passwort" - } + }, + "description": "Wollen Sie mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/poolsense/translations/zh-Hant.json b/homeassistant/components/poolsense/translations/zh-Hant.json index 3841a173df..93a99ba1d3 100644 --- a/homeassistant/components/poolsense/translations/zh-Hant.json +++ b/homeassistant/components/poolsense/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/powerwall/translations/pt.json b/homeassistant/components/powerwall/translations/pt.json index 0c5c776056..c748619963 100644 --- a/homeassistant/components/powerwall/translations/pt.json +++ b/homeassistant/components/powerwall/translations/pt.json @@ -1,7 +1,18 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o IP" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 8cfa8bdb46..45edbf2d88 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/profiler/translations/pt.json b/homeassistant/components/profiler/translations/pt.json new file mode 100644 index 0000000000..c299020ce9 --- /dev/null +++ b/homeassistant/components/profiler/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/zh-Hant.json b/homeassistant/components/profiler/translations/zh-Hant.json index 85797ab208..c7d73c344d 100644 --- a/homeassistant/components/profiler/translations/zh-Hant.json +++ b/homeassistant/components/profiler/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json index f772a8586d..2e5bed4b66 100644 --- a/homeassistant/components/progettihwsw/translations/de.json +++ b/homeassistant/components/progettihwsw/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "relay_modes": { "data": { diff --git a/homeassistant/components/progettihwsw/translations/pt.json b/homeassistant/components/progettihwsw/translations/pt.json index 82e6756df1..072d1ea056 100644 --- a/homeassistant/components/progettihwsw/translations/pt.json +++ b/homeassistant/components/progettihwsw/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "unknown": "Erro inesperado" diff --git a/homeassistant/components/progettihwsw/translations/zh-Hant.json b/homeassistant/components/progettihwsw/translations/zh-Hant.json index 13a968114a..815ee581e6 100644 --- a/homeassistant/components/progettihwsw/translations/zh-Hant.json +++ b/homeassistant/components/progettihwsw/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index a8bdb2b9c4..a494b96abf 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -23,6 +23,7 @@ CONF_MODE = "Config Mode" CONF_AUTO = "Auto Discover" CONF_MANUAL = "Manual Entry" +LOCAL_UDP_PORT = 1988 UDP_PORT = 987 TCP_PORT = 997 PORT_MSG = {UDP_PORT: "port_987_bind_error", TCP_PORT: "port_997_bind_error"} @@ -107,8 +108,9 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): if user_input is None: # Search for device. + # If LOCAL_UDP_PORT cannot be used, a random port will be selected. devices = await self.hass.async_add_executor_job( - self.helper.has_devices, self.m_device + self.helper.has_devices, self.m_device, LOCAL_UDP_PORT ) # Abort if can't find device. @@ -147,7 +149,12 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): self.host = user_input[CONF_IP_ADDRESS] is_ready, is_login = await self.hass.async_add_executor_job( - self.helper.link, self.host, self.creds, self.pin, DEFAULT_ALIAS + self.helper.link, + self.host, + self.creds, + self.pin, + DEFAULT_ALIAS, + LOCAL_UDP_PORT, ) if is_ready is False: diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 3527a05e5b..500c243b8c 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,6 +3,6 @@ "name": "Sony PlayStation 4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": ["pyps4-2ndscreen==1.1.1"], + "requirements": ["pyps4-2ndscreen==1.2.0"], "codeowners": ["@ktnrg45"] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 8ef9413edb..24a1589db0 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -3,6 +3,7 @@ import asyncio import logging from pyps4_2ndscreen.errors import NotReady, PSDataIncomplete +from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP import pyps4_2ndscreen.ps4 as pyps4 from homeassistant.components.media_player import MediaPlayerEntity @@ -262,7 +263,7 @@ class PS4Device(MediaPlayerEntity): app_name = title.name art = title.cover_art # Assume media type is game if not app. - if title.game_type != "App": + if title.game_type != PS_TYPE_APP: media_type = MEDIA_TYPE_GAME else: media_type = MEDIA_TYPE_APP diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 71dd785b4f..5dd638a717 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -7,6 +7,7 @@ "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" }, "error": { + "cannot_connect": "Verbindungsfehler", "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.", "login_failed": "Fehler beim Koppeln mit PlayStation 4. \u00dcberpr\u00fcfe, ob die PIN korrekt ist.", "no_ipaddress": "Gib die IP-Adresse der PlayStation 4 ein, die konfiguriert werden soll." diff --git a/homeassistant/components/ps4/translations/no.json b/homeassistant/components/ps4/translations/no.json index f6f93fada0..185b0e031c 100644 --- a/homeassistant/components/ps4/translations/no.json +++ b/homeassistant/components/ps4/translations/no.json @@ -10,7 +10,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "credential_timeout": "Legitimasjonstjenesten ble tidsavbrutt. Trykk send for \u00e5 starte p\u00e5 nytt.", - "login_failed": "Kunne ikke koble til PlayStation 4. Bekreft at PIN-kode er riktig.", + "login_failed": "Kunne ikke koble til PlayStation 4. Bekreft at PIN kode er riktig.", "no_ipaddress": "Skriv inn IP adresse til PlayStation 4 du vil konfigurere." }, "step": { @@ -20,12 +20,12 @@ }, "link": { "data": { - "code": "PIN-kode", + "code": "PIN kode", "ip_address": "IP adresse", "name": "Navn", "region": "" }, - "description": "Skriv inn PlayStation 4-informasjonen din. For PIN-kode , naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Innstillinger for tilkobling av mobilapp' og velg 'Legg til enhet'. Skriv inn PIN-kode som vises. Se [dokumentasjon] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", + "description": "Skriv inn PlayStation 4-informasjonen din. For PIN kode , naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Innstillinger for tilkobling av mobilapp' og velg 'Legg til enhet'. Skriv inn PIN kode som vises. Se [dokumentasjon] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", "title": "" }, "mode": { diff --git a/homeassistant/components/ps4/translations/pt.json b/homeassistant/components/ps4/translations/pt.json index a8b6c3c6cc..5956937ac2 100644 --- a/homeassistant/components/ps4/translations/pt.json +++ b/homeassistant/components/ps4/translations/pt.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "credential_error": "Erro ao obter credenciais.", - "no_devices_found": "N\u00e3o foram encontrados dispositivos PlayStation 4 na rede." + "no_devices_found": "N\u00e3o foram encontrados dispositivos PlayStation 4 na rede.", + "port_987_bind_error": "N\u00e3o foi poss\u00edvel ligar-se \u00e0 porta 987. Consulte a [documenta\u00e7\u00e3o](https://www.home-assistant.io/components/ps4/) para obter mais informa\u00e7\u00f5es.", + "port_997_bind_error": "N\u00e3o foi poss\u00edvel ligar-se \u00e0 porta 997. Consulte a [documenta\u00e7\u00e3o](https://www.home-assistant.io/components/ps4/) para obter mais informa\u00e7\u00f5es." }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "credential_timeout": "O servi\u00e7o de credencial expirou. Pressione enviar para reiniciar.", "login_failed": "Falha ao emparelhar com a PlayStation 4. Verifique se o PIN est\u00e1 correto." }, "step": { diff --git a/homeassistant/components/ps4/translations/zh-Hans.json b/homeassistant/components/ps4/translations/zh-Hans.json index e2c38ad5d0..3c240d9613 100644 --- a/homeassistant/components/ps4/translations/zh-Hans.json +++ b/homeassistant/components/ps4/translations/zh-Hans.json @@ -24,6 +24,12 @@ }, "description": "\u8f93\u5165\u60a8\u7684 PlayStation 4 \u4fe1\u606f\u3002\u5bf9\u4e8e \"PIN\", \u8bf7\u5bfc\u822a\u5230 PlayStation 4 \u63a7\u5236\u53f0\u4e0a\u7684 \"\u8bbe\u7f6e\"\u3002\u7136\u540e\u5bfc\u822a\u5230 \"\u79fb\u52a8\u5e94\u7528\u8fde\u63a5\u8bbe\u7f6e\", \u7136\u540e\u9009\u62e9 \"\u6dfb\u52a0\u8bbe\u5907\"\u3002\u8f93\u5165\u663e\u793a\u7684 PIN\u3002", "title": "PlayStation 4" + }, + "mode": { + "data": { + "mode": "\u8bbe\u7f6e\u6a21\u5f0f" + }, + "title": "PlayStation 4" } } } diff --git a/homeassistant/components/ps4/translations/zh-Hant.json b/homeassistant/components/ps4/translations/zh-Hant.json index ddfcbef493..77bfa7bfdb 100644 --- a/homeassistant/components/ps4/translations/zh-Hant.json +++ b/homeassistant/components/ps4/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "credential_error": "\u53d6\u5f97\u6191\u8b49\u932f\u8aa4\u3002", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", "port_987_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 987\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "port_997_bind_error": "\u7121\u6cd5\u7d81\u5b9a\u901a\u8a0a\u57e0 997\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002" }, @@ -15,7 +15,7 @@ }, "step": { "creds": { - "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u8a2d\u5099\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", + "description": "\u9700\u8981\u6191\u8b49\u3002\u6309\u4e0b\u300c\u50b3\u9001\u300d\u5f8c\u3001\u65bc PS4 \u7b2c\u4e8c\u756b\u9762 App\uff0c\u66f4\u65b0\u88dd\u7f6e\u4e26\u9078\u64c7\u300cHome-Assistant\u300d\u4ee5\u7e7c\u7e8c\u3002", "title": "PlayStation 4" }, "link": { @@ -25,7 +25,7 @@ "name": "\u540d\u7a31", "region": "\u5340\u57df" }, - "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN \u78bc\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u8a2d\u5099\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", + "description": "\u8f38\u5165\u60a8\u7684 PlayStation 4 \u8cc7\u8a0a\uff0c\u300cPIN \u78bc\u300d\u65bc PlayStation 4 \u4e3b\u6a5f\u7684\u300c\u8a2d\u5b9a\u300d\u5167\uff0c\u4e26\u65bc\u300c\u884c\u52d5\u7a0b\u5f0f\u9023\u7dda\u8a2d\u5b9a\uff08Mobile App Connection Settings\uff09\u300d\u4e2d\u9078\u64c7\u300c\u65b0\u589e\u88dd\u7f6e\u300d\u3002\u8f38\u5165\u6240\u986f\u793a\u7684 PIN \u78bc\u3002\u8acb\u53c3\u8003 [documentation](https://www.home-assistant.io/components/ps4/) \u4ee5\u7372\u5f97\u66f4\u591a\u8cc7\u8a0a\u3002", "title": "PlayStation 4" }, "mode": { @@ -33,7 +33,7 @@ "ip_address": "IP \u4f4d\u5740\uff08\u5982\u679c\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22\u65b9\u5f0f\uff0c\u8acb\u4fdd\u7559\u7a7a\u767d\uff09\u3002", "mode": "\u8a2d\u5b9a\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u8a2d\u5099\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", + "description": "\u9078\u64c7\u6a21\u5f0f\u4ee5\u9032\u884c\u8a2d\u5b9a\u3002\u5047\u5982\u9078\u64c7\u81ea\u52d5\u63a2\u7d22\u6a21\u5f0f\u7684\u8a71\uff0c\u7531\u65bc\u6703\u81ea\u52d5\u9032\u884c\u88dd\u7f6e\u641c\u5c0b\uff0cIP \u4f4d\u5740\u53ef\u4fdd\u7559\u70ba\u7a7a\u767d\u3002", "title": "PlayStation 4" } } diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index fb3446fb65..32d33f19e8 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_API_KEY, CONF_NAME, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -53,14 +54,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= verify_ssl = DEFAULT_VERIFY_SSL headers = {"X-Pvoutput-Apikey": api_key, "X-Pvoutput-SystemId": system_id} - rest = RestData(method, _ENDPOINT, auth, headers, None, payload, verify_ssl) + rest = RestData(hass, method, _ENDPOINT, auth, headers, None, payload, verify_ssl) await rest.async_update() if rest.data is None: _LOGGER.error("Unable to fetch data from PVOutput") return False - async_add_entities([PvoutputSensor(rest, name)], True) + async_add_entities([PvoutputSensor(rest, name)]) class PvoutputSensor(Entity): @@ -114,13 +115,18 @@ class PvoutputSensor(Entity): async def async_update(self): """Get the latest data from the PVOutput API and updates the state.""" + await self.rest.async_update() + self._async_update_from_rest_data() + + async def async_added_to_hass(self): + """Ensure the data from the initial update is reflected in the state.""" + self._async_update_from_rest_data() + + @callback + def _async_update_from_rest_data(self): + """Update state from the rest data.""" try: - await self.rest.async_update() self.pvcoutput = self.status._make(self.rest.data.split(",")) except TypeError: self.pvcoutput = None _LOGGER.error("Unable to fetch data from PVOutput. %s", self.rest.data) - - async def async_will_remove_from_hass(self): - """Shutdown the session.""" - await self.rest.async_remove() diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/pt.json b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json new file mode 100644 index 0000000000..d252c078a2 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/pt.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index fad88a19b3..0b5af0ae43 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -2,7 +2,7 @@ "domain": "python_script", "name": "Python Scripts", "documentation": "https://www.home-assistant.io/integrations/python_script", - "requirements": ["restrictedpython==5.0"], + "requirements": ["restrictedpython==5.1"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index c839cb7593..2f3e8cf4f1 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -2,6 +2,6 @@ "domain": "qbittorrent", "name": "qBittorrent", "documentation": "https://www.home-assistant.io/integrations/qbittorrent", - "requirements": ["python-qbittorrent==0.4.1"], - "codeowners": [] + "requirements": ["python-qbittorrent==0.4.2"], + "codeowners": ["@geoffreylagaisse"] } diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 943e1b3319..721fb36fd3 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -49,8 +49,11 @@ KEY_CUSTOM_SLOPE = "customSlope" STATUS_ONLINE = "ONLINE" +MODEL_GENERATION_1 = "GENERATION1" SCHEDULE_TYPE_FIXED = "FIXED" SCHEDULE_TYPE_FLEX = "FLEX" +SERVICE_PAUSE_WATERING = "pause_watering" +SERVICE_RESUME_WATERING = "resume_watering" SERVICE_SET_ZONE_MOISTURE = "set_zone_moisture_percent" SERVICE_START_MULTIPLE_ZONES = "start_multiple_zone_schedule" diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py index 9d7c305793..c9de7eea7d 100644 --- a/homeassistant/components/rachio/device.py +++ b/homeassistant/components/rachio/device.py @@ -1,11 +1,14 @@ """Adapter to wrap the rachiopy api for home assistant.""" - import logging from typing import Optional +import voluptuous as vol + from homeassistant.const import EVENT_HOMEASSISTANT_STOP, HTTP_OK +from homeassistant.helpers import config_validation as cv from .const import ( + DOMAIN, KEY_DEVICES, KEY_ENABLED, KEY_EXTERNAL_ID, @@ -19,11 +22,27 @@ from .const import ( KEY_STATUS, KEY_USERNAME, KEY_ZONES, + MODEL_GENERATION_1, + SERVICE_PAUSE_WATERING, + SERVICE_RESUME_WATERING, ) from .webhooks import LISTEN_EVENT_TYPES, WEBHOOK_CONST_ID _LOGGER = logging.getLogger(__name__) +ATTR_DEVICES = "devices" +ATTR_DURATION = "duration" +PERMISSION_ERROR = "7" + +PAUSE_SERVICE_SCHEMA = vol.Schema( + { + vol.Optional(ATTR_DEVICES): cv.string, + vol.Optional(ATTR_DURATION, default=60): cv.positive_int, + } +) + +RESUME_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_DEVICES): cv.string}) + class RachioPerson: """Represent a Rachio user.""" @@ -39,6 +58,8 @@ class RachioPerson: def setup(self, hass): """Rachio device setup.""" + all_devices = [] + can_pause = False response = self.rachio.person.info() assert int(response[0][KEY_STATUS]) == HTTP_OK, "API key error" self._id = response[1][KEY_ID] @@ -58,18 +79,60 @@ class RachioPerson: # webhooks are normally a list, however if there is an error # rachio hands us back a dict if isinstance(webhooks, dict): - _LOGGER.error( - "Failed to add rachio controller '%s' because of an error: %s", - controller[KEY_NAME], - webhooks.get("error", "Unknown Error"), - ) + if webhooks.get("code") == PERMISSION_ERROR: + _LOGGER.info( + "Not adding controller '%s', only controllers owned by '%s' may be added", + controller[KEY_NAME], + self.username, + ) + else: + _LOGGER.error( + "Failed to add rachio controller '%s' because of an error: %s", + controller[KEY_NAME], + webhooks.get("error", "Unknown Error"), + ) continue rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) rachio_iro.setup() self._controllers.append(rachio_iro) + all_devices.append(rachio_iro.name) + # Generation 1 controllers don't support pause or resume + if rachio_iro.model.split("_")[0] != MODEL_GENERATION_1: + can_pause = True + _LOGGER.info('Using Rachio API as user "%s"', self.username) + def pause_water(service): + """Service to pause watering on all or specific controllers.""" + duration = service.data[ATTR_DURATION] + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.pause_watering(duration) + + def resume_water(service): + """Service to resume watering on all or specific controllers.""" + devices = service.data.get(ATTR_DEVICES, all_devices) + for iro in self._controllers: + if iro.name in devices: + iro.resume_watering() + + if can_pause: + hass.services.register( + DOMAIN, + SERVICE_PAUSE_WATERING, + pause_water, + schema=PAUSE_SERVICE_SCHEMA, + ) + + hass.services.register( + DOMAIN, + SERVICE_RESUME_WATERING, + resume_water, + schema=RESUME_SERVICE_SCHEMA, + ) + @property def user_id(self) -> str: """Get the user ID as defined by the Rachio API.""" @@ -102,7 +165,7 @@ class RachioIro: self._flex_schedules = data[KEY_FLEX_SCHEDULES] self._init_data = data self._webhooks = webhooks - _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) + _LOGGER.debug('%s has ID "%s"', self, self.controller_id) def setup(self): """Rachio Iro setup for webhooks.""" @@ -195,4 +258,14 @@ class RachioIro: def stop_watering(self) -> None: """Stop watering all zones connected to this controller.""" self.rachio.device.stop_water(self.controller_id) - _LOGGER.info("Stopped watering of all zones on %s", str(self)) + _LOGGER.info("Stopped watering of all zones on %s", self) + + def pause_watering(self, duration) -> None: + """Pause watering on this controller.""" + self.rachio.device.pause_zone_run(self.controller_id, duration * 60) + _LOGGER.debug("Paused watering on %s for %s minutes", self, duration) + + def resume_watering(self) -> None: + """Resume paused watering on this controller.""" + self.rachio.device.resume_zone_run(self.controller_id) + _LOGGER.debug("Resuming watering on %s", self) diff --git a/homeassistant/components/rachio/services.yaml b/homeassistant/components/rachio/services.yaml index 480d53aa45..815a860131 100644 --- a/homeassistant/components/rachio/services.yaml +++ b/homeassistant/components/rachio/services.yaml @@ -16,3 +16,18 @@ start_multiple_zone_schedule: duration: description: Number of minutes to run the zone(s). If only 1 duration is given, that time will be used for all zones. If given a list of durations, the durations will apply to the respective zone listed above. [Required] example: 15, 20 +pause_watering: + description: Pause any currently running zones or schedules. + fields: + devices: + description: Name of controllers to pause. Defaults to all controllers on the account if not provided. [Optional] + example: Main House + duration: + description: The number of minutes to pause running schedules. Accepts 1-60. Default is 60 minutes. [Optional] + example: 30 +resume_watering: + description: Resume any paused zone runs or schedules. + fields: + devices: + description: Name of controllers to resume. Defaults to all controllers on the account if not provided. [Optional] + example: Main House diff --git a/homeassistant/components/rachio/translations/pt.json b/homeassistant/components/rachio/translations/pt.json index 4c01137c49..626909b9b2 100644 --- a/homeassistant/components/rachio/translations/pt.json +++ b/homeassistant/components/rachio/translations/pt.json @@ -1,12 +1,17 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "user": { "data": { - "api_key": "Chave de API" + "api_key": "API Key" } } } diff --git a/homeassistant/components/rachio/translations/zh-Hant.json b/homeassistant/components/rachio/translations/zh-Hant.json index f8dbde4691..b800daee77 100644 --- a/homeassistant/components/rachio/translations/zh-Hant.json +++ b/homeassistant/components/rachio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -14,7 +14,7 @@ "api_key": "API \u5bc6\u9470" }, "description": "\u5c07\u6703\u9700\u8981\u7531 https://app.rach.io/ \u53d6\u5f97 App \u5bc6\u9470\u3002\u9078\u64c7\u8a2d\u5b9a\u4e26\u9078\u64c7\u7372\u5f97\u5bc6\u9470\uff08GET API KEY\uff09\u3002", - "title": "\u9023\u7dda\u81f3 Rachio \u8a2d\u5099" + "title": "\u9023\u7dda\u81f3 Rachio \u88dd\u7f6e" } } }, diff --git a/homeassistant/components/rainmachine/translations/pt.json b/homeassistant/components/rainmachine/translations/pt.json index e6c1baf1ca..1102078fc6 100644 --- a/homeassistant/components/rainmachine/translations/pt.json +++ b/homeassistant/components/rainmachine/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/rainmachine/translations/zh-Hant.json b/homeassistant/components/rainmachine/translations/zh-Hant.json index cefc44956c..9b5829cf20 100644 --- a/homeassistant/components/rainmachine/translations/zh-Hant.json +++ b/homeassistant/components/rainmachine/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/recollect_waste/__init__.py b/homeassistant/components/recollect_waste/__init__.py index 57bd346c91..0600d73d8a 100644 --- a/homeassistant/components/recollect_waste/__init__.py +++ b/homeassistant/components/recollect_waste/__init__.py @@ -1,4 +1,4 @@ -"""The Recollect Waste integration.""" +"""The ReCollect Waste integration.""" import asyncio from datetime import date, timedelta from typing import List @@ -14,6 +14,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DATA_COORDINATOR, DOMAIN, LOGGER +DATA_LISTENER = "listener" + DEFAULT_NAME = "recollect_waste" DEFAULT_UPDATE_INTERVAL = timedelta(days=1) @@ -22,7 +24,7 @@ PLATFORMS = ["sensor"] async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the RainMachine component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}} + hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_LISTENER: {}} return True @@ -41,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) except RecollectError as err: raise UpdateFailed( - f"Error while requesting data from Recollect: {err}" + f"Error while requesting data from ReCollect: {err}" ) from err coordinator = DataUpdateCoordinator( @@ -64,9 +66,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_forward_entry_setup(entry, component) ) + hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( + async_reload_entry + ) + return True +async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Handle an options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an RainMachine config entry.""" unload_ok = all( @@ -79,5 +90,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) if unload_ok: hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) + cancel_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id) + cancel_listener() return unload_ok diff --git a/homeassistant/components/recollect_waste/config_flow.py b/homeassistant/components/recollect_waste/config_flow.py index f0d1527a0f..8e208f57cc 100644 --- a/homeassistant/components/recollect_waste/config_flow.py +++ b/homeassistant/components/recollect_waste/config_flow.py @@ -1,9 +1,13 @@ -"""Config flow for Recollect Waste integration.""" +"""Config flow for ReCollect Waste integration.""" +from typing import Optional + from aiorecollect.client import Client from aiorecollect.errors import RecollectError import voluptuous as vol from homeassistant import config_entries +from homeassistant.const import CONF_FRIENDLY_NAME +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import ( # pylint:disable=unused-import @@ -19,11 +23,19 @@ DATA_SCHEMA = vol.Schema( class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): - """Handle a config flow for Recollect Waste.""" + """Handle a config flow for ReCollect Waste.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Define the config flow to handle options.""" + return RecollectWasteOptionsFlowHandler(config_entry) + async def async_step_import(self, import_config: dict = None) -> dict: """Handle configuration via YAML import.""" return await self.async_step_user(import_config) @@ -62,3 +74,28 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_SERVICE_ID: user_input[CONF_SERVICE_ID], }, ) + + +class RecollectWasteOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a Recollect Waste options flow.""" + + def __init__(self, entry: config_entries.ConfigEntry): + """Initialize.""" + self._entry = entry + + async def async_step_init(self, user_input: Optional[dict] = None): + """Manage the 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_FRIENDLY_NAME, + default=self._entry.options.get(CONF_FRIENDLY_NAME), + ): bool + } + ), + ) diff --git a/homeassistant/components/recollect_waste/const.py b/homeassistant/components/recollect_waste/const.py index 8012bdbb02..4a6c9dbda6 100644 --- a/homeassistant/components/recollect_waste/const.py +++ b/homeassistant/components/recollect_waste/const.py @@ -1,4 +1,4 @@ -"""Define Recollect Waste constants.""" +"""Define ReCollect Waste constants.""" import logging DOMAIN = "recollect_waste" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 53304c9321..d66c2aae0e 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,11 +1,12 @@ -"""Support for Recollect Waste sensors.""" -from typing import Callable +"""Support for ReCollect Waste sensors.""" +from typing import Callable, List +from aiorecollect.client import PickupType import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_FRIENDLY_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.update_coordinator import ( @@ -20,7 +21,7 @@ ATTR_AREA_NAME = "area_name" ATTR_NEXT_PICKUP_TYPES = "next_pickup_types" ATTR_NEXT_PICKUP_DATE = "next_pickup_date" -DEFAULT_ATTRIBUTION = "Pickup data provided by Recollect Waste" +DEFAULT_ATTRIBUTION = "Pickup data provided by ReCollect Waste" DEFAULT_NAME = "recollect_waste" DEFAULT_ICON = "mdi:trash-can-outline" @@ -35,15 +36,28 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +@callback +def async_get_pickup_type_names( + entry: ConfigEntry, pickup_types: List[PickupType] +) -> List[str]: + """Return proper pickup type names from their associated objects.""" + return [ + t.friendly_name + if entry.options.get(CONF_FRIENDLY_NAME) and t.friendly_name + else t.name + for t in pickup_types + ] + + async def async_setup_platform( hass: HomeAssistant, config: dict, async_add_entities: Callable, discovery_info: dict = None, ): - """Import Awair configuration from YAML.""" + """Import Recollect Waste configuration from YAML.""" LOGGER.warning( - "Loading Recollect Waste via platform setup is deprecated. " + "Loading ReCollect Waste via platform setup is deprecated. " "Please remove it from your configuration." ) hass.async_create_task( @@ -58,20 +72,19 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable ) -> None: - """Set up Recollect Waste sensors based on a config entry.""" + """Set up ReCollect Waste sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] - async_add_entities([RecollectWasteSensor(coordinator, entry)]) + async_add_entities([ReCollectWasteSensor(coordinator, entry)]) -class RecollectWasteSensor(CoordinatorEntity): - """Recollect Waste Sensor.""" +class ReCollectWasteSensor(CoordinatorEntity): + """ReCollect Waste Sensor.""" def __init__(self, coordinator: DataUpdateCoordinator, entry: ConfigEntry) -> None: """Initialize the sensor.""" super().__init__(coordinator) self._attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._place_id = entry.data[CONF_PLACE_ID] - self._service_id = entry.data[CONF_SERVICE_ID] + self._entry = entry self._state = None @property @@ -97,7 +110,7 @@ class RecollectWasteSensor(CoordinatorEntity): @property def unique_id(self) -> str: """Return a unique ID.""" - return f"{self._place_id}{self._service_id}" + return f"{self._entry.data[CONF_PLACE_ID]}{self._entry.data[CONF_SERVICE_ID]}" @callback def _handle_coordinator_update(self) -> None: @@ -120,11 +133,13 @@ class RecollectWasteSensor(CoordinatorEntity): self._state = pickup_event.date self._attributes.update( { - ATTR_PICKUP_TYPES: [t.name for t in pickup_event.pickup_types], + ATTR_PICKUP_TYPES: async_get_pickup_type_names( + self._entry, pickup_event.pickup_types + ), ATTR_AREA_NAME: pickup_event.area_name, - ATTR_NEXT_PICKUP_TYPES: [ - t.name for t in next_pickup_event.pickup_types - ], + ATTR_NEXT_PICKUP_TYPES: async_get_pickup_type_names( + self._entry, next_pickup_event.pickup_types + ), ATTR_NEXT_PICKUP_DATE: next_date, } ) diff --git a/homeassistant/components/recollect_waste/strings.json b/homeassistant/components/recollect_waste/strings.json index 0cd251c737..a350b9880f 100644 --- a/homeassistant/components/recollect_waste/strings.json +++ b/homeassistant/components/recollect_waste/strings.json @@ -14,5 +14,15 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "options": { + "step": { + "init": { + "title": "Configure Recollect Waste", + "data": { + "friendly_name": "Use friendly names for pickup types (when possible)" + } + } + } } } diff --git a/homeassistant/components/recollect_waste/translations/ca.json b/homeassistant/components/recollect_waste/translations/ca.json index 395fe5b9da..33d3ddbfaf 100644 --- a/homeassistant/components/recollect_waste/translations/ca.json +++ b/homeassistant/components/recollect_waste/translations/ca.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Utilitza els sobrenoms per als tipus de recollida (quan sigui possible)" + }, + "title": "Configuraci\u00f3 de Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/en.json b/homeassistant/components/recollect_waste/translations/en.json index 28d73d189b..e9deabec71 100644 --- a/homeassistant/components/recollect_waste/translations/en.json +++ b/homeassistant/components/recollect_waste/translations/en.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Use friendly names for pickup types (when possible)" + }, + "title": "Configure Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/es.json b/homeassistant/components/recollect_waste/translations/es.json index 5771c9da9a..2fdeb991bf 100644 --- a/homeassistant/components/recollect_waste/translations/es.json +++ b/homeassistant/components/recollect_waste/translations/es.json @@ -14,5 +14,14 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Utilizar nombres descriptivos para los tipos de recogida (cuando sea posible)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/et.json b/homeassistant/components/recollect_waste/translations/et.json index e1402d12e4..8bbc0de94b 100644 --- a/homeassistant/components/recollect_waste/translations/et.json +++ b/homeassistant/components/recollect_waste/translations/et.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "S\u00f5bralike nimede kasutamine pr\u00fcgi t\u00fc\u00fcpide puhul (kui see on v\u00f5imalik)" + }, + "title": "Seadista Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/it.json b/homeassistant/components/recollect_waste/translations/it.json index d52e7be128..5c9a9157ba 100644 --- a/homeassistant/components/recollect_waste/translations/it.json +++ b/homeassistant/components/recollect_waste/translations/it.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Usa nomi descrittivi per i tipi di ritiro (quando possibile)" + }, + "title": "Configura la raccolta dei rifiuti" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/no.json b/homeassistant/components/recollect_waste/translations/no.json index 6c4932505b..eb9f73fef9 100644 --- a/homeassistant/components/recollect_waste/translations/no.json +++ b/homeassistant/components/recollect_waste/translations/no.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "Bruk vennlige navn for hentetyper (n\u00e5r det er mulig)" + }, + "title": "Konfigurer Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/pt.json b/homeassistant/components/recollect_waste/translations/pt.json index 57e7ea502f..31e872a71c 100644 --- a/homeassistant/components/recollect_waste/translations/pt.json +++ b/homeassistant/components/recollect_waste/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/recollect_waste/translations/ru.json b/homeassistant/components/recollect_waste/translations/ru.json index 21e926ec9e..c90c1aefee 100644 --- a/homeassistant/components/recollect_waste/translations/ru.json +++ b/homeassistant/components/recollect_waste/translations/ru.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u043e\u043d\u044f\u0442\u043d\u044b\u0435 \u0438\u043c\u0435\u043d\u0430 \u0434\u043b\u044f \u0442\u0438\u043f\u043e\u0432 \u043f\u043e\u0434\u0431\u043e\u0440\u0449\u0438\u043a\u0430 (\u0435\u0441\u043b\u0438 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e)" + }, + "title": "Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/sl.json b/homeassistant/components/recollect_waste/translations/sl.json index cae09d7762..480780db0a 100644 --- a/homeassistant/components/recollect_waste/translations/sl.json +++ b/homeassistant/components/recollect_waste/translations/sl.json @@ -10,5 +10,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u010ce je to mogo\u010de, uporabi prijazna imena za vrste pobiranja." + }, + "title": "Nastavi ponovno rabo zavr\u017eenega" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/zh-Hant.json b/homeassistant/components/recollect_waste/translations/zh-Hant.json index 7ce887b05c..75615c1cce 100644 --- a/homeassistant/components/recollect_waste/translations/zh-Hant.json +++ b/homeassistant/components/recollect_waste/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_place_or_service_id": "\u5730\u9ede\u6216\u670d\u52d9 ID \u7121\u6548" @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u91dd\u5c0d\u9078\u53d6\u985e\u578b\u4f7f\u7528\u53cb\u5584\u540d\u7a31\uff08\u5047\u5982\u9069\u7528\uff09" + }, + "title": "\u8a2d\u5b9a Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index c633c114b4..4501b25385 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -211,7 +211,13 @@ def _update_states_table_with_foreign_key_options(engine): inspector = reflection.Inspector.from_engine(engine) alters = [] for foreign_key in inspector.get_foreign_keys(TABLE_STATES): - if foreign_key["name"] and not foreign_key["options"]: + if foreign_key["name"] and ( + # MySQL/MariaDB will have empty options + not foreign_key["options"] + or + # Postgres will have ondelete set to None + foreign_key["options"].get("ondelete") is None + ): alters.append( { "old_fk": ForeignKeyConstraint((), (), name=foreign_key["name"]), @@ -312,6 +318,10 @@ def _apply_update(engine, new_version, old_version): _create_index(engine, "events", "ix_events_event_type_time_fired") _drop_index(engine, "events", "ix_events_event_type") elif new_version == 10: + # Now done in step 11 + pass + elif new_version == 11: + _create_index(engine, "states", "ix_states_old_state_id") _update_states_table_with_foreign_key_options(engine) else: raise ValueError(f"No schema migration defined for version {new_version}") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 5b37f7e3f9..9481e954bd 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -25,7 +25,7 @@ import homeassistant.util.dt as dt_util # pylint: disable=invalid-name Base = declarative_base() -SCHEMA_VERSION = 10 +SCHEMA_VERSION = 11 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index ed7f5affc5..abf1426868 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -149,7 +149,7 @@ def basic_sanity_check(cursor): """Check tables to make sure select does not fail.""" for table in ALL_TABLES: - cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # sec: not injection + cursor.execute(f"SELECT * FROM {table} LIMIT 1;") # nosec # not injection return True diff --git a/homeassistant/components/rest/binary_sensor.py b/homeassistant/components/rest/binary_sensor.py index 7f0f920b84..49c10354c5 100644 --- a/homeassistant/components/rest/binary_sensor.py +++ b/homeassistant/components/rest/binary_sensor.py @@ -101,9 +101,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= auth = None rest = RestData( - method, resource, auth, headers, params, payload, verify_ssl, timeout + hass, method, resource, auth, headers, params, payload, verify_ssl, timeout ) await rest.async_update() + if rest.data is None: raise PlatformNotReady @@ -119,7 +120,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= resource_template, ) ], - True, ) @@ -187,10 +187,6 @@ class RestBinarySensor(BinarySensorEntity): """Force update.""" return self._force_update - async def async_will_remove_from_hass(self): - """Shutdown the session.""" - await self.rest.async_remove() - async def async_update(self): """Get the latest data from REST API and updates the state.""" if self._resource_template is not None: diff --git a/homeassistant/components/rest/data.py b/homeassistant/components/rest/data.py index bd35383e98..dd2e29616c 100644 --- a/homeassistant/components/rest/data.py +++ b/homeassistant/components/rest/data.py @@ -3,6 +3,8 @@ import logging import httpx +from homeassistant.helpers.httpx_client import get_async_client + DEFAULT_TIMEOUT = 10 _LOGGER = logging.getLogger(__name__) @@ -13,6 +15,7 @@ class RestData: def __init__( self, + hass, method, resource, auth, @@ -23,6 +26,7 @@ class RestData: timeout=DEFAULT_TIMEOUT, ): """Initialize the data object.""" + self._hass = hass self._method = method self._resource = resource self._auth = auth @@ -35,11 +39,6 @@ class RestData: self.data = None self.headers = None - async def async_remove(self): - """Destroy the http session on destroy.""" - if self._async_client: - await self._async_client.aclose() - def set_url(self, url): """Set url.""" self._resource = url @@ -47,7 +46,9 @@ class RestData: async def async_update(self): """Get the latest data from REST service with provided method.""" if not self._async_client: - self._async_client = httpx.AsyncClient(verify=self._verify_ssl) + self._async_client = get_async_client( + self._hass, verify_ssl=self._verify_ssl + ) _LOGGER.debug("Updating from %s", self._resource) try: diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index 3e4f97d5bc..f15df42864 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -30,6 +30,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.reload import setup_reload_service +from homeassistant.helpers.template import Template from . import DOMAIN, PLATFORMS @@ -56,8 +57,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_TARGET_PARAMETER_NAME): cv.string, vol.Optional(CONF_TITLE_PARAMETER_NAME): cv.string, - vol.Optional(CONF_DATA): dict, - vol.Optional(CONF_DATA_TEMPLATE): {cv.match_all: cv.template_complex}, + vol.Optional(CONF_DATA): vol.All(dict, cv.template_complex), + vol.Optional(CONF_DATA_TEMPLATE): vol.All(dict, cv.template_complex), vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), @@ -155,9 +156,7 @@ class RestNotificationService(BaseNotificationService): # integrations, so just return the first target in the list. data[self._target_param_name] = kwargs[ATTR_TARGET][0] - if self._data: - data.update(self._data) - elif self._data_template: + if self._data_template or self._data: kwargs[ATTR_MESSAGE] = message def _data_template_creator(value): @@ -168,10 +167,15 @@ class RestNotificationService(BaseNotificationService): return { key: _data_template_creator(item) for key, item in value.items() } + if not isinstance(value, Template): + return value value.hass = self._hass return value.async_render(kwargs, parse_result=False) - data.update(_data_template_creator(self._data_template)) + if self._data: + data.update(_data_template_creator(self._data)) + if self._data_template: + data.update(_data_template_creator(self._data_template)) if self._method == "POST": response = requests.post( @@ -197,7 +201,7 @@ class RestNotificationService(BaseNotificationService): response = requests.get( self._resource, headers=self._headers, - params=self._params.update(data), + params={**self._params, **data} if self._params else data, timeout=10, auth=self._auth, verify=self._verify_ssl, diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index f048eaa3b4..85d79b6b33 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -116,8 +116,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= else: auth = None rest = RestData( - method, resource, auth, headers, params, payload, verify_ssl, timeout + hass, method, resource, auth, headers, params, payload, verify_ssl, timeout ) + await rest.async_update() if rest.data is None: @@ -140,7 +141,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= json_attrs_path, ) ], - True, ) @@ -210,7 +210,14 @@ class RestSensor(Entity): self.rest.set_url(self._resource_template.async_render(parse_result=False)) await self.rest.async_update() + self._update_from_rest_data() + async def async_added_to_hass(self): + """Ensure the data from the initial update is reflected in the state.""" + self._update_from_rest_data() + + def _update_from_rest_data(self): + """Update state from the rest data.""" value = self.rest.data _LOGGER.debug("Data fetched from resource: %s", value) if self.rest.headers is not None: @@ -220,6 +227,7 @@ class RestSensor(Entity): if content_type and ( content_type.startswith("text/xml") or content_type.startswith("application/xml") + or content_type.startswith("application/xhtml+xml") ): try: value = json.dumps(xmltodict.parse(value)) @@ -266,10 +274,6 @@ class RestSensor(Entity): self._state = value - async def async_will_remove_from_hass(self): - """Shutdown the session.""" - await self.rest.async_remove() - @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index 6d934ed0e6..1979a10cb8 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert. Nur eine Konfiguration m\u00f6glich." + "already_configured": "Ger\u00e4t ist bereits konfiguriert. Nur eine Konfiguration m\u00f6glich.", + "cannot_connect": "Verbindungsfehler" + }, + "error": { + "cannot_connect": "Verbindungsfehler" }, "step": { "setup_network": { @@ -25,6 +29,9 @@ } }, "options": { + "error": { + "unknown": "Unerwarteter Fehler" + }, "step": { "prompt_options": { "data": { diff --git a/homeassistant/components/rfxtrx/translations/hu.json b/homeassistant/components/rfxtrx/translations/hu.json index 964b143f1d..4e04ab16ce 100644 --- a/homeassistant/components/rfxtrx/translations/hu.json +++ b/homeassistant/components/rfxtrx/translations/hu.json @@ -2,9 +2,23 @@ "config": { "abort": { "already_configured": "Az eszk\u00f6z m\u00e1r konfigur\u00e1lva van" + }, + "step": { + "setup_serial": { + "data": { + "device": "Eszk\u00f6z kiv\u00e1laszt\u00e1sa" + }, + "title": "Eszk\u00f6z" + }, + "setup_serial_manual_path": { + "title": "El\u00e9r\u00e9si \u00fat" + } } }, "options": { + "error": { + "invalid_event_code": "\u00c9rv\u00e9nytelen esem\u00e9nyk\u00f3d" + }, "step": { "prompt_options": { "data": { diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json index 3323384072..752136dac7 100644 --- a/homeassistant/components/rfxtrx/translations/no.json +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -25,7 +25,7 @@ "data": { "device": "USB enhetsbane" }, - "title": "Sti" + "title": "Bane" }, "user": { "data": { diff --git a/homeassistant/components/rfxtrx/translations/pt.json b/homeassistant/components/rfxtrx/translations/pt.json index ce8a928727..c962097e6a 100644 --- a/homeassistant/components/rfxtrx/translations/pt.json +++ b/homeassistant/components/rfxtrx/translations/pt.json @@ -1,7 +1,30 @@ { "config": { "abort": { - "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "setup_network": { + "data": { + "host": "Servidor", + "port": "Porta" + } + }, + "setup_serial_manual_path": { + "data": { + "device": "Caminho do Dispositivo USB" + } + } + } + }, + "options": { + "error": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "unknown": "Erro inesperado" } } } \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/sv.json b/homeassistant/components/rfxtrx/translations/sv.json new file mode 100644 index 0000000000..bdafd86c9a --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/sv.json @@ -0,0 +1,66 @@ +{ + "config": { + "step": { + "setup_network": { + "data": { + "host": "V\u00e4rdnamn", + "port": "Port" + }, + "title": "V\u00e4lj anslutningsadress" + }, + "setup_serial": { + "data": { + "device": "V\u00e4lj enhet" + }, + "title": "Enhet" + }, + "setup_serial_manual_path": { + "data": { + "device": "USB-s\u00f6kv\u00e4g" + }, + "title": "S\u00f6kv\u00e4g" + }, + "user": { + "data": { + "type": "Anslutningstyp" + }, + "title": "V\u00e4lj anslutningstyp" + } + } + }, + "options": { + "error": { + "already_configured_device": "Enheten \u00e4r redan konfigurerad", + "invalid_event_code": "Ogiltig h\u00e4ndelsekod", + "invalid_input_2262_off": "Ogiltig v\u00e4rde f\u00f6r av-kommando", + "invalid_input_2262_on": "Ogiltig v\u00e4rde f\u00f6r p\u00e5-kommando", + "invalid_input_off_delay": "Ogiltigt v\u00e4rde f\u00f6r avst\u00e4ngningsf\u00f6rdr\u00f6jning", + "unknown": "Ok\u00e4nt fel" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "Aktivera automatisk till\u00e4gg av enheter", + "debug": "Aktivera fels\u00f6kning", + "device": "V\u00e4lj enhet att konfigurera", + "event_code": "Ange h\u00e4ndelsekod att l\u00e4gga till", + "remove_device": "V\u00e4lj enhet som ska tas bort" + }, + "title": "Rfxtrx-alternativ" + }, + "set_device_options": { + "data": { + "command_off": "Databitv\u00e4rde f\u00f6r av-kommando", + "command_on": "Databitv\u00e4rde f\u00f6r p\u00e5-kommando", + "data_bit": "Antal databitar", + "fire_event": "Aktivera enhetsh\u00e4ndelse", + "off_delay": "Avst\u00e4ngningsf\u00f6rdr\u00f6jning", + "off_delay_enabled": "Aktivera avst\u00e4ngningsf\u00f6rdr\u00f6jning", + "replace_device": "V\u00e4lj enhet att ers\u00e4tta", + "signal_repetitions": "Antal signalrepetitioner" + }, + "title": "Konfigurera enhetsalternativ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 827def7f30..3da2e5f538 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "already_configured": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { @@ -17,13 +17,13 @@ }, "setup_serial": { "data": { - "device": "\u9078\u64c7\u8a2d\u5099" + "device": "\u9078\u64c7\u88dd\u7f6e" }, - "title": "\u8a2d\u5099" + "title": "\u88dd\u7f6e" }, "setup_serial_manual_path": { "data": { - "device": "USB \u8a2d\u5099\u8def\u5f91" + "device": "USB \u88dd\u7f6e\u8def\u5f91" }, "title": "\u8def\u5f91" }, @@ -37,7 +37,7 @@ }, "options": { "error": { - "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "invalid_event_code": "\u4e8b\u4ef6\u4ee3\u78bc\u7121\u6548", "invalid_input_2262_off": "\u547d\u4ee4\u95dc\u9589\u8f38\u5165\u7121\u6548", "invalid_input_2262_on": "\u547d\u4ee4\u958b\u555f\u8f38\u5165\u7121\u6548", @@ -49,9 +49,9 @@ "data": { "automatic_add": "\u958b\u555f\u81ea\u52d5\u65b0\u589e", "debug": "\u958b\u555f\u9664\u932f", - "device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u8a2d\u5b9a", + "device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a", "event_code": "\u8f38\u5165\u4e8b\u4ef6\u4ee3\u78bc\u4ee5\u65b0\u589e", - "remove_device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u522a\u9664" + "remove_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u522a\u9664" }, "title": "Rfxtrx \u9078\u9805" }, @@ -60,13 +60,13 @@ "command_off": "\u547d\u4ee4\u95dc\u9589\u7684\u8cc7\u6599\u4f4d\u5143\u503c", "command_on": "\u547d\u4ee4\u958b\u555f\u7684\u8cc7\u6599\u4f4d\u5143\u503c", "data_bit": "\u8cc7\u6599\u4f4d\u5143\u6578", - "fire_event": "\u958b\u555f\u8a2d\u5099\u4e8b\u4ef6", + "fire_event": "\u958b\u555f\u88dd\u7f6e\u4e8b\u4ef6", "off_delay": "\u5ef6\u9072", "off_delay_enabled": "\u958b\u555f\u5ef6\u9072", - "replace_device": "\u9078\u64c7\u8a2d\u5099\u4ee5\u53d6\u4ee3", + "replace_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u53d6\u4ee3", "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578" }, - "title": "\u8a2d\u5b9a\u8a2d\u5099\u9078\u9805" + "title": "\u8a2d\u5b9a\u88dd\u7f6e\u9078\u9805" } } } diff --git a/homeassistant/components/ring/translations/no.json b/homeassistant/components/ring/translations/no.json index 566568ce37..b1561285eb 100644 --- a/homeassistant/components/ring/translations/no.json +++ b/homeassistant/components/ring/translations/no.json @@ -10,7 +10,7 @@ "step": { "2fa": { "data": { - "2fa": "To-faktorskode" + "2fa": "Totrinnsbekreftelse kode" }, "title": "Totrinnsbekreftelse" }, diff --git a/homeassistant/components/ring/translations/pt.json b/homeassistant/components/ring/translations/pt.json index 4a071063d4..0918f2cca1 100644 --- a/homeassistant/components/ring/translations/pt.json +++ b/homeassistant/components/ring/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/ring/translations/zh-Hant.json b/homeassistant/components/ring/translations/zh-Hant.json index b9a66540c3..5eb31bfa79 100644 --- a/homeassistant/components/ring/translations/zh-Hant.json +++ b/homeassistant/components/ring/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index a2c942db57..ad863f7ff7 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/risco/translations/no.json b/homeassistant/components/risco/translations/no.json index 5e57cd1464..758c5c68bb 100644 --- a/homeassistant/components/risco/translations/no.json +++ b/homeassistant/components/risco/translations/no.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Passord", - "pin": "PIN-kode", + "pin": "PIN kode", "username": "Brukernavn" } } @@ -32,8 +32,8 @@ }, "init": { "data": { - "code_arm_required": "Krev PIN-kode for \u00e5 tilkoble", - "code_disarm_required": "Krev PIN-kode for \u00e5 frakoble", + "code_arm_required": "Krev PIN kode for \u00e5 tilkoble", + "code_disarm_required": "Krev PIN kode for \u00e5 frakoble", "scan_interval": "Hvor ofte skal man unders\u00f8ke Risco (i l\u00f8pet av sekunder)" }, "title": "Konfigurer alternativer" diff --git a/homeassistant/components/risco/translations/zh-Hant.json b/homeassistant/components/risco/translations/zh-Hant.json index 9509ace654..c76871bcec 100644 --- a/homeassistant/components/risco/translations/zh-Hant.json +++ b/homeassistant/components/risco/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 6e494ce269..f8e9034292 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -85,6 +85,36 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) + async def async_step_homekit(self, discovery_info): + """Handle a flow initialized by homekit discovery.""" + + # If we already have the host configured do + # not open connections to it if we can avoid it. + if self._host_already_configured(discovery_info[CONF_HOST]): + return self.async_abort(reason="already_configured") + + self.discovery_info.update({CONF_HOST: discovery_info[CONF_HOST]}) + + try: + info = await validate_input(self.hass, self.discovery_info) + except RokuError: + _LOGGER.debug("Roku Error", exc_info=True) + return self.async_abort(reason=ERROR_CANNOT_CONNECT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error trying to connect") + return self.async_abort(reason=ERROR_UNKNOWN) + + await self.async_set_unique_id(info["serial_number"]) + self._abort_if_unique_id_configured( + updates={CONF_HOST: discovery_info[CONF_HOST]}, + ) + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update({"title_placeholders": {"name": info["title"]}}) + self.discovery_info.update({CONF_NAME: info["title"]}) + + return await self.async_step_discovery_confirm() + async def async_step_ssdp( self, discovery_info: Optional[Dict] = None ) -> Dict[str, Any]: @@ -110,16 +140,16 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unknown error trying to connect") return self.async_abort(reason=ERROR_UNKNOWN) - return await self.async_step_ssdp_confirm() + return await self.async_step_discovery_confirm() - async def async_step_ssdp_confirm( + async def async_step_discovery_confirm( self, user_input: Optional[Dict] = None ) -> Dict[str, Any]: """Handle user-confirmation of discovered device.""" # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 if user_input is None: return self.async_show_form( - step_id="ssdp_confirm", + step_id="discovery_confirm", description_placeholders={"name": self.discovery_info[CONF_NAME]}, errors={}, ) @@ -128,3 +158,12 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): title=self.discovery_info[CONF_NAME], data=self.discovery_info, ) + + def _host_already_configured(self, host): + """See if we already have a hub with the host address configured.""" + existing_hosts = { + entry.data[CONF_HOST] + for entry in self._async_current_entries() + if CONF_HOST in entry.data + } + return host in existing_hosts diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 39b48b91a8..682576b534 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -3,6 +3,14 @@ "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": ["rokuecp==0.6.0"], + "homekit": { + "models": [ + "3810X", + "4660X", + "7820X", + "C105X" + ] + }, "ssdp": [ { "st": "roku:ecp", diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 6d9000b866..55b533d4f1 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -8,7 +8,7 @@ "host": "[%key:common::config_flow::data::host%]" } }, - "ssdp_confirm": { + "discovery_confirm": { "title": "Roku", "description": "Do you want to set up {name}?", "data": {} diff --git a/homeassistant/components/roku/translations/pt.json b/homeassistant/components/roku/translations/pt.json index 7880adf5ff..e67de50945 100644 --- a/homeassistant/components/roku/translations/pt.json +++ b/homeassistant/components/roku/translations/pt.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o" }, + "flow_title": "Roku: {name}", "step": { + "ssdp_confirm": { + "title": "Roku" + }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index 94e6d6cb48..cfa3a4aa3b 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 794c67454f..932e5cadd7 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -13,7 +13,7 @@ "password": "\u5bc6\u78bc" }, "description": "\u76ee\u524d\u63a5\u6536 BLID \u8207\u5bc6\u78bc\u70ba\u624b\u52d5\u904e\u7a0b\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1ahttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", - "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + "title": "\u9023\u7dda\u81f3\u88dd\u7f6e" } } }, diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 9e6453222a..9918e38670 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -1,7 +1,8 @@ { "config": { "error": { - "duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt." + "duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt.", + "unknown": "Unerwarteter Fehler" } } } \ No newline at end of file diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json index acfb900218..9067e2c6f5 100644 --- a/homeassistant/components/roon/translations/no.json +++ b/homeassistant/components/roon/translations/no.json @@ -10,8 +10,8 @@ }, "step": { "link": { - "description": "Du m\u00e5 autorisere home assistant i Roon. N\u00e5r du klikker send inn, g\u00e5r du til Roon Core-programmet, \u00e5pner Innstillinger og aktiverer HomeAssistant p\u00e5 Utvidelser-fanen.", - "title": "Autoriser HomeAssistant i Roon" + "description": "Du m\u00e5 godkjenne Home Assistant i Roon. N\u00e5r du klikker send inn, g\u00e5r du til Roon Core-programmet, \u00e5pner innstillingene og aktiverer Home Assistant p\u00e5 utvidelser-fanen.", + "title": "Autoriser Home Assistant i Roon" }, "user": { "data": { diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index 00b152205f..f34bce445f 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "duplicate_entry": "\u8a72\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u3002", diff --git a/homeassistant/components/rpi_power/translations/pt.json b/homeassistant/components/rpi_power/translations/pt.json new file mode 100644 index 0000000000..9890048c36 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ 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 index 37dbb151d8..05cdeb6852 100644 --- a/homeassistant/components/rpi_power/translations/zh-Hant.json +++ b/homeassistant/components/rpi_power/translations/zh-Hant.json @@ -2,7 +2,7 @@ "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" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/ruckus_unleashed/translations/de.json b/homeassistant/components/ruckus_unleashed/translations/de.json index 1b5c5cb760..ae15ec058b 100644 --- a/homeassistant/components/ruckus_unleashed/translations/de.json +++ b/homeassistant/components/ruckus_unleashed/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindungsfehler", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/ruckus_unleashed/translations/pt.json b/homeassistant/components/ruckus_unleashed/translations/pt.json new file mode 100644 index 0000000000..561c8d7728 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "host": "Servidor", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json index 8bf65ef6ee..cad7d736a9 100644 --- a/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json +++ b/homeassistant/components/ruckus_unleashed/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index c083048e4c..e335426763 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Dieser Samsung TV ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.", "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", + "cannot_connect": "Verbindungsfehler", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." }, "flow_title": "Samsung TV: {model}", diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index 5ce246347c..15e61d2362 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -1,6 +1,15 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "flow_title": "TV Samsung: {model}", "step": { + "confirm": { + "title": "TV Samsung" + }, "user": { "data": { "host": "Servidor", diff --git a/homeassistant/components/samsungtv/translations/zh-Hant.json b/homeassistant/components/samsungtv/translations/zh-Hant.json index e932e18a2b..00b442399c 100644 --- a/homeassistant/components/samsungtv/translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u8a2d\u5b9a\u4ee5\u76e1\u8208\u9a57\u8b49\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b76995fe39..e8b6fcfd2c 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -78,7 +78,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= auth = HTTPBasicAuth(username, password) else: auth = None - rest = RestData(method, resource, auth, headers, None, payload, verify_ssl) + rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl) await rest.async_update() if rest.data is None: @@ -137,6 +137,14 @@ class ScrapeSensor(Entity): async def async_update(self): """Get the latest data from the source and updates the state.""" await self.rest.async_update() + await self._async_update_from_rest_data() + + async def async_added_to_hass(self): + """Ensure the data from the initial update is reflected in the state.""" + await self._async_update_from_rest_data() + + async def _async_update_from_rest_data(self): + """Update state from the rest data.""" if self.rest.data is None: _LOGGER.error("Unable to retrieve data for %s", self.name) return @@ -153,7 +161,3 @@ class ScrapeSensor(Entity): ) else: self._state = value - - async def async_will_remove_from_hass(self): - """Shutdown the session.""" - await self.rest.async_remove() diff --git a/homeassistant/components/sense/translations/pt.json b/homeassistant/components/sense/translations/pt.json index 196be985b6..e3b78cd8e4 100644 --- a/homeassistant/components/sense/translations/pt.json +++ b/homeassistant/components/sense/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/sense/translations/zh-Hant.json b/homeassistant/components/sense/translations/zh-Hant.json index 356e58f640..d819bfd4bb 100644 --- a/homeassistant/components/sense/translations/zh-Hant.json +++ b/homeassistant/components/sense/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/sensor/translations/nl.json b/homeassistant/components/sensor/translations/nl.json index 7b4c1e71d5..869599296d 100644 --- a/homeassistant/components/sensor/translations/nl.json +++ b/homeassistant/components/sensor/translations/nl.json @@ -2,17 +2,23 @@ "device_automation": { "condition_type": { "is_battery_level": "Huidige batterijniveau {entity_name}", + "is_current": "Huidige {entity_name} stroom", + "is_energy": "Huidige {entity_name} energie", "is_humidity": "Huidige {entity_name} vochtigheidsgraad", "is_illuminance": "Huidige {entity_name} verlichtingssterkte", "is_power": "Huidige {entity_name}\nvermogen", + "is_power_factor": "Huidige {entity_name} vermogensfactor", "is_pressure": "Huidige {entity_name} druk", "is_signal_strength": "Huidige {entity_name} signaalsterkte", "is_temperature": "Huidige {entity_name} temperatuur", "is_timestamp": "Huidige {entity_name} tijdstip", - "is_value": "Huidige {entity_name} waarde" + "is_value": "Huidige {entity_name} waarde", + "is_voltage": "Huidige {entity_name} spanning" }, "trigger_type": { "battery_level": "{entity_name} batterijniveau gewijzigd", + "current": "{entity_name} huidige wijzigingen", + "energy": "{entity_name} energieveranderingen", "humidity": "{entity_name} vochtigheidsgraad gewijzigd", "illuminance": "{entity_name} verlichtingssterkte gewijzigd", "power": "{entity_name} vermogen gewijzigd", diff --git a/homeassistant/components/sentry/translations/cs.json b/homeassistant/components/sentry/translations/cs.json index ad96aeba53..28a2d603dd 100644 --- a/homeassistant/components/sentry/translations/cs.json +++ b/homeassistant/components/sentry/translations/cs.json @@ -22,7 +22,8 @@ "init": { "data": { "environment": "Voliteln\u00e9 jm\u00e9no prost\u0159ed\u00ed.", - "tracing": "Povolit sledov\u00e1n\u00ed v\u00fdkonu" + "tracing": "Povolit sledov\u00e1n\u00ed v\u00fdkonu", + "tracing_sample_rate": "Vzorkovac\u00ed frekvence trasov\u00e1n\u00ed; mezi 0.0 a 1.0 (1.0 = 100 %)" } } } diff --git a/homeassistant/components/sentry/translations/zh-Hant.json b/homeassistant/components/sentry/translations/zh-Hant.json index a63efaf6dc..b73a2e57f1 100644 --- a/homeassistant/components/sentry/translations/zh-Hant.json +++ b/homeassistant/components/sentry/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "bad_dsn": "DSN \u7121\u6548", diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index d8305d1055..ce85d07d08 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -2,6 +2,6 @@ "domain": "serial", "name": "Serial", "documentation": "https://www.home-assistant.io/integrations/serial", - "requirements": ["pyserial-asyncio==0.4"], + "requirements": ["pyserial-asyncio==0.5"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/sharkiq/translations/de.json b/homeassistant/components/sharkiq/translations/de.json index 5a1d4f2f18..2294960d6f 100644 --- a/homeassistant/components/sharkiq/translations/de.json +++ b/homeassistant/components/sharkiq/translations/de.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/sharkiq/translations/no.json b/homeassistant/components/sharkiq/translations/no.json index d04e579654..4454bd940d 100644 --- a/homeassistant/components/sharkiq/translations/no.json +++ b/homeassistant/components/sharkiq/translations/no.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Kontoen er allerede konfigurert", "cannot_connect": "Tilkobling mislyktes", - "reauth_successful": "Reautentisering var vellykket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "error": { diff --git a/homeassistant/components/sharkiq/translations/pt.json b/homeassistant/components/sharkiq/translations/pt.json index 565b9f6c0e..dfae15e968 100644 --- a/homeassistant/components/sharkiq/translations/pt.json +++ b/homeassistant/components/sharkiq/translations/pt.json @@ -1,11 +1,23 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "unknown": "Erro inesperado" + }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { + "reauth": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 298c7e111b..147d9fb950 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -24,6 +24,7 @@ from homeassistant.helpers import ( ) from .const import ( + BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, COAP, DATA_CONFIG_ENTRY, DOMAIN, @@ -143,6 +144,8 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): ) self._last_input_events_count = dict() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop) + @callback def _async_input_events_handler(self): """Handle device input events.""" @@ -184,6 +187,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def _async_update_data(self): """Fetch data.""" + _LOGGER.debug("Polling Shelly Device - %s", self.name) try: async with async_timeout.timeout( @@ -206,6 +210,7 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): async def async_setup(self): """Set up the wrapper.""" + dev_reg = await device_registry.async_get_registry(self.hass) model_type = self.device.settings["device"]["type"] entry = dev_reg.async_get_or_create( @@ -225,18 +230,33 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): self.device.shutdown() self._async_remove_input_events_handler() + @callback + def _handle_ha_stop(self, _): + """Handle Home Assistant stopping.""" + _LOGGER.debug("Stopping ShellyDeviceWrapper for %s", self.name) + self.shutdown() + class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): """Rest Wrapper for a Shelly device with Home Assistant specific functions.""" def __init__(self, hass, device: aioshelly.Device): """Initialize the Shelly device wrapper.""" + if ( + device.settings["device"]["type"] + in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION + ): + update_interval = ( + SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"] + ) + else: + update_interval = REST_SENSORS_UPDATE_INTERVAL super().__init__( hass, _LOGGER, name=get_device_name(device), - update_interval=timedelta(seconds=REST_SENSORS_UPDATE_INTERVAL), + update_interval=timedelta(seconds=update_interval), ) self.device = device diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 53038352d4..d53f089054 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_GAS, DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, DEVICE_CLASS_POWER, DEVICE_CLASS_PROBLEM, @@ -70,6 +71,9 @@ SENSORS = { default_enabled=False, removal_condition=is_momentary_input, ), + ("sensor", "motion"): BlockAttributeDescription( + name="Motion", device_class=DEVICE_CLASS_MOTION + ), } REST_SENSORS = { diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index e23e9561c7..b3dd7bb80f 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -59,7 +59,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Shelly.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH host = None info = None @@ -138,9 +138,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_zeroconf(self, zeroconf_info): """Handle zeroconf discovery.""" - if not zeroconf_info.get("name", "").startswith("shelly"): - return self.async_abort(reason="not_shelly") - try: self.info = info = await self._async_get_info(zeroconf_info["host"]) except HTTP_CONNECT_ERRORS: diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index cd74746697..9f5c5b2efc 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -32,3 +32,6 @@ INPUTS_EVENTS_DICT = { "SL": "single_long", "LS": "long_single", } + +# List of battery devices that maintain a permanent WiFi connection +BATTERY_DEVICES_WITH_PERMANENT_CONNECTION = ["SHMOS-01"] diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index dac21a6af7..74d0f831c8 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "flow_title": "Shelly: {name}", "step": { "credentials": { diff --git a/homeassistant/components/shelly/translations/pt.json b/homeassistant/components/shelly/translations/pt.json index b02332eb0b..d66cc0e5dd 100644 --- a/homeassistant/components/shelly/translations/pt.json +++ b/homeassistant/components/shelly/translations/pt.json @@ -5,6 +5,7 @@ }, "error": { "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "flow_title": "Shelly: {name}", @@ -12,6 +13,12 @@ "confirm_discovery": { "description": "Deseja configurar o {model} em {host} ?" }, + "credentials": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + }, "user": { "data": { "host": "Servidor" diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index 59ac0f5ccc..bf0150523b 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "unsupported_firmware": "\u8a2d\u5099\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4\u3002" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "unsupported_firmware": "\u88dd\u7f6e\u4f7f\u7528\u7684\u97cc\u9ad4\u4e0d\u652f\u63f4\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -12,7 +12,7 @@ "flow_title": "{name}", "step": { "confirm_discovery": { - "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" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u4f4d\u65bc {host} \u7684 {model}\uff1f\n\n\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u3002" }, "credentials": { "data": { @@ -24,7 +24,7 @@ "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" + "description": "\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u3002" } } } diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index 67f059c1cd..ab05cf649d 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet." }, "error": { - "identifier_exists": "Konto bereits registriert" + "identifier_exists": "Konto bereits registriert", + "unknown": "Unerwarteter Fehler" }, "step": { "reauth_confirm": { diff --git a/homeassistant/components/simplisafe/translations/no.json b/homeassistant/components/simplisafe/translations/no.json index 401c5a540d..3280224885 100644 --- a/homeassistant/components/simplisafe/translations/no.json +++ b/homeassistant/components/simplisafe/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Denne SimpliSafe-kontoen er allerede i bruk.", - "reauth_successful": "Reautentisering var vellykket" + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "identifier_exists": "Konto er allerede registrert", @@ -13,14 +13,14 @@ "step": { "mfa": { "description": "Sjekk e-posten din for en lenke fra SimpliSafe. Etter \u00e5 ha bekreftet lenken, g\u00e5 tilbake hit for \u00e5 fullf\u00f8re installasjonen av integrasjonen.", - "title": "SimpliSafe flerfaktorautentisering" + "title": "SimpliSafe flertrinnsbekreftelse" }, "reauth_confirm": { "data": { "password": "Passord" }, - "description": "Adgangstokenet ditt har utl\u00f8pt eller blitt opphevet. Skriv inn passordet ditt for \u00e5 koble kontoen din p\u00e5 nytt.", - "title": "Bekreft integrering p\u00e5 nytt" + "description": "Tilgangstokenet ditt har utl\u00f8pt eller blitt tilbakekalt. Skriv inn passordet ditt for \u00e5 koble til kontoen din p\u00e5 nytt.", + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/simplisafe/translations/pt.json b/homeassistant/components/simplisafe/translations/pt.json index a208b49150..9b5df6cf93 100644 --- a/homeassistant/components/simplisafe/translations/pt.json +++ b/homeassistant/components/simplisafe/translations/pt.json @@ -1,14 +1,19 @@ { "config": { + "abort": { + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, "error": { "identifier_exists": "Conta j\u00e1 registada", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { "reauth_confirm": { "data": { "password": "Palavra-passe" - } + }, + "title": "Reautenticar integra\u00e7\u00e3o" }, "user": { "data": { diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 1b97b80095..4d621d18fa 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -2,6 +2,6 @@ "domain": "skybell", "name": "SkyBell", "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": ["skybellpy==0.6.1"], + "requirements": ["skybellpy==0.6.3"], "codeowners": [] } diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json index 0e77c8fbd7..a609492f42 100644 --- a/homeassistant/components/smappee/translations/de.json +++ b/homeassistant/components/smappee/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "cannot_connect": "Verbindungsfehler" + }, "flow_title": "Smappee: {name}", "step": { "environment": { diff --git a/homeassistant/components/smappee/translations/et.json b/homeassistant/components/smappee/translations/et.json index 4996f9dea9..37a10c69ec 100644 --- a/homeassistant/components/smappee/translations/et.json +++ b/homeassistant/components/smappee/translations/et.json @@ -21,7 +21,7 @@ "data": { "host": "" }, - "description": "Smappee kohaliku sidumise algatamiseks sisestage hostinimi" + "description": "Smappee kohaliku sidumise algatamiseks sisesta hostinimi" }, "pick_implementation": { "title": "Vali tuvastusmeetod" diff --git a/homeassistant/components/smappee/translations/no.json b/homeassistant/components/smappee/translations/no.json index 5e37836946..f1307e2a16 100644 --- a/homeassistant/components/smappee/translations/no.json +++ b/homeassistant/components/smappee/translations/no.json @@ -3,10 +3,10 @@ "abort": { "already_configured_device": "Enheten er allerede konfigurert", "already_configured_local_device": "Lokal(e) enhet(er) er allerede konfigurert. Fjern de f\u00f8rst f\u00f8r du konfigurerer en skyenhet.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "cannot_connect": "Tilkobling mislyktes", "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 hjelpseksjonen]({docs_url})" }, "flow_title": "", diff --git a/homeassistant/components/smappee/translations/pt.json b/homeassistant/components/smappee/translations/pt.json index aba871acf6..75c24278a8 100644 --- a/homeassistant/components/smappee/translations/pt.json +++ b/homeassistant/components/smappee/translations/pt.json @@ -1,13 +1,20 @@ { "config": { "abort": { - "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado" + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" }, "step": { "local": { "data": { "host": "Servidor" } + }, + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } } diff --git a/homeassistant/components/smappee/translations/zh-Hant.json b/homeassistant/components/smappee/translations/zh-Hant.json index 77f726a0dc..4f41b5a1e5 100644 --- a/homeassistant/components/smappee/translations/zh-Hant.json +++ b/homeassistant/components/smappee/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "already_configured_local_device": "\u672c\u5730\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\uff0c\u8acb\u5148\u9032\u884c\u79fb\u9664\u5f8c\u518d\u8a2d\u5b9a\u96f2\u7aef\u8a2d\u5099\u3002", + "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_local_device": "\u672c\u5730\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\uff0c\u8acb\u5148\u9032\u884c\u79fb\u9664\u5f8c\u518d\u8a2d\u5b9a\u96f2\u7aef\u88dd\u7f6e\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "cannot_connect": "\u9023\u7dda\u5931\u6557", - "invalid_mdns": "Smappee \u6574\u5408\u4e0d\u652f\u63f4\u7684\u8a2d\u5099\u3002", + "invalid_mdns": "Smappee \u6574\u5408\u4e0d\u652f\u63f4\u7684\u88dd\u7f6e\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})" }, @@ -27,8 +27,8 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Smappee \u8a2d\u5099\u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Smappee \u8a2d\u5099" + "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f\u70ba `{serial_number}` \u4e4b Smappee \u88dd\u7f6e\u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Smappee \u88dd\u7f6e" } } } diff --git a/homeassistant/components/smart_meter_texas/translations/de.json b/homeassistant/components/smart_meter_texas/translations/de.json index 6f39806287..3821567570 100644 --- a/homeassistant/components/smart_meter_texas/translations/de.json +++ b/homeassistant/components/smart_meter_texas/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/smart_meter_texas/translations/zh-Hant.json b/homeassistant/components/smart_meter_texas/translations/zh-Hant.json index df467dd38b..d232b491b6 100644 --- a/homeassistant/components/smart_meter_texas/translations/zh-Hant.json +++ b/homeassistant/components/smart_meter_texas/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/smarthab/translations/pt.json b/homeassistant/components/smarthab/translations/pt.json index 2933743c86..7430480cc0 100644 --- a/homeassistant/components/smarthab/translations/pt.json +++ b/homeassistant/components/smarthab/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/smartthings/translations/no.json b/homeassistant/components/smartthings/translations/no.json index 41bb76282b..ca8c6f81ed 100644 --- a/homeassistant/components/smartthings/translations/no.json +++ b/homeassistant/components/smartthings/translations/no.json @@ -6,9 +6,9 @@ }, "error": { "app_setup_error": "Kan ikke konfigurere SmartApp. Vennligst pr\u00f8v p\u00e5 nytt.", - "token_forbidden": "Tokenet har ikke de n\u00f8dvendige OAuth-omfangene.", + "token_forbidden": "Tokenet har ikke de n\u00f8dvendige OAuth-omfangene", "token_invalid_format": "Token m\u00e5 v\u00e6re i UID/GUID format", - "token_unauthorized": "Tokenet er ugyldig eller er ikke lenger godkjent.", + "token_unauthorized": "Tokenet er ugyldig eller er ikke lenger godkjent", "webhook_error": "SmartThings kan ikke validere URL-adressen for webhook. Kontroller at URL-adressen for webhook kan n\u00e5s fra Internett, og pr\u00f8v p\u00e5 nytt." }, "step": { diff --git a/homeassistant/components/smartthings/translations/pt.json b/homeassistant/components/smartthings/translations/pt.json index efab29fd69..9f2ed5a4b9 100644 --- a/homeassistant/components/smartthings/translations/pt.json +++ b/homeassistant/components/smartthings/translations/pt.json @@ -15,6 +15,9 @@ "title": "Autorizar o Home Assistant" }, "pat": { + "data": { + "access_token": "Token de Acesso" + }, "title": "Insira o Token de acesso pessoal" }, "select_location": { diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json index 273daf6ef0..1252313a43 100644 --- a/homeassistant/components/sms/translations/de.json +++ b/homeassistant/components/sms/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sms/translations/pt.json b/homeassistant/components/sms/translations/pt.json index 38544eb2ce..4ccc36bcc2 100644 --- a/homeassistant/components/sms/translations/pt.json +++ b/homeassistant/components/sms/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/sms/translations/zh-Hant.json b/homeassistant/components/sms/translations/zh-Hant.json index 30951f88d0..35952af999 100644 --- a/homeassistant/components/sms/translations/zh-Hant.json +++ b/homeassistant/components/sms/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "device": "\u8a2d\u5099" + "device": "\u88dd\u7f6e" }, "title": "\u9023\u7dda\u81f3\u6578\u64da\u6a5f" } diff --git a/homeassistant/components/solaredge/translations/de.json b/homeassistant/components/solaredge/translations/de.json index d3fe05bce1..ec9b5681e7 100644 --- a/homeassistant/components/solaredge/translations/de.json +++ b/homeassistant/components/solaredge/translations/de.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", "site_exists": "Diese site_id ist bereits konfiguriert" }, "error": { - "site_exists": "Diese site_id ist bereits konfiguriert" + "already_configured": "Das Ger\u00e4t ist bereits konfiguriert", + "could_not_connect": "Es konnte keine Verbindung zur Solaredge-API hergestellt werden", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", + "site_exists": "Diese site_id ist bereits konfiguriert", + "site_not_active": "Die Seite ist nicht aktiv" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/hu.json b/homeassistant/components/solaredge/translations/hu.json index e66bf3b404..3189026992 100644 --- a/homeassistant/components/solaredge/translations/hu.json +++ b/homeassistant/components/solaredge/translations/hu.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "site_not_active": "Az oldal nem akt\u00edv" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/solaredge/translations/pt.json b/homeassistant/components/solaredge/translations/pt.json index 01078bbddf..52bbd75e9b 100644 --- a/homeassistant/components/solaredge/translations/pt.json +++ b/homeassistant/components/solaredge/translations/pt.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "invalid_api_key": "Chave de API inv\u00e1lida" + }, "step": { "user": { "data": { - "api_key": "Chave de API" + "api_key": "API Key" } } } diff --git a/homeassistant/components/solaredge/translations/sl.json b/homeassistant/components/solaredge/translations/sl.json index 3f6e78fd3b..3414e37a65 100644 --- a/homeassistant/components/solaredge/translations/sl.json +++ b/homeassistant/components/solaredge/translations/sl.json @@ -1,10 +1,15 @@ { "config": { "abort": { + "already_configured": "Naprava je \u017ee name\u0161\u010dena", "site_exists": "Ta site_id je \u017ee nastavljen" }, "error": { - "site_exists": "Ta site_id je \u017ee nastavljen" + "already_configured": "Naprava je \u017ee name\u0161\u010dena", + "could_not_connect": "Ni se bilo mogo\u010de povezati s Solaredge API", + "invalid_api_key": "Neveljaven API klju\u010d", + "site_exists": "Ta site_id je \u017ee nastavljen", + "site_not_active": "Stran ni aktivna" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/tr.json b/homeassistant/components/solaredge/translations/tr.json new file mode 100644 index 0000000000..5307276a71 --- /dev/null +++ b/homeassistant/components/solaredge/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/zh-Hant.json b/homeassistant/components/solaredge/translations/zh-Hant.json index 01c1db919c..18cf04cf5a 100644 --- a/homeassistant/components/solaredge/translations/zh-Hant.json +++ b/homeassistant/components/solaredge/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "could_not_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 solaredge API", "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", "site_exists": "site_id \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", diff --git a/homeassistant/components/solarlog/translations/pt.json b/homeassistant/components/solarlog/translations/pt.json index 88cfc4a797..a37c510a36 100644 --- a/homeassistant/components/solarlog/translations/pt.json +++ b/homeassistant/components/solarlog/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "cannot_connect": "Falha na liga\u00e7\u00e3o, por favor verifique o endere\u00e7o do servidor" }, "step": { diff --git a/homeassistant/components/solarlog/translations/zh-Hant.json b/homeassistant/components/solarlog/translations/zh-Hant.json index b8f53a74ff..b97772a8d4 100644 --- a/homeassistant/components/solarlog/translations/zh-Hant.json +++ b/homeassistant/components/solarlog/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 93ee4fc9b8..3439684f97 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -27,7 +27,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SOMA_COMPONENTS = ["cover"] +SOMA_COMPONENTS = ["cover", "sensor"] async def async_setup(hass, config): @@ -74,6 +74,7 @@ class SomaEntity(Entity): self.device = device self.api = api self.current_position = 50 + self.battery_state = 0 self.is_available = True @property @@ -120,4 +121,25 @@ class SomaEntity(Entity): self.is_available = False return self.current_position = 100 - response["position"] + try: + response = await self.hass.async_add_executor_job( + self.api.get_battery_level, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + self.is_available = False + return + # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API + # battery_level response is expected to be min = 360, max 410 for + # 0-100% levels above 410 are consider 100% and below 360, 0% as the + # device considers 360 the minimum to move the motor. + _battery = round(2 * (response["battery_level"] - 360)) + battery = max(min(100, _battery), 0) + self.battery_state = battery self.is_available = True diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py new file mode 100644 index 0000000000..2d37a0b0dc --- /dev/null +++ b/homeassistant/components/soma/sensor.py @@ -0,0 +1,40 @@ +"""Support for Soma sensors.""" +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE +from homeassistant.helpers.entity import Entity + +from . import DEVICES, SomaEntity +from .const import API, DOMAIN + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Soma sensor platform.""" + + devices = hass.data[DOMAIN][DEVICES] + + async_add_entities( + [SomaSensor(sensor, hass.data[DOMAIN][API]) for sensor in devices], True + ) + + +class SomaSensor(SomaEntity, Entity): + """Representation of a Soma cover device.""" + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_BATTERY + + @property + def name(self): + """Return the name of the device.""" + return self.device["name"] + " battery level" + + @property + def state(self): + """Return the state of the entity.""" + return self.battery_state + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return PERCENTAGE diff --git a/homeassistant/components/soma/translations/no.json b/homeassistant/components/soma/translations/no.json index 4b9fe3b564..f9b64dc848 100644 --- a/homeassistant/components/soma/translations/no.json +++ b/homeassistant/components/soma/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", - "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "connection_error": "Kunne ikke koble til SOMA Connect.", "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", "result_error": "SOMA Connect svarte med feilstatus." diff --git a/homeassistant/components/soma/translations/zh-Hant.json b/homeassistant/components/soma/translations/zh-Hant.json index 1665930404..3dfb164955 100644 --- a/homeassistant/components/soma/translations/zh-Hant.json +++ b/homeassistant/components/soma/translations/zh-Hant.json @@ -8,7 +8,7 @@ "result_error": "SOMA \u9023\u7dda\u56de\u61c9\u72c0\u614b\u932f\u8aa4\u3002" }, "create_entry": { - "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u88dd\u7f6e\u3002" }, "step": { "user": { diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 728e54b456..2fc83ea71d 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -21,6 +21,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from . import api @@ -47,7 +48,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SOMFY_COMPONENTS = ["cover", "switch"] +SOMFY_COMPONENTS = ["climate", "cover", "sensor", "switch"] async def async_setup(hass, config): @@ -92,6 +93,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def _update_all_devices(): """Update all the devices.""" devices = await hass.async_add_executor_job(data[API].get_devices) + previous_devices = data[COORDINATOR].data + # Sometimes Somfy returns an empty list. + if not devices and previous_devices: + raise UpdateFailed("No devices returned") return {dev.id: dev for dev in devices} coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/somfy/climate.py b/homeassistant/components/somfy/climate.py new file mode 100644 index 0000000000..00a2738f4f --- /dev/null +++ b/homeassistant/components/somfy/climate.py @@ -0,0 +1,178 @@ +"""Support for Somfy Thermostat.""" + +from typing import List, Optional + +from pymfy.api.devices.category import Category +from pymfy.api.devices.thermostat import ( + DurationType, + HvacState, + RegulationState, + TargetMode, + Thermostat, +) + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + PRESET_AWAY, + PRESET_HOME, + PRESET_SLEEP, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS + +from . import SomfyEntity +from .const import API, COORDINATOR, DOMAIN + +SUPPORTED_CATEGORIES = {Category.HVAC.value} + +PRESET_FROST_GUARD = "Frost Guard" +PRESET_GEOFENCING = "Geofencing" +PRESET_MANUAL = "Manual" + +PRESETS_MAPPING = { + TargetMode.AT_HOME: PRESET_HOME, + TargetMode.AWAY: PRESET_AWAY, + TargetMode.SLEEP: PRESET_SLEEP, + TargetMode.MANUAL: PRESET_MANUAL, + TargetMode.GEOFENCING: PRESET_GEOFENCING, + TargetMode.FROST_PROTECTION: PRESET_FROST_GUARD, +} +REVERSE_PRESET_MAPPING = {v: k for k, v in PRESETS_MAPPING.items()} + +HVAC_MODES_MAPPING = {HvacState.COOL: HVAC_MODE_COOL, HvacState.HEAT: HVAC_MODE_HEAT} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Somfy climate platform.""" + + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] + + climates = [ + SomfyClimate(coordinator, device_id, api) + for device_id, device in coordinator.data.items() + if SUPPORTED_CATEGORIES & set(device.categories) + ] + + async_add_entities(climates) + + +class SomfyClimate(SomfyEntity, ClimateEntity): + """Representation of a Somfy thermostat device.""" + + def __init__(self, coordinator, device_id, api): + """Initialize the Somfy device.""" + super().__init__(coordinator, device_id, api) + self._climate = None + self._create_device() + + def _create_device(self): + """Update the device with the latest data.""" + self._climate = Thermostat(self.device, self.api) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._climate.get_ambient_temperature() + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._climate.get_target_temperature() + + def set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is None: + return + + self._climate.set_target(TargetMode.MANUAL, temperature, DurationType.NEXT_MODE) + + @property + def max_temp(self) -> float: + """Return the maximum temperature.""" + return 26.0 + + @property + def min_temp(self) -> float: + """Return the minimum temperature.""" + return 15.0 + + @property + def current_humidity(self): + """Return the current humidity.""" + return self._climate.get_humidity() + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + if self._climate.get_regulation_state() == RegulationState.TIMETABLE: + return HVAC_MODE_AUTO + return HVAC_MODES_MAPPING.get(self._climate.get_hvac_state()) + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes. + + HEAT and COOL mode are exclusive. End user has to enable a mode manually within the Somfy application. + So only one mode can be displayed. Auto mode is a scheduler. + """ + hvac_state = HVAC_MODES_MAPPING[self._climate.get_hvac_state()] + return [HVAC_MODE_AUTO, hvac_state] + + def set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + if hvac_mode == HVAC_MODE_AUTO: + self._climate.cancel_target() + else: + self._climate.set_target( + TargetMode.MANUAL, self.target_temperature, DurationType.FURTHER_NOTICE + ) + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode.""" + mode = self._climate.get_target_mode() + return PRESETS_MAPPING.get(mode) + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes.""" + return list(PRESETS_MAPPING.values()) + + def set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + if self.preset_mode == preset_mode: + return + + if preset_mode == PRESET_HOME: + temperature = self._climate.get_at_home_temperature() + elif preset_mode == PRESET_AWAY: + temperature = self._climate.get_away_temperature() + elif preset_mode == PRESET_SLEEP: + temperature = self._climate.get_night_temperature() + elif preset_mode == PRESET_FROST_GUARD: + temperature = self._climate.get_frost_protection_temperature() + elif preset_mode in [PRESET_MANUAL, PRESET_GEOFENCING]: + temperature = self.target_temperature + else: + raise ValueError(f"Preset mode not supported: {preset_mode}") + + self._climate.set_target( + REVERSE_PRESET_MAPPING[preset_mode], temperature, DurationType.NEXT_MODE + ) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 696412ac3c..e730855812 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -36,19 +36,17 @@ SUPPORTED_CATEGORIES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy cover platform.""" - def get_covers(): - """Retrieve covers.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - api = domain_data[API] + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] - return [ - SomfyCover(coordinator, device_id, api, domain_data[CONF_OPTIMISTIC]) - for device_id, device in coordinator.data.items() - if SUPPORTED_CATEGORIES & set(device.categories) - ] + covers = [ + SomfyCover(coordinator, device_id, api, domain_data[CONF_OPTIMISTIC]) + for device_id, device in coordinator.data.items() + if SUPPORTED_CATEGORIES & set(device.categories) + ] - async_add_entities(await hass.async_add_executor_job(get_covers)) + async_add_entities(covers) class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): @@ -62,12 +60,12 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): self._closed = None self._is_opening = None self._is_closing = None - self.cover = None + self._cover = None self._create_device() def _create_device(self) -> Blind: """Update the device with the latest data.""" - self.cover = Blind(self.device, self.api) + self._cover = Blind(self.device, self.api) @property def supported_features(self) -> int: @@ -97,7 +95,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): self.async_write_ha_state() try: # Blocks until the close command is sent - await self.hass.async_add_executor_job(self.cover.close) + await self.hass.async_add_executor_job(self._cover.close) self._closed = True finally: self._is_closing = None @@ -109,7 +107,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): self.async_write_ha_state() try: # Blocks until the open command is sent - await self.hass.async_add_executor_job(self.cover.open) + await self.hass.async_add_executor_job(self._cover.open) self._closed = False finally: self._is_opening = None @@ -117,11 +115,11 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): def stop_cover(self, **kwargs): """Stop the cover.""" - self.cover.stop() + self._cover.stop() def set_cover_position(self, **kwargs): """Move the cover shutter to a specific position.""" - self.cover.set_position(100 - kwargs[ATTR_POSITION]) + self._cover.set_position(100 - kwargs[ATTR_POSITION]) @property def device_class(self): @@ -137,7 +135,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): """Return the current position of cover shutter.""" if not self.has_state("position"): return None - return 100 - self.cover.get_position() + return 100 - self._cover.get_position() @property def is_opening(self): @@ -158,7 +156,7 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): """Return if the cover is closed.""" is_closed = None if self.has_state("position"): - is_closed = self.cover.is_closed() + is_closed = self._cover.is_closed() elif self.optimistic: is_closed = self._closed return is_closed @@ -171,23 +169,23 @@ class SomfyCover(SomfyEntity, RestoreEntity, CoverEntity): """ if not self.has_state("orientation"): return None - return 100 - self.cover.orientation + return 100 - self._cover.orientation def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - self.cover.orientation = 100 - kwargs[ATTR_TILT_POSITION] + self._cover.orientation = 100 - kwargs[ATTR_TILT_POSITION] def open_cover_tilt(self, **kwargs): """Open the cover tilt.""" - self.cover.orientation = 0 + self._cover.orientation = 0 def close_cover_tilt(self, **kwargs): """Close the cover tilt.""" - self.cover.orientation = 100 + self._cover.orientation = 100 def stop_cover_tilt(self, **kwargs): """Stop the cover.""" - self.cover.stop() + self._cover.stop() async def async_added_to_hass(self): """Complete the initialization.""" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 69450c4c4d..ea84bf3458 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.9.1"] -} + "requirements": ["pymfy==0.9.3"] +} \ No newline at end of file diff --git a/homeassistant/components/somfy/sensor.py b/homeassistant/components/somfy/sensor.py new file mode 100644 index 0000000000..1becc929ad --- /dev/null +++ b/homeassistant/components/somfy/sensor.py @@ -0,0 +1,56 @@ +"""Support for Somfy Thermostat Battery.""" + +from pymfy.api.devices.category import Category +from pymfy.api.devices.thermostat import Thermostat + +from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE + +from . import SomfyEntity +from .const import API, COORDINATOR, DOMAIN + +SUPPORTED_CATEGORIES = {Category.HVAC.value} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Somfy sensor platform.""" + + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] + + sensors = [ + SomfyThermostatBatterySensor(coordinator, device_id, api) + for device_id, device in coordinator.data.items() + if SUPPORTED_CATEGORIES & set(device.categories) + ] + + async_add_entities(sensors) + + +class SomfyThermostatBatterySensor(SomfyEntity): + """Representation of a Somfy thermostat battery.""" + + def __init__(self, coordinator, device_id, api): + """Initialize the Somfy device.""" + super().__init__(coordinator, device_id, api) + self._climate = None + self._create_device() + + def _create_device(self): + """Update the device with the latest data.""" + self._climate = Thermostat(self.device, self.api) + + @property + def state(self) -> int: + """Return the state of the sensor.""" + return self._climate.get_battery() + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of the sensor.""" + return PERCENTAGE diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index d614776778..1432895336 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -11,19 +11,17 @@ from .const import API, COORDINATOR, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Somfy switch platform.""" - def get_shutters(): - """Retrieve switches.""" - domain_data = hass.data[DOMAIN] - coordinator = domain_data[COORDINATOR] - api = domain_data[API] + domain_data = hass.data[DOMAIN] + coordinator = domain_data[COORDINATOR] + api = domain_data[API] - return [ - SomfyCameraShutter(coordinator, device_id, api) - for device_id, device in coordinator.data.items() - if Category.CAMERA.value in device.categories - ] + switches = [ + SomfyCameraShutter(coordinator, device_id, api) + for device_id, device in coordinator.data.items() + if Category.CAMERA.value in device.categories + ] - async_add_entities(await hass.async_add_executor_job(get_shutters), True) + async_add_entities(switches) class SomfyCameraShutter(SomfyEntity, SwitchEntity): diff --git a/homeassistant/components/somfy/translations/no.json b/homeassistant/components/somfy/translations/no.json index 2bb48d39f2..57bc6e6843 100644 --- a/homeassistant/components/somfy/translations/no.json +++ b/homeassistant/components/somfy/translations/no.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, diff --git a/homeassistant/components/somfy/translations/pt.json b/homeassistant/components/somfy/translations/pt.json new file mode 100644 index 0000000000..592ccd8558 --- /dev/null +++ b/homeassistant/components/somfy/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/zh-Hant.json b/homeassistant/components/somfy/translations/zh-Hant.json index 4768da884d..71390930e3 100644 --- a/homeassistant/components/somfy/translations/zh-Hant.json +++ b/homeassistant/components/somfy/translations/zh-Hant.json @@ -4,7 +4,7 @@ "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" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/sonarr/translations/no.json b/homeassistant/components/sonarr/translations/no.json index 39f14020ae..88fe5330ee 100644 --- a/homeassistant/components/sonarr/translations/no.json +++ b/homeassistant/components/sonarr/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "reauth_successful": "Reautentisering var vellykket", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket", "unknown": "Uventet feil" }, "error": { @@ -13,7 +13,7 @@ "step": { "reauth_confirm": { "description": "Sonarr-integrasjonen m\u00e5 autentiseres p\u00e5 nytt med Sonarr API vert p\u00e5: {host}", - "title": "Bekreft integrering p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" }, "user": { "data": { diff --git a/homeassistant/components/sonarr/translations/pt.json b/homeassistant/components/sonarr/translations/pt.json index ce7cbc3f54..24a1283331 100644 --- a/homeassistant/components/sonarr/translations/pt.json +++ b/homeassistant/components/sonarr/translations/pt.json @@ -1,9 +1,26 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida", + "unknown": "Erro inesperado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "flow_title": "Sonarr: {name}", "step": { + "reauth_confirm": { + "title": "Reautenticar integra\u00e7\u00e3o" + }, "user": { "data": { - "host": "Servidor" + "api_key": "API Key", + "host": "Servidor", + "port": "Porta", + "ssl": "Utiliza um certificado SSL", + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/songpal/translations/pt.json b/homeassistant/components/songpal/translations/pt.json new file mode 100644 index 0000000000..db0e0c2a13 --- /dev/null +++ b/homeassistant/components/songpal/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/zh-Hant.json b/homeassistant/components/songpal/translations/zh-Hant.json index b73aaade30..ddb334d754 100644 --- a/homeassistant/components/songpal/translations/zh-Hant.json +++ b/homeassistant/components/songpal/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "not_songpal_device": "\u4e26\u975e Songpal \u8a2d\u5099" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_songpal_device": "\u4e26\u975e Songpal \u88dd\u7f6e" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" diff --git a/homeassistant/components/sonos/translations/zh-Hant.json b/homeassistant/components/sonos/translations/zh-Hant.json index b47280e3a9..31a9d3ce95 100644 --- a/homeassistant/components/sonos/translations/zh-Hant.json +++ b/homeassistant/components/sonos/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/speedtestdotnet/translations/nl.json b/homeassistant/components/speedtestdotnet/translations/nl.json index 703ac8614c..0c0c184b5f 100644 --- a/homeassistant/components/speedtestdotnet/translations/nl.json +++ b/homeassistant/components/speedtestdotnet/translations/nl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk." + "single_instance_allowed": "Al geconfigureerd. Slechts \u00e9\u00e9n configuratie mogelijk.", + "wrong_server_id": "Server-ID is niet geldig" } } } \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/pt.json b/homeassistant/components/speedtestdotnet/translations/pt.json new file mode 100644 index 0000000000..c299020ce9 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "user": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json index e9459bf08f..e88b4ec392 100644 --- a/homeassistant/components/speedtestdotnet/translations/zh-Hant.json +++ b/homeassistant/components/speedtestdotnet/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "wrong_server_id": "\u4f3a\u670d\u5668 ID \u7121\u6548" }, "step": { diff --git a/homeassistant/components/spider/climate.py b/homeassistant/components/spider/climate.py index 7730d8b34c..78764ccf4e 100644 --- a/homeassistant/components/spider/climate.py +++ b/homeassistant/components/spider/climate.py @@ -44,6 +44,16 @@ class SpiderThermostat(ClimateEntity): self.api = api self.thermostat = thermostat + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.thermostat.id)}, + "name": self.thermostat.name, + "manufacturer": self.thermostat.manufacturer, + "model": self.thermostat.model, + } + @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index b285cafcfa..32567e6d13 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -3,7 +3,7 @@ "name": "Itho Daalderop Spider", "documentation": "https://www.home-assistant.io/integrations/spider", "requirements": [ - "spiderpy==1.3.1" + "spiderpy==1.4.2" ], "codeowners": [ "@peternijssen" diff --git a/homeassistant/components/spider/switch.py b/homeassistant/components/spider/switch.py index 1b0c86468e..c9a99f3c20 100644 --- a/homeassistant/components/spider/switch.py +++ b/homeassistant/components/spider/switch.py @@ -5,7 +5,7 @@ from .const import DOMAIN async def async_setup_entry(hass, config, async_add_entities): - """Initialize a Spider thermostat.""" + """Initialize a Spider Power Plug.""" api = hass.data[DOMAIN][config.entry_id] async_add_entities( [ @@ -19,10 +19,20 @@ class SpiderPowerPlug(SwitchEntity): """Representation of a Spider Power Plug.""" def __init__(self, api, power_plug): - """Initialize the Vera device.""" + """Initialize the Spider Power Plug.""" self.api = api self.power_plug = power_plug + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.power_plug.id)}, + "name": self.power_plug.name, + "manufacturer": self.power_plug.manufacturer, + "model": self.power_plug.model, + } + @property def unique_id(self): """Return the ID of this switch.""" diff --git a/homeassistant/components/spider/translations/zh-Hant.json b/homeassistant/components/spider/translations/zh-Hant.json index 96b9ad519d..ce15c28f47 100644 --- a/homeassistant/components/spider/translations/zh-Hant.json +++ b/homeassistant/components/spider/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index ef3f1224a4..e4450e7a30 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -82,12 +82,16 @@ SUPPORT_SPOTIFY = ( | SUPPORT_VOLUME_SET ) -REPEAT_MODE_MAPPING = { +REPEAT_MODE_MAPPING_TO_HA = { "context": REPEAT_MODE_ALL, "off": REPEAT_MODE_OFF, "track": REPEAT_MODE_ONE, } +REPEAT_MODE_MAPPING_TO_SPOTIFY = { + value: key for key, value in REPEAT_MODE_MAPPING_TO_HA.items() +} + BROWSE_LIMIT = 48 MEDIA_TYPE_SHOW = "show" @@ -390,7 +394,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity): def repeat(self) -> Optional[str]: """Return current repeat mode.""" repeat_state = self._currently_playing.get("repeat_state") - return REPEAT_MODE_MAPPING.get(repeat_state) + return REPEAT_MODE_MAPPING_TO_HA.get(repeat_state) @property def supported_features(self) -> int: @@ -469,9 +473,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity): @spotify_exception_handler def set_repeat(self, repeat: str) -> None: """Set repeat mode.""" - for spotify, home_assistant in REPEAT_MODE_MAPPING.items(): - if home_assistant == repeat: - self._spotify.repeat(spotify) + if repeat not in REPEAT_MODE_MAPPING_TO_SPOTIFY: + raise ValueError(f"Unsupported repeat mode: {repeat}") + self._spotify.repeat(REPEAT_MODE_MAPPING_TO_SPOTIFY[repeat]) @spotify_exception_handler def update(self) -> None: diff --git a/homeassistant/components/spotify/translations/ca.json b/homeassistant/components/spotify/translations/ca.json index 0210147a48..fffb248573 100644 --- a/homeassistant/components/spotify/translations/ca.json +++ b/homeassistant/components/spotify/translations/ca.json @@ -18,5 +18,10 @@ "title": "Reautenticaci\u00f3 de la integraci\u00f3" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Endpoint de l'API d'Spotify accessible" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index b6a10d7cde..bfd393bbbb 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -12,5 +12,10 @@ "title": "Authentifizierungsmethode ausw\u00e4hlen" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify-API-Endpunkt erreichbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/no.json b/homeassistant/components/spotify/translations/no.json index eee2386a92..8e2ec3d36c 100644 --- a/homeassistant/components/spotify/translations/no.json +++ b/homeassistant/components/spotify/translations/no.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse.", + "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 hjelpseksjonen]({docs_url})", - "reauth_account_mismatch": "Spotify-kontoen som er autentisert med, samsvarer ikke med den kontoen som trengs re-autentisering." + "reauth_account_mismatch": "Spotify-kontoen som er godkjent samsvarer ikke med kontoen som trenger godkjenning p\u00e5 nytt" }, "create_entry": { "default": "Vellykket godkjenning med Spotify." @@ -15,7 +15,7 @@ }, "reauth_confirm": { "description": "Spotify-integreringen m\u00e5 godkjennes p\u00e5 nytt med Spotify for konto: {account}", - "title": "Bekreft integrering p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" } } }, diff --git a/homeassistant/components/spotify/translations/pt.json b/homeassistant/components/spotify/translations/pt.json index b459d4e6bf..0719e226cd 100644 --- a/homeassistant/components/spotify/translations/pt.json +++ b/homeassistant/components/spotify/translations/pt.json @@ -1,9 +1,13 @@ { "config": { "abort": { + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "reauth_account_mismatch": "A conta Spotify com a qual foi autenticada n\u00e3o corresponde \u00e0 conta necess\u00e1ria para a reautentica\u00e7\u00e3o." }, "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, "reauth_confirm": { "description": "A integra\u00e7\u00e3o do Spotify precisa ser reautenticada com o Spotify para a conta: {account}", "title": "Reautenticar com Spotify" diff --git a/homeassistant/components/spotify/translations/sl.json b/homeassistant/components/spotify/translations/sl.json index 0b13821124..a56222e22e 100644 --- a/homeassistant/components/spotify/translations/sl.json +++ b/homeassistant/components/spotify/translations/sl.json @@ -12,5 +12,10 @@ "title": "Izberite na\u010din preverjanja pristnosti" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Kon\u010dna to\u010dka Spotify API je dosegljiva" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/tr.json b/homeassistant/components/spotify/translations/tr.json index c7307d119a..c543f155e4 100644 --- a/homeassistant/components/spotify/translations/tr.json +++ b/homeassistant/components/spotify/translations/tr.json @@ -12,5 +12,10 @@ "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API u\u00e7 noktas\u0131na ula\u015f\u0131labilir" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/zh-Hans.json b/homeassistant/components/spotify/translations/zh-Hans.json new file mode 100644 index 0000000000..19a6909de4 --- /dev/null +++ b/homeassistant/components/spotify/translations/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "system_health": { + "info": { + "api_endpoint_reachable": "\u53ef\u8bbf\u95ee Spotify API" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 27656c260d..670f5e6614 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -74,6 +74,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if value_template is not None: value_template.hass = hass + # MSSQL uses TOP and not LIMIT + if not ("LIMIT" in query_str or "SELECT TOP" in query_str): + query_str = ( + query_str.replace("SELECT", "SELECT TOP 1") + if "mssql" in db_url + else query_str.replace(";", " LIMIT 1;") + ) + sensor = SQLSensor( name, sessmaker, query_str, column_name, unit, value_template ) @@ -88,10 +96,7 @@ class SQLSensor(Entity): def __init__(self, name, sessmaker, query, column, unit, value_template): """Initialize the SQL sensor.""" self._name = name - if "LIMIT" in query: - self._query = query - else: - self._query = query.replace(";", " LIMIT 1;") + self._query = query self._unit_of_measurement = unit self._template = value_template self._column_name = column diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json index 24087b5061..667bf6dbd1 100644 --- a/homeassistant/components/squeezebox/translations/de.json +++ b/homeassistant/components/squeezebox/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "edit": { "data": { diff --git a/homeassistant/components/squeezebox/translations/zh-Hant.json b/homeassistant/components/squeezebox/translations/zh-Hant.json index 85942f812b..067374f6c1 100644 --- a/homeassistant/components/squeezebox/translations/zh-Hant.json +++ b/homeassistant/components/squeezebox/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "no_server_found": "\u627e\u4e0d\u5230 LMS \u4f3a\u670d\u5668\u3002" }, "error": { diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json new file mode 100644 index 0000000000..23fe89c73b --- /dev/null +++ b/homeassistant/components/srp_energy/translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/hu.json b/homeassistant/components/srp_energy/translations/hu.json new file mode 100644 index 0000000000..f46e17923a --- /dev/null +++ b/homeassistant/components/srp_energy/translations/hu.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "A fi\u00f3k azonos\u00edt\u00f3ja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/pt.json b/homeassistant/components/srp_energy/translations/pt.json new file mode 100644 index 0000000000..3e10b97777 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/pt.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/tr.json b/homeassistant/components/srp_energy/translations/tr.json new file mode 100644 index 0000000000..1b08426f63 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_account": "Hesap kimli\u011fi 9 haneli bir say\u0131 olmal\u0131d\u0131r" + }, + "step": { + "user": { + "data": { + "id": "Hesap Kimli\u011fi", + "is_tou": "Kullan\u0131m Zaman\u0131 Plan\u0131 m\u0131", + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "title": "SRP Enerji" +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/zh-Hant.json b/homeassistant/components/srp_energy/translations/zh-Hant.json index f8cb25f7df..87bf347795 100644 --- a/homeassistant/components/srp_energy/translations/zh-Hant.json +++ b/homeassistant/components/srp_energy/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 40313a4155..084811c8fa 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -126,7 +126,7 @@ async def discover_devices(hass, hass_config): if channel_function == SUPLA_FUNCTION_NONE: _LOGGER.debug( - "Ignored function: %s, channel id: %s", + "Ignored function: %s, channel ID: %s", channel_function, channel["id"], ) @@ -136,7 +136,7 @@ async def discover_devices(hass, hass_config): if component_name is None: _LOGGER.warning( - "Unsupported function: %s, channel id: %s", + "Unsupported function: %s, channel ID: %s", channel_function, channel["id"], ) diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index 7f0213be4e..165e0bfd98 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -24,7 +24,7 @@ SURE_IDS = "sure_ids" TOPIC_UPDATE = f"{DOMAIN}_data_update" # sure petcare api -SURE_API_TIMEOUT = 15 +SURE_API_TIMEOUT = 60 # flap BATTERY_ICON = "mdi:battery" diff --git a/homeassistant/components/syncthru/translations/zh-Hant.json b/homeassistant/components/syncthru/translations/zh-Hant.json index a31ea74fb0..fbbc85c4a1 100644 --- a/homeassistant/components/syncthru/translations/zh-Hant.json +++ b/homeassistant/components/syncthru/translations/zh-Hant.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "invalid_url": "\u7db2\u5740\u7121\u6548", - "syncthru_not_supported": "\u8a2d\u5099\u4e0d\u652f\u63f4 SyncThru", + "syncthru_not_supported": "\u88dd\u7f6e\u4e0d\u652f\u63f4 SyncThru", "unknown_state": "\u5370\u8868\u6a5f\u72c0\u614b\u672a\u77e5\uff0c\u8acb\u78ba\u8a8d URL \u8207\u7db2\u8def\u9023\u7dda" }, "flow_title": "Samsung SyncThru \u5370\u8868\u6a5f\uff1a{name}", diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 47c01a0309..303321ea94 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Host bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindungsfehler", "missing_data": "Fehlende Daten: Bitte versuchen Sie es sp\u00e4ter noch einmal oder eine andere Konfiguration", "otp_failed": "Die zweistufige Authentifizierung ist fehlgeschlagen. Versuchen Sie es erneut mit einem neuen Code", "unknown": "Unbekannter Fehler: Bitte \u00fcberpr\u00fcfen Sie die Protokolle, um weitere Details zu erhalten" diff --git a/homeassistant/components/synology_dsm/translations/pt.json b/homeassistant/components/synology_dsm/translations/pt.json index 4264b1e4a0..9745f897e0 100644 --- a/homeassistant/components/synology_dsm/translations/pt.json +++ b/homeassistant/components/synology_dsm/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "2sa": { "data": { @@ -10,7 +18,9 @@ "data": { "password": "Palavra-passe", "port": "Porta", - "username": "Nome de Utilizador" + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" } }, "user": { @@ -18,7 +28,9 @@ "host": "Servidor", "password": "Palavra-passe", "port": "Porta (opcional)", - "username": "Nome de Utilizador" + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" } } } diff --git a/homeassistant/components/synology_dsm/translations/zh-Hant.json b/homeassistant/components/synology_dsm/translations/zh-Hant.json index 4f50be2795..d5e78faf91 100644 --- a/homeassistant/components/synology_dsm/translations/zh-Hant.json +++ b/homeassistant/components/synology_dsm/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 3b08a7afe1..9ea39b6388 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,6 +2,6 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.7.2"], + "requirements": ["psutil==5.8.0"], "codeowners": [] } diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 1aa6bcdea7..00f193f866 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -268,7 +268,7 @@ class SystemMonitorSensor(Entity): return except psutil.NoSuchProcess as err: _LOGGER.warning( - "Failed to load process with id: %s, old name: %s", + "Failed to load process with ID: %s, old name: %s", err.pid, err.name, ) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 44a0f551ae..228ac48bcb 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -30,7 +30,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -TADO_COMPONENTS = ["sensor", "climate", "water_heater"] +TADO_COMPONENTS = ["binary_sensor", "sensor", "climate", "water_heater"] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=15) @@ -168,13 +168,12 @@ class TadoConnector: self._password = password self._fallback = fallback - self.device_id = None + self.home_id = None self.tado = None self.zones = None self.devices = None self.data = { "zone": {}, - "device": {}, } @property @@ -188,16 +187,15 @@ class TadoConnector: self.tado.setDebugging(True) # Load zones and devices self.zones = self.tado.getZones() - self.devices = self.tado.getMe()["homes"] - self.device_id = self.devices[0]["id"] + self.devices = self.tado.getDevices() + self.home_id = self.tado.getMe()["homes"][0]["id"] @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the registered zones.""" for zone in self.zones: self.update_sensor("zone", zone["id"]) - for device in self.devices: - self.update_sensor("device", device["id"]) + self.devices = self.tado.getDevices() def update_sensor(self, sensor_type, sensor): """Update the internal data from Tado.""" @@ -205,13 +203,6 @@ class TadoConnector: try: if sensor_type == "zone": data = self.tado.getZoneState(sensor) - elif sensor_type == "device": - devices_data = self.tado.getDevices() - if not devices_data: - _LOGGER.info("There are no devices to setup on this tado account") - return - - data = devices_data[0] else: _LOGGER.debug("Unknown sensor: %s", sensor_type) return @@ -227,14 +218,14 @@ class TadoConnector: _LOGGER.debug( "Dispatching update to %s %s %s: %s", - self.device_id, + self.home_id, sensor_type, sensor, data, ) dispatcher_send( self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format(self.device_id, sensor_type, sensor), + SIGNAL_TADO_UPDATE_RECEIVED.format(self.home_id, sensor_type, sensor), ) def get_capabilities(self, zone_id): diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py new file mode 100644 index 0000000000..279633b07b --- /dev/null +++ b/homeassistant/components/tado/binary_sensor.py @@ -0,0 +1,127 @@ +"""Support for Tado sensors for each zone.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_CONNECTIVITY, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, TYPE_BATTERY, TYPE_POWER +from .entity import TadoDeviceEntity + +_LOGGER = logging.getLogger(__name__) + +DEVICE_SENSORS = { + TYPE_BATTERY: [ + "battery state", + "connection state", + ], + TYPE_POWER: [ + "connection state", + ], +} + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up the Tado sensor platform.""" + + tado = hass.data[DOMAIN][entry.entry_id][DATA] + devices = tado.devices + entities = [] + + # Create device sensors + for device in devices: + if "batteryState" in device: + device_type = TYPE_BATTERY + else: + device_type = TYPE_POWER + + entities.extend( + [ + TadoDeviceSensor(tado, device, variable) + for variable in DEVICE_SENSORS[device_type] + ] + ) + + if entities: + async_add_entities(entities, True) + + +class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): + """Representation of a tado Sensor.""" + + def __init__(self, tado, device_info, device_variable): + """Initialize of the Tado Sensor.""" + self._tado = tado + super().__init__(device_info) + + self.device_variable = device_variable + + self._unique_id = f"{device_variable} {self.device_id} {tado.home_id}" + + self._state = None + + async def async_added_to_hass(self): + """Register for sensor updates.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format( + self._tado.home_id, "device", self.device_id + ), + self._async_update_callback, + ) + ) + self._async_update_device_data() + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.device_name} {self.device_variable}" + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this sensor.""" + if self.device_variable == "battery state": + return DEVICE_CLASS_BATTERY + if self.device_variable == "connection state": + return DEVICE_CLASS_CONNECTIVITY + return None + + @callback + def _async_update_callback(self): + """Update and write state.""" + self._async_update_device_data() + self.async_write_ha_state() + + @callback + def _async_update_device_data(self): + """Handle update callbacks.""" + for device in self._tado.devices: + if device["serialNo"] == self.device_id: + self._device_info = device + break + + if self.device_variable == "battery state": + self._state = self._device_info["batteryState"] == "LOW" + elif self.device_variable == "connection state": + self._state = self._device_info.get("connectionState", {}).get( + "value", False + ) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 3e0c79ad65..423205f15b 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -89,15 +89,13 @@ def _generate_entities(tado): entities = [] for zone in tado.zones: if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]: - entity = create_climate_entity( - tado, zone["name"], zone["id"], zone["devices"][0] - ) + entity = create_climate_entity(tado, zone["name"], zone["id"]) if entity: entities.append(entity) return entities -def create_climate_entity(tado, name: str, zone_id: int, zone: dict): +def create_climate_entity(tado, name: str, zone_id: int): """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) @@ -180,7 +178,6 @@ def create_climate_entity(tado, name: str, zone_id: int, zone: dict): supported_hvac_modes, supported_fan_modes, support_flags, - zone, ) return entity @@ -203,15 +200,14 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): supported_hvac_modes, supported_fan_modes, support_flags, - device_info, ): """Initialize of Tado climate entity.""" self._tado = tado - super().__init__(zone_name, device_info, tado.device_id, zone_id) + super().__init__(zone_name, tado.home_id, zone_id) self.zone_id = zone_id self.zone_type = zone_type - self._unique_id = f"{zone_type} {zone_id} {tado.device_id}" + self._unique_id = f"{zone_type} {zone_id} {tado.home_id}" self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._supported_hvac_modes = supported_hvac_modes @@ -249,7 +245,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "zone", self.zone_id + self._tado.home_id, "zone", self.zone_id ), self._async_update_callback, ) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 9fc7b19805..95c524b043 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -53,6 +53,9 @@ TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" TYPE_HEATING = "HEATING" TYPE_HOT_WATER = "HOT_WATER" +TYPE_BATTERY = "BATTERY" +TYPE_POWER = "POWER" + # Base modes CONST_MODE_OFF = "OFF" CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule @@ -144,6 +147,6 @@ UNIQUE_ID = "unique_id" DEFAULT_NAME = "Tado" -TADO_BRIDGE = "Tado Bridge" +TADO_ZONE = "Zone" UPDATE_LISTENER = "update_listener" diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index d91896a4e1..03900fdeeb 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -1,25 +1,25 @@ -"""Base class for August entity.""" +"""Base class for Tado entity.""" from homeassistant.helpers.entity import Entity -from .const import DEFAULT_NAME, DOMAIN +from .const import DEFAULT_NAME, DOMAIN, TADO_ZONE -class TadoZoneEntity(Entity): - """Base implementation for tado device.""" +class TadoDeviceEntity(Entity): + """Base implementation for Tado device.""" - def __init__(self, zone_name, device_info, device_id, zone_id): - """Initialize an August device.""" + def __init__(self, device_info): + """Initialize a Tado device.""" super().__init__() - self._device_zone_id = f"{device_id}_{zone_id}" self._device_info = device_info - self.zone_name = zone_name + self.device_name = device_info["shortSerialNo"] + self.device_id = device_info["serialNo"] @property def device_info(self): """Return the device_info of the device.""" return { - "identifiers": {(DOMAIN, self._device_zone_id)}, - "name": self.zone_name, + "identifiers": {(DOMAIN, self.device_id)}, + "name": self.device_name, "manufacturer": DEFAULT_NAME, "sw_version": self._device_info["currentFwVersion"], "model": self._device_info["deviceType"], @@ -30,3 +30,28 @@ class TadoZoneEntity(Entity): def should_poll(self): """Do not poll.""" return False + + +class TadoZoneEntity(Entity): + """Base implementation for Tado zone.""" + + def __init__(self, zone_name, home_id, zone_id): + """Initialize a Tado zone.""" + super().__init__() + self._device_zone_id = f"{home_id}_{zone_id}" + self.zone_name = zone_name + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._device_zone_id)}, + "name": self.zone_name, + "manufacturer": DEFAULT_NAME, + "model": TADO_ZONE, + } + + @property + def should_poll(self): + """Do not poll.""" + return False diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 56be5eb012..4e8f69b17c 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -9,10 +9,8 @@ from homeassistant.helpers.entity import Entity from .const import ( DATA, - DEFAULT_NAME, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, - TADO_BRIDGE, TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER, @@ -46,8 +44,6 @@ ZONE_SENSORS = { TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], } -DEVICE_SENSORS = ["tado bridge status"] - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities @@ -57,7 +53,6 @@ async def async_setup_entry( tado = hass.data[DOMAIN][entry.entry_id][DATA] # Create zone sensors zones = tado.zones - devices = tado.devices entities = [] for zone in zones: @@ -68,22 +63,11 @@ async def async_setup_entry( entities.extend( [ - TadoZoneSensor( - tado, zone["name"], zone["id"], variable, zone["devices"][0] - ) + TadoZoneSensor(tado, zone["name"], zone["id"], variable) for variable in ZONE_SENSORS[zone_type] ] ) - # Create device sensors - for device in devices: - entities.extend( - [ - TadoDeviceSensor(tado, device["name"], device["id"], variable, device) - for variable in DEVICE_SENSORS - ] - ) - if entities: async_add_entities(entities, True) @@ -91,15 +75,15 @@ async def async_setup_entry( class TadoZoneSensor(TadoZoneEntity, Entity): """Representation of a tado Sensor.""" - def __init__(self, tado, zone_name, zone_id, zone_variable, device_info): + def __init__(self, tado, zone_name, zone_id, zone_variable): """Initialize of the Tado Sensor.""" self._tado = tado - super().__init__(zone_name, device_info, tado.device_id, zone_id) + super().__init__(zone_name, tado.home_id, zone_id) self.zone_id = zone_id self.zone_variable = zone_variable - self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}" + self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}" self._state = None self._state_attributes = None @@ -112,7 +96,7 @@ class TadoZoneSensor(TadoZoneEntity, Entity): async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "zone", self.zone_id + self._tado.home_id, "zone", self.zone_id ), self._async_update_callback, ) @@ -227,83 +211,3 @@ class TadoZoneSensor(TadoZoneEntity, Entity): or self._tado_zone_data.open_window_detected ) self._state_attributes = self._tado_zone_data.open_window_attr - - -class TadoDeviceSensor(Entity): - """Representation of a tado Sensor.""" - - def __init__(self, tado, device_name, device_id, device_variable, device_info): - """Initialize of the Tado Sensor.""" - self._tado = tado - - self._device_info = device_info - self.device_name = device_name - self.device_id = device_id - self.device_variable = device_variable - - self._unique_id = f"{device_variable} {device_id} {tado.device_id}" - - self._state = None - self._state_attributes = None - self._tado_device_data = None - - async def async_added_to_hass(self): - """Register for sensor updates.""" - - self.async_on_remove( - async_dispatcher_connect( - self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "device", self.device_id - ), - self._async_update_callback, - ) - ) - self._async_update_device_data() - - @property - def unique_id(self): - """Return the unique id.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.device_name} {self.device_variable}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def should_poll(self): - """Do not poll.""" - return False - - @callback - def _async_update_callback(self): - """Update and write state.""" - self._async_update_device_data() - self.async_write_ha_state() - - @callback - def _async_update_device_data(self): - """Handle update callbacks.""" - try: - data = self._tado.data["device"][self.device_id] - except KeyError: - return - - if self.device_variable == "tado bridge status": - self._state = data.get("connectionState", {}).get("value", False) - - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self.device_id)}, - "name": self.device_name, - "manufacturer": DEFAULT_NAME, - "model": TADO_BRIDGE, - } diff --git a/homeassistant/components/tado/translations/pt.json b/homeassistant/components/tado/translations/pt.json index 4a071063d4..7953cf5625 100644 --- a/homeassistant/components/tado/translations/pt.json +++ b/homeassistant/components/tado/translations/pt.json @@ -1,6 +1,11 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { diff --git a/homeassistant/components/tado/translations/zh-Hant.json b/homeassistant/components/tado/translations/zh-Hant.json index 59e2d80c56..9126e0e4ea 100644 --- a/homeassistant/components/tado/translations/zh-Hant.json +++ b/homeassistant/components/tado/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index 1a99db5c24..3fcdb6426f 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -113,7 +113,6 @@ def create_water_heater_entity(tado, name: str, zone_id: int, zone: str): supports_temperature_control, min_temp, max_temp, - zone["devices"][0], ) return entity @@ -130,15 +129,14 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): supports_temperature_control, min_temp, max_temp, - device_info, ): """Initialize of Tado water heater entity.""" self._tado = tado - super().__init__(zone_name, device_info, tado.device_id, zone_id) + super().__init__(zone_name, tado.home_id, zone_id) self.zone_id = zone_id - self._unique_id = f"{zone_id} {tado.device_id}" + self._unique_id = f"{zone_id} {tado.home_id}" self._device_is_active = False @@ -163,7 +161,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format( - self._tado.device_id, "zone", self.zone_id + self._tado.home_id, "zone", self.zone_id ), self._async_update_callback, ) diff --git a/homeassistant/components/tag/__init__.py b/homeassistant/components/tag/__init__.py index 321dce9a29..6c385181aa 100644 --- a/homeassistant/components/tag/__init__.py +++ b/homeassistant/components/tag/__init__.py @@ -41,8 +41,8 @@ class TagIDExistsError(HomeAssistantError): """Raised when an item is not found.""" def __init__(self, item_id: str): - """Initialize tag id exists error.""" - super().__init__(f"Tag with id: {item_id} already exists.") + """Initialize tag ID exists error.""" + super().__init__(f"Tag with ID {item_id} already exists.") self.item_id = item_id diff --git a/homeassistant/components/tapsaff/manifest.json b/homeassistant/components/tapsaff/manifest.json index 7d78491ad1..30b9a2066c 100644 --- a/homeassistant/components/tapsaff/manifest.json +++ b/homeassistant/components/tapsaff/manifest.json @@ -3,5 +3,5 @@ "name": "Taps Aff", "documentation": "https://www.home-assistant.io/integrations/tapsaff", "requirements": ["tapsaff==0.2.1"], - "codeowners": [] + "codeowners": ["@bazwilliams"] } diff --git a/homeassistant/components/tasmota/device_trigger.py b/homeassistant/components/tasmota/device_trigger.py index f06d815e5c..463b1c65a9 100644 --- a/homeassistant/components/tasmota/device_trigger.py +++ b/homeassistant/components/tasmota/device_trigger.py @@ -157,7 +157,7 @@ async def async_setup_trigger(hass, tasmota_trigger, config_entry, discovery_has discovery_id = tasmota_trigger.cfg.trigger_id remove_update_signal = None _LOGGER.debug( - "Discovered trigger with id: %s '%s'", discovery_id, tasmota_trigger.cfg + "Discovered trigger with ID: %s '%s'", discovery_id, tasmota_trigger.cfg ) async def discovery_update(trigger_config): diff --git a/homeassistant/components/tasmota/fan.py b/homeassistant/components/tasmota/fan.py index 362149e9fc..bdcc00dc76 100644 --- a/homeassistant/components/tasmota/fan.py +++ b/homeassistant/components/tasmota/fan.py @@ -63,7 +63,7 @@ class TasmotaFan( @property def speed_list(self): """Get the list of available speeds.""" - return list(HA_TO_TASMOTA_SPEED_MAP.keys()) + return list(HA_TO_TASMOTA_SPEED_MAP) @property def supported_features(self): @@ -72,6 +72,8 @@ class TasmotaFan( async def async_set_speed(self, speed): """Set the speed of the fan.""" + if speed not in HA_TO_TASMOTA_SPEED_MAP: + raise ValueError(f"Unsupported speed {speed}") if speed == fan.SPEED_OFF: await self.async_turn_off() else: diff --git a/homeassistant/components/tasmota/translations/hu.json b/homeassistant/components/tasmota/translations/hu.json new file mode 100644 index 0000000000..c76efd0e89 --- /dev/null +++ b/homeassistant/components/tasmota/translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "config": { + "title": "Tasmota" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/pt.json b/homeassistant/components/tasmota/translations/pt.json new file mode 100644 index 0000000000..3df19f11fa --- /dev/null +++ b/homeassistant/components/tasmota/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_discovery_topic": "Prefixo do t\u00f3pico para descoberta inv\u00e1lido." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "Prefixo do t\u00f3pico para descoberta" + }, + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/zh-Hant.json b/homeassistant/components/tasmota/translations/zh-Hant.json index 1431fc8e1b..477eb0ffa9 100644 --- a/homeassistant/components/tasmota/translations/zh-Hant.json +++ b/homeassistant/components/tasmota/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_discovery_topic": "\u63a2\u7d22\u4e3b\u984c prefix \u7121\u6548\u3002" diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index fc592c9e5c..b6ca788161 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -79,6 +79,7 @@ DOMAIN = "telegram_bot" SERVICE_SEND_MESSAGE = "send_message" SERVICE_SEND_PHOTO = "send_photo" SERVICE_SEND_STICKER = "send_sticker" +SERVICE_SEND_ANIMATION = "send_animation" SERVICE_SEND_VIDEO = "send_video" SERVICE_SEND_VOICE = "send_voice" SERVICE_SEND_DOCUMENT = "send_document" @@ -224,6 +225,7 @@ SERVICE_MAP = { SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE, SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_STICKER: SERVICE_SCHEMA_SEND_FILE, + SERVICE_SEND_ANIMATION: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VIDEO: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_VOICE: SERVICE_SCHEMA_SEND_FILE, SERVICE_SEND_DOCUMENT: SERVICE_SCHEMA_SEND_FILE, @@ -367,6 +369,7 @@ async def async_setup(hass, config): elif msgtype in [ SERVICE_SEND_PHOTO, SERVICE_SEND_STICKER, + SERVICE_SEND_ANIMATION, SERVICE_SEND_VIDEO, SERVICE_SEND_VOICE, SERVICE_SEND_DOCUMENT, @@ -550,7 +553,7 @@ class TelegramNotificationService: ) return params - def _send_msg(self, func_send, msg_error, *args_msg, **kwargs_msg): + def _send_msg(self, func_send, msg_error, message_tag, *args_msg, **kwargs_msg): """Send one message.""" try: @@ -569,7 +572,6 @@ class TelegramNotificationService: ATTR_CHAT_ID: chat_id, ATTR_MESSAGEID: message_id, } - message_tag = kwargs_msg.get(ATTR_MESSAGE_TAG) if message_tag is not None: event_data[ATTR_MESSAGE_TAG] = message_tag self.hass.bus.async_fire(EVENT_TELEGRAM_SENT, event_data) @@ -591,7 +593,17 @@ class TelegramNotificationService: for chat_id in self._get_target_chat_ids(target): _LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params) self._send_msg( - self.bot.sendMessage, "Error sending message", chat_id, text, **params + self.bot.send_message, + "Error sending message", + params[ATTR_MESSAGE_TAG], + chat_id, + text, + parse_mode=params[ATTR_PARSER], + disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV], + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_to_message_id=params[ATTR_REPLY_TO_MSGID], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], ) def delete_message(self, chat_id=None, **kwargs): @@ -600,7 +612,7 @@ class TelegramNotificationService: message_id, _ = self._get_msg_ids(kwargs, chat_id) _LOGGER.debug("Delete message %s in chat ID %s", message_id, chat_id) deleted = self._send_msg( - self.bot.deleteMessage, "Error deleting message", chat_id, message_id + self.bot.delete_message, "Error deleting message", None, chat_id, message_id ) # reduce message_id anyway: if self._last_message_id[chat_id] is not None: @@ -625,26 +637,41 @@ class TelegramNotificationService: text = f"{title}\n{message}" if title else message _LOGGER.debug("Editing message with ID %s", message_id or inline_message_id) return self._send_msg( - self.bot.editMessageText, + self.bot.edit_message_text, "Error editing text message", + params[ATTR_MESSAGE_TAG], text, chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, - **params, + parse_mode=params[ATTR_PARSER], + disable_web_page_preview=params[ATTR_DISABLE_WEB_PREV], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], ) if type_edit == SERVICE_EDIT_CAPTION: - func_send = self.bot.editMessageCaption - params[ATTR_CAPTION] = kwargs.get(ATTR_CAPTION) - else: - func_send = self.bot.editMessageReplyMarkup + return self._send_msg( + self.bot.edit_message_caption, + "Error editing message attributes", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + message_id=message_id, + inline_message_id=inline_message_id, + caption=kwargs.get(ATTR_CAPTION), + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + return self._send_msg( - func_send, + self.bot.edit_message_reply_markup, "Error editing message attributes", + params[ATTR_MESSAGE_TAG], chat_id=chat_id, message_id=message_id, inline_message_id=inline_message_id, - **params, + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], ) def answer_callback_query( @@ -659,25 +686,18 @@ class TelegramNotificationService: show_alert, ) self._send_msg( - self.bot.answerCallbackQuery, + self.bot.answer_callback_query, "Error sending answer callback query", + params[ATTR_MESSAGE_TAG], callback_query_id, text=message, show_alert=show_alert, - **params, + timeout=params[ATTR_TIMEOUT], ) def send_file(self, file_type=SERVICE_SEND_PHOTO, target=None, **kwargs): """Send a photo, sticker, video, or document.""" params = self._get_msg_kwargs(kwargs) - caption = kwargs.get(ATTR_CAPTION) - func_send = { - SERVICE_SEND_PHOTO: self.bot.sendPhoto, - SERVICE_SEND_STICKER: self.bot.sendSticker, - SERVICE_SEND_VIDEO: self.bot.sendVideo, - SERVICE_SEND_VOICE: self.bot.sendVoice, - SERVICE_SEND_DOCUMENT: self.bot.sendDocument, - }.get(file_type) file_content = load_data( self.hass, url=kwargs.get(ATTR_URL), @@ -687,17 +707,89 @@ class TelegramNotificationService: authentication=kwargs.get(ATTR_AUTHENTICATION), verify_ssl=kwargs.get(ATTR_VERIFY_SSL), ) + if file_content: for chat_id in self._get_target_chat_ids(target): - _LOGGER.debug("Send file to chat ID %s. Caption: %s", chat_id, caption) - self._send_msg( - func_send, - "Error sending file", - chat_id, - file_content, - caption=caption, - **params, - ) + _LOGGER.debug("Sending file to chat ID %s", chat_id) + + if file_type == SERVICE_SEND_PHOTO: + self._send_msg( + self.bot.send_photo, + "Error sending photo", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + photo=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + + elif file_type == SERVICE_SEND_STICKER: + self._send_msg( + self.bot.send_sticker, + "Error sending sticker", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + sticker=file_content, + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + ) + + elif file_type == SERVICE_SEND_VIDEO: + self._send_msg( + self.bot.send_video, + "Error sending video", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + video=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + elif file_type == SERVICE_SEND_DOCUMENT: + self._send_msg( + self.bot.send_document, + "Error sending document", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + document=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + elif file_type == SERVICE_SEND_VOICE: + self._send_msg( + self.bot.send_voice, + "Error sending voice", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + voice=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + ) + elif file_type == SERVICE_SEND_ANIMATION: + self._send_msg( + self.bot.send_animation, + "Error sending animation", + params[ATTR_MESSAGE_TAG], + chat_id=chat_id, + animation=file_content, + caption=kwargs.get(ATTR_CAPTION), + disable_notification=params[ATTR_DISABLE_NOTIF], + reply_markup=params[ATTR_REPLYMARKUP], + timeout=params[ATTR_TIMEOUT], + parse_mode=params[ATTR_PARSER], + ) + file_content.seek(0) else: _LOGGER.error("Can't send file with kwargs: %s", kwargs) @@ -712,19 +804,23 @@ class TelegramNotificationService: "Send location %s/%s to chat ID %s", latitude, longitude, chat_id ) self._send_msg( - self.bot.sendLocation, + self.bot.send_location, "Error sending location", + params[ATTR_MESSAGE_TAG], chat_id=chat_id, latitude=latitude, longitude=longitude, - **params, + disable_notification=params[ATTR_DISABLE_NOTIF], + timeout=params[ATTR_TIMEOUT], ) def leave_chat(self, chat_id=None): """Remove bot from chat.""" chat_id = self._get_target_chat_ids(chat_id)[0] _LOGGER.debug("Leave from chat ID %s", chat_id) - leaved = self._send_msg(self.bot.leaveChat, "Error leaving chat", chat_id) + leaved = self._send_msg( + self.bot.leave_chat, "Error leaving chat", None, chat_id + ) return leaved diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index 29f6ade8af..80d9b50932 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -2,7 +2,7 @@ "domain": "telegram_bot", "name": "Telegram bot", "documentation": "https://www.home-assistant.io/integrations/telegram_bot", - "requirements": ["python-telegram-bot==11.1.0", "PySocks==1.7.1"], + "requirements": ["python-telegram-bot==13.1", "PySocks==1.7.1"], "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 8bdeef2511..b617826411 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -3,10 +3,10 @@ import logging from telegram import Update from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut -from telegram.ext import Handler, Updater +from telegram.ext import CallbackContext, Dispatcher, Handler, Updater +from telegram.utils.types import HandlerArg from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback from . import CONF_ALLOWED_CHAT_IDS, BaseTelegramBotEntity, initialize_bot @@ -18,12 +18,10 @@ async def async_setup_platform(hass, config): bot = initialize_bot(config) pol = TelegramPoll(bot, hass, config[CONF_ALLOWED_CHAT_IDS]) - @callback def _start_bot(_event): """Start the bot.""" pol.start_polling() - @callback def _stop_bot(_event): """Stop the bot.""" pol.stop_polling() @@ -34,15 +32,15 @@ async def async_setup_platform(hass, config): return True -def process_error(bot, update, error): +def process_error(update: Update, context: CallbackContext): """Telegram bot error handler.""" try: - raise error + raise context.error except (TimedOut, NetworkError, RetryAfter): # Long polling timeout or connection problem. Nothing serious. pass except TelegramError: - _LOGGER.error('Update "%s" caused error "%s"', update, error) + _LOGGER.error('Update "%s" caused error: "%s"', update, context.error) def message_handler(handler): @@ -59,10 +57,17 @@ def message_handler(handler): """Check is update valid.""" return isinstance(update, Update) - def handle_update(self, update, dispatcher): + def handle_update( + self, + update: HandlerArg, + dispatcher: Dispatcher, + check_result: object, + context: CallbackContext = None, + ): """Handle update.""" optional_args = self.collect_optional_args(dispatcher, update) - return self.callback(dispatcher.bot, update, **optional_args) + context.args = optional_args + return self.callback(update, context) return MessageHandler() @@ -89,6 +94,6 @@ class TelegramPoll(BaseTelegramBotEntity): """Stop the polling task.""" self.updater.stop() - def process_update(self, bot, update): + def process_update(self, update: HandlerArg, context: CallbackContext): """Process incoming message.""" self.process_message(update.to_dict()) diff --git a/homeassistant/components/telegram_bot/services.yaml b/homeassistant/components/telegram_bot/services.yaml index a4e0adc81a..5e2b06564d 100644 --- a/homeassistant/components/telegram_bot/services.yaml +++ b/homeassistant/components/telegram_bot/services.yaml @@ -13,7 +13,7 @@ send_message: description: An array of pre-authorized chat_ids to send the notification to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" parse_mode: - description: "Parser for the message text: `html` or `markdown`." + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." example: "html" disable_notification: description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. @@ -55,6 +55,9 @@ send_photo: target: description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + parse_mode: + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + example: "html" disable_notification: description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. example: true @@ -78,10 +81,10 @@ send_sticker: description: Send a sticker. fields: url: - description: Remote path to an webp sticker. + description: Remote path to a static .webp or animated .tgs sticker. example: "http://example.org/path/to/the/sticker.webp" file: - description: Local path to an webp sticker. + description: Local path to a static .webp or animated .tgs sticker. example: "/path/to/the/sticker.webp" username: description: Username for a URL which require HTTP basic authentication. @@ -111,6 +114,46 @@ send_sticker: description: 'Tag for sent message. In telegram_sent event data: {{trigger.event.data.message_tag}}' example: "msg_to_edit" +send_animation: + description: Send an anmiation. + fields: + url: + description: Remote path to a GIF or H.264/MPEG-4 AVC video without sound. + example: "http://example.org/path/to/the/animation.gif" + file: + description: Local path to a GIF or H.264/MPEG-4 AVC video without sound. + example: "/path/to/the/animation.gif" + caption: + description: The title of the animation. + example: "My animation" + username: + description: Username for a URL which require HTTP basic authentication. + example: myuser + password: + description: Password for a URL which require HTTP basic authentication. + example: myuser_pwd + target: + description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. + example: "[12345, 67890] or 12345" + parse_mode: + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + example: "html" + disable_notification: + description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. + example: true + verify_ssl: + description: Enable or disable SSL certificate verification. Set to false if you're downloading the file from a URL and you don't want to validate the SSL certificate of the server. + example: false + timeout: + description: Timeout for send sticker. Will help with timeout errors (poor internet connection, etc) + example: "1000" + keyboard: + description: List of rows of commands, comma-separated, to make a custom keyboard. + example: '["/command1, /command2", "/command3"]' + inline_keyboard: + description: List of rows of commands, comma-separated, to make a custom inline keyboard with buttons with associated callback data. + example: '["/button1, /button2", "/button3"] or [[["Text button1", "/button1"], ["Text button2", "/button2"]], [["Text button3", "/button3"]]]' + send_video: description: Send a video. fields: @@ -118,7 +161,7 @@ send_video: description: Remote path to a video. example: "http://example.org/path/to/the/video.mp4" file: - description: Local path to an image. + description: Local path to a video. example: "/path/to/the/video.mp4" caption: description: The title of the video. @@ -132,6 +175,9 @@ send_video: target: description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + parse_mode: + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + example: "html" disable_notification: description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. example: true @@ -212,6 +258,9 @@ send_document: target: description: An array of pre-authorized chat_ids to send the document to. If not present, first allowed chat_id is the default. example: "[12345, 67890] or 12345" + parse_mode: + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." + example: "html" disable_notification: description: Sends the message silently. iOS users and Web users will not receive a notification, Android users will receive a notification with no sound. example: true @@ -275,7 +324,7 @@ edit_message: description: Optional title for your notification. Will be composed as '%title\n%message' example: "Your Garage Door Friend" parse_mode: - description: "Parser for the message text: `html` or `markdown`." + description: "Parser for the message text: `markdownv2`, `html` or `markdown`." example: "html" disable_web_page_preview: description: Disables link previews for links in the message. @@ -325,6 +374,9 @@ answer_callback_query: show_alert: description: Show a permanent notification. example: true + timeout: + description: Timeout for sending the answer. Will help with timeout errors (poor internet connection, etc) + example: "1000" delete_message: description: Delete a previously sent message. diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index 768f114ac4..a1f6f595a0 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Dienst ist bereits konfiguriert", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "unknown": "Unbekannter Fehler ist aufgetreten" + "unknown": "Unbekannter Fehler ist aufgetreten", + "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, "step": { "auth": { diff --git a/homeassistant/components/tellduslive/translations/no.json b/homeassistant/components/tellduslive/translations/no.json index 95bd22cbec..649de0f86e 100644 --- a/homeassistant/components/tellduslive/translations/no.json +++ b/homeassistant/components/tellduslive/translations/no.json @@ -2,17 +2,17 @@ "config": { "abort": { "already_configured": "Tjenesten er allerede konfigurert", - "authorize_url_fail": "Ukjent feil ved oppretting av godkjenningsadresse.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", + "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", "unknown": "Uventet feil", - "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." + "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" }, "error": { "invalid_auth": "Ugyldig godkjenning" }, "step": { "auth": { - "description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SUBMIT**. \n\n [Link TelldusLive-konto]({auth_url})", + "description": "For \u00e5 koble TelldusLive-kontoen din:\n 1. Klikk p\u00e5 linken under\n 2. Logg inn p\u00e5 Telldus Live \n 3. Tillat **{app_name}** (klikk**Ja**). \n 4. Kom tilbake hit og klikk **SEND**. \n\n [TelldusLive-konto]({auth_url})", "title": "Godkjenn mot TelldusLive" }, "user": { diff --git a/homeassistant/components/tellduslive/translations/pt.json b/homeassistant/components/tellduslive/translations/pt.json index 6030c97244..cde0a2ad9c 100644 --- a/homeassistant/components/tellduslive/translations/pt.json +++ b/homeassistant/components/tellduslive/translations/pt.json @@ -1,9 +1,14 @@ { "config": { "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", - "unknown": "Ocorreu um erro desconhecido" + "unknown": "Ocorreu um erro desconhecido", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" }, "step": { "auth": { diff --git a/homeassistant/components/tellduslive/translations/sl.json b/homeassistant/components/tellduslive/translations/sl.json index 9feea6d628..ec94501527 100644 --- a/homeassistant/components/tellduslive/translations/sl.json +++ b/homeassistant/components/tellduslive/translations/sl.json @@ -3,7 +3,8 @@ "abort": { "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje URL-ja je potekla.", - "unknown": "Pri\u0161lo je do neznane napake" + "unknown": "Pri\u0161lo je do neznane napake", + "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." }, "step": { "auth": { diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index 67ad3cc5e5..09100c355c 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tesla/translations/pt.json b/homeassistant/components/tesla/translations/pt.json index 0df67a9418..c249c325ad 100644 --- a/homeassistant/components/tesla/translations/pt.json +++ b/homeassistant/components/tesla/translations/pt.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "already_configured": "Conta j\u00e1 configurada", + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json index cd8edbc3e5..670f57df8b 100644 --- a/homeassistant/components/tibber/translations/de.json +++ b/homeassistant/components/tibber/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Ein Tibber-Konto ist bereits konfiguriert." }, "error": { + "cannot_connect": "Verbindungsfehler", "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "timeout": "Zeit\u00fcberschreitung beim Verbinden mit Tibber" }, diff --git a/homeassistant/components/tibber/translations/pt.json b/homeassistant/components/tibber/translations/pt.json index 23f4662a4c..941089ee0c 100644 --- a/homeassistant/components/tibber/translations/pt.json +++ b/homeassistant/components/tibber/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O servi\u00e7o j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_access_token": "Token de acesso inv\u00e1lido" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tile/translations/nl.json b/homeassistant/components/tile/translations/nl.json index 31529d69a2..26c5726868 100644 --- a/homeassistant/components/tile/translations/nl.json +++ b/homeassistant/components/tile/translations/nl.json @@ -7,7 +7,18 @@ "user": { "data": { "password": "Wachtwoord" - } + }, + "title": "Tegel configureren" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Toon inactieve tegels" + }, + "title": "Tegel configureren" } } } diff --git a/homeassistant/components/tile/translations/pt.json b/homeassistant/components/tile/translations/pt.json index e266cf0626..bfafaa77b4 100644 --- a/homeassistant/components/tile/translations/pt.json +++ b/homeassistant/components/tile/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index 4f4dd8a095..d9060a719d 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_agreements": "Dieses Konto hat keine Toon-Anzeigen." + "no_agreements": "Dieses Konto hat keine Toon-Anzeigen.", + "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/no.json b/homeassistant/components/toon/translations/no.json index e5a72f35e2..a64a64ab74 100644 --- a/homeassistant/components/toon/translations/no.json +++ b/homeassistant/components/toon/translations/no.json @@ -2,12 +2,12 @@ "config": { "abort": { "already_configured": "Den valgte avtalen er allerede konfigurert.", - "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.", + "authorize_url_fail": "Ukjent feil ved generering av godkjenningsadresse", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_agreements": "Denne kontoen har ingen Toon skjermer.", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})", - "unknown_authorize_url_generation": "Ukjent feil ved generering av autoriseringsadresse." + "unknown_authorize_url_generation": "Ukjent feil ved generering av godkjenningsadresse" }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/pt.json b/homeassistant/components/toon/translations/pt.json index 9ecaef216f..e4aaaa3913 100644 --- a/homeassistant/components/toon/translations/pt.json +++ b/homeassistant/components/toon/translations/pt.json @@ -2,7 +2,10 @@ "config": { "abort": { "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", - "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "unknown_authorize_url_generation": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/sl.json b/homeassistant/components/toon/translations/sl.json index 1883a5ab05..3a015b5ad6 100644 --- a/homeassistant/components/toon/translations/sl.json +++ b/homeassistant/components/toon/translations/sl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_agreements": "Ta ra\u010dun nima prikazov Toon." + "no_agreements": "Ta ra\u010dun nima prikazov Toon.", + "unknown_authorize_url_generation": "Neznana napaka pri ustvarjanju overitvenega url." } } } \ No newline at end of file diff --git a/homeassistant/components/toon/translations/zh-Hant.json b/homeassistant/components/toon/translations/zh-Hant.json index daf5ff0ec1..46f6f6cf16 100644 --- a/homeassistant/components/toon/translations/zh-Hant.json +++ b/homeassistant/components/toon/translations/zh-Hant.json @@ -5,7 +5,7 @@ "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\u88dd\u7f6e\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})", "unknown_authorize_url_generation": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, diff --git a/homeassistant/components/totalconnect/translations/pt.json b/homeassistant/components/totalconnect/translations/pt.json index d399370847..3c17682089 100644 --- a/homeassistant/components/totalconnect/translations/pt.json +++ b/homeassistant/components/totalconnect/translations/pt.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Conta j\u00e1 configurada" }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tplink/translations/zh-Hant.json b/homeassistant/components/tplink/translations/zh-Hant.json index e88d982b8a..2fac2ac142 100644 --- a/homeassistant/components/tplink/translations/zh-Hant.json +++ b/homeassistant/components/tplink/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u8a2d\u5099\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a TP-Link \u667a\u80fd\u88dd\u7f6e\uff1f" } } } diff --git a/homeassistant/components/traccar/translations/pt.json b/homeassistant/components/traccar/translations/pt.json new file mode 100644 index 0000000000..3d0630027a --- /dev/null +++ b/homeassistant/components/traccar/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/zh-Hant.json b/homeassistant/components/traccar/translations/zh-Hant.json index 71d22d66cb..2204e7c332 100644 --- a/homeassistant/components/traccar/translations/zh-Hant.json +++ b/homeassistant/components/traccar/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 57f58f0599..5c6bf76a16 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -3,7 +3,7 @@ "name": "IKEA TRÅDFRI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", - "requirements": ["pytradfri[async]==7.0.5"], + "requirements": ["pytradfri[async]==7.0.6"], "homekit": { "models": ["TRADFRI"] }, diff --git a/homeassistant/components/tradfri/translations/pt.json b/homeassistant/components/tradfri/translations/pt.json index e4cf0e9787..a92f8d4dbd 100644 --- a/homeassistant/components/tradfri/translations/pt.json +++ b/homeassistant/components/tradfri/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Bridge j\u00e1 est\u00e1 configurada" + "already_configured": "Bridge j\u00e1 est\u00e1 configurada", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" }, "error": { "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar \u00e0 gateway.", diff --git a/homeassistant/components/tradfri/translations/zh-Hant.json b/homeassistant/components/tradfri/translations/zh-Hant.json index 21b232b757..9a48c1bc52 100644 --- a/homeassistant/components/tradfri/translations/zh-Hant.json +++ b/homeassistant/components/tradfri/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { diff --git a/homeassistant/components/transmission/translations/pt.json b/homeassistant/components/transmission/translations/pt.json index a68c763550..c3d4131d99 100644 --- a/homeassistant/components/transmission/translations/pt.json +++ b/homeassistant/components/transmission/translations/pt.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "name_exists": "Nome j\u00e1 existe" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/transmission/translations/zh-Hant.json b/homeassistant/components/transmission/translations/zh-Hant.json index fc75254a9e..5329ceb31e 100644 --- a/homeassistant/components/transmission/translations/zh-Hant.json +++ b/homeassistant/components/transmission/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/tuya/translations/ca.json b/homeassistant/components/tuya/translations/ca.json index d891eea20a..908cf287ee 100644 --- a/homeassistant/components/tuya/translations/ca.json +++ b/homeassistant/components/tuya/translations/ca.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, "error": { "dev_multi_type": "Per configurar una selecci\u00f3 de m\u00faltiples dispositius, aquests han de ser del mateix tipus", "dev_not_config": "El tipus d'aquest dispositiu no \u00e9s configurable", @@ -42,7 +45,7 @@ "tuya_max_coltemp": "Temperatura de color m\u00e0xima enviada pel dispositiu", "unit_of_measurement": "Unitat de temperatura utilitzada pel dispositiu" }, - "description": "Configura les opcions per ajustar la informaci\u00f3 mostrada per {device_type} dispositiu `{device_name}`", + "description": "Configura les opcions per ajustar la informaci\u00f3 mostrada pel dispositiu {device_type} `{device_name}`", "title": "Configuraci\u00f3 de dispositiu Tuya" }, "init": { diff --git a/homeassistant/components/tuya/translations/cs.json b/homeassistant/components/tuya/translations/cs.json index 99cf4be4ff..1dda4ea6df 100644 --- a/homeassistant/components/tuya/translations/cs.json +++ b/homeassistant/components/tuya/translations/cs.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "error": { "dev_multi_type": "V\u00edce vybran\u00fdch za\u0159\u00edzen\u00ed k nastaven\u00ed mus\u00ed b\u00fdt stejn\u00e9ho typu", "dev_not_config": "Typ za\u0159\u00edzen\u00ed nelze nastavit", diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 07e72a2960..4cdcdfced7 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "cannot_connect": "Verbindungsfehler", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "flow_title": "Tuya Konfiguration", "step": { "user": { @@ -13,6 +17,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "error": { "dev_not_config": "Ger\u00e4tetyp nicht konfigurierbar", "dev_not_found": "Ger\u00e4t nicht gefunden" diff --git a/homeassistant/components/tuya/translations/en.json b/homeassistant/components/tuya/translations/en.json index 75c84a5337..46756b18cb 100644 --- a/homeassistant/components/tuya/translations/en.json +++ b/homeassistant/components/tuya/translations/en.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Failed to connect" + }, "error": { "dev_multi_type": "Multiple selected devices to configure must be of the same type", "dev_not_config": "Device type not configurable", diff --git a/homeassistant/components/tuya/translations/es.json b/homeassistant/components/tuya/translations/es.json index 3107b919e5..cd8da78187 100644 --- a/homeassistant/components/tuya/translations/es.json +++ b/homeassistant/components/tuya/translations/es.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "No se pudo conectar" + }, "error": { "dev_multi_type": "Los m\u00faltiples dispositivos seleccionados para configurar deben ser del mismo tipo", "dev_not_config": "Tipo de dispositivo no configurable", diff --git a/homeassistant/components/tuya/translations/et.json b/homeassistant/components/tuya/translations/et.json index 52f502b546..967b38cdb8 100644 --- a/homeassistant/components/tuya/translations/et.json +++ b/homeassistant/components/tuya/translations/et.json @@ -17,12 +17,15 @@ "platform": "\u00c4pp kus teie konto registreeriti", "username": "Kasutajanimi" }, - "description": "Sisestage oma Tuya konto andmed.", + "description": "Sisesta oma Tuya konto andmed.", "title": "" } } }, "options": { + "abort": { + "cannot_connect": "\u00dchendamine nurjus" + }, "error": { "dev_multi_type": "Mitu h\u00e4\u00e4lestatavat seadet peavad olema sama t\u00fc\u00fcpi", "dev_not_config": "Seda t\u00fc\u00fcpi seade pole seadistatav", diff --git a/homeassistant/components/tuya/translations/fr.json b/homeassistant/components/tuya/translations/fr.json index e25dcb40d4..9ef1c325d1 100644 --- a/homeassistant/components/tuya/translations/fr.json +++ b/homeassistant/components/tuya/translations/fr.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Impossible de se connecter" + }, "error": { "dev_multi_type": "Plusieurs p\u00e9riph\u00e9riques s\u00e9lectionn\u00e9s \u00e0 configurer doivent \u00eatre du m\u00eame type", "dev_not_config": "Type d'appareil non configurable", diff --git a/homeassistant/components/tuya/translations/hu.json b/homeassistant/components/tuya/translations/hu.json index 7c90b93732..b128be6708 100644 --- a/homeassistant/components/tuya/translations/hu.json +++ b/homeassistant/components/tuya/translations/hu.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "A kapcsol\u00f3d\u00e1s nem siker\u00fclt" + }, "error": { "dev_multi_type": "A konfigur\u00e1land\u00f3 eszk\u00f6z\u00f6knek azonos t\u00edpus\u00faaknak kell lennie", "dev_not_config": "Ez az eszk\u00f6zt\u00edpus nem konfigur\u00e1lhat\u00f3", diff --git a/homeassistant/components/tuya/translations/it.json b/homeassistant/components/tuya/translations/it.json index 1277d8fca8..639f583492 100644 --- a/homeassistant/components/tuya/translations/it.json +++ b/homeassistant/components/tuya/translations/it.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Impossibile connettersi" + }, "error": { "dev_multi_type": "Pi\u00f9 dispositivi selezionati da configurare devono essere dello stesso tipo", "dev_not_config": "Tipo di dispositivo non configurabile", diff --git a/homeassistant/components/tuya/translations/no.json b/homeassistant/components/tuya/translations/no.json index 38f054ae4c..d0c1a3ca18 100644 --- a/homeassistant/components/tuya/translations/no.json +++ b/homeassistant/components/tuya/translations/no.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Tilkobling mislyktes" + }, "error": { "dev_multi_type": "Flere valgte enheter som skal konfigureres, m\u00e5 v\u00e6re av samme type", "dev_not_config": "Enhetstype kan ikke konfigureres", diff --git a/homeassistant/components/tuya/translations/pl.json b/homeassistant/components/tuya/translations/pl.json index ba4810c3f9..a24c1dbe26 100644 --- a/homeassistant/components/tuya/translations/pl.json +++ b/homeassistant/components/tuya/translations/pl.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, "error": { "dev_multi_type": "Wybrane urz\u0105dzenia do skonfigurowania musz\u0105 by\u0107 tego samego typu", "dev_not_config": "Typ urz\u0105dzenia nie jest konfigurowalny", diff --git a/homeassistant/components/tuya/translations/pt.json b/homeassistant/components/tuya/translations/pt.json index b8a454fbab..566746538c 100644 --- a/homeassistant/components/tuya/translations/pt.json +++ b/homeassistant/components/tuya/translations/pt.json @@ -1,11 +1,25 @@ { "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Nome de Utilizador" } } } + }, + "options": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } } } \ No newline at end of file diff --git a/homeassistant/components/tuya/translations/ru.json b/homeassistant/components/tuya/translations/ru.json index 31e2791c9f..b98c6c8e9c 100644 --- a/homeassistant/components/tuya/translations/ru.json +++ b/homeassistant/components/tuya/translations/ru.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, "error": { "dev_multi_type": "\u041d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043e\u043b\u0436\u043d\u044b \u0431\u044b\u0442\u044c \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430.", "dev_not_config": "\u0422\u0438\u043f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", diff --git a/homeassistant/components/tuya/translations/sl.json b/homeassistant/components/tuya/translations/sl.json index 4879603af6..b07ad70ada 100644 --- a/homeassistant/components/tuya/translations/sl.json +++ b/homeassistant/components/tuya/translations/sl.json @@ -1,5 +1,8 @@ { "options": { + "abort": { + "cannot_connect": "Povezovanje ni uspelo." + }, "error": { "dev_not_config": "Vrsta naprave ni nastavljiva", "dev_not_found": "Naprave ni mogo\u010de najti" diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index d2af1633f9..5a4de08033 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -1,8 +1,27 @@ { "options": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { + "device": { + "data": { + "max_temp": "Maksimum hedef s\u0131cakl\u0131k (varsay\u0131lan olarak min ve maks = 0 kullan\u0131n)", + "min_kelvin": "Kelvin destekli min renk s\u0131cakl\u0131\u011f\u0131", + "min_temp": "Minimum hedef s\u0131cakl\u0131k (varsay\u0131lan i\u00e7in min ve maks = 0 kullan\u0131n)", + "support_color": "Vurgu rengi", + "temp_divider": "S\u0131cakl\u0131k de\u011ferleri ay\u0131r\u0131c\u0131 (0 = varsay\u0131lan\u0131 kullan)", + "tuya_max_coltemp": "Cihaz taraf\u0131ndan bildirilen maksimum renk s\u0131cakl\u0131\u011f\u0131", + "unit_of_measurement": "Cihaz\u0131n kulland\u0131\u011f\u0131 s\u0131cakl\u0131k birimi" + }, + "description": "{device_type} ayg\u0131t\u0131 '{device_name}' i\u00e7in g\u00f6r\u00fcnt\u00fclenen bilgileri ayarlamak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "title": "Tuya Cihaz\u0131n\u0131 Yap\u0131land\u0131r\u0131n" + }, "init": { "data": { + "discovery_interval": "Cihaz\u0131 yoklama aral\u0131\u011f\u0131 saniye cinsinden", + "list_devices": "Yap\u0131land\u0131rmay\u0131 kaydetmek i\u00e7in yap\u0131land\u0131r\u0131lacak veya bo\u015f b\u0131rak\u0131lacak cihazlar\u0131 se\u00e7in", + "query_device": "Daha h\u0131zl\u0131 durum g\u00fcncellemesi i\u00e7in sorgu y\u00f6ntemini kullanacak cihaz\u0131 se\u00e7in", "query_interval": "Ayg\u0131t yoklama aral\u0131\u011f\u0131 saniye cinsinden" }, "description": "Yoklama aral\u0131\u011f\u0131 de\u011ferlerini \u00e7ok d\u00fc\u015f\u00fck ayarlamay\u0131n, aksi takdirde \u00e7a\u011fr\u0131lar g\u00fcnl\u00fckte hata mesaj\u0131 olu\u015fturarak ba\u015far\u0131s\u0131z olur", diff --git a/homeassistant/components/tuya/translations/zh-Hans.json b/homeassistant/components/tuya/translations/zh-Hans.json index a5f4ff11f0..ff3887c840 100644 --- a/homeassistant/components/tuya/translations/zh-Hans.json +++ b/homeassistant/components/tuya/translations/zh-Hans.json @@ -1,10 +1,59 @@ { "config": { + "abort": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25", + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548", + "single_instance_allowed": "\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86\uff0c\u4e14\u53ea\u80fd\u914d\u7f6e\u4e00\u6b21\u3002" + }, + "error": { + "invalid_auth": "\u8eab\u4efd\u8ba4\u8bc1\u65e0\u6548" + }, + "flow_title": "\u6d82\u9e26\u914d\u7f6e", "step": { "user": { "data": { + "country_code": "\u60a8\u7684\u5e10\u6237\u56fd\u5bb6(\u5730\u533a)\u4ee3\u7801\uff08\u4f8b\u5982\u4e2d\u56fd\u4e3a 86\uff0c\u7f8e\u56fd\u4e3a 1\uff09", + "password": "\u5bc6\u7801", + "platform": "\u60a8\u6ce8\u518c\u5e10\u6237\u7684\u5e94\u7528", "username": "\u7528\u6237\u540d" - } + }, + "description": "\u8bf7\u8f93\u5165\u6d82\u9e26\u8d26\u6237\u4fe1\u606f\u3002", + "title": "\u6d82\u9e26" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u8fde\u63a5\u5931\u8d25" + }, + "error": { + "dev_multi_type": "\u591a\u4e2a\u8981\u914d\u7f6e\u7684\u8bbe\u5907\u5fc5\u987b\u5177\u6709\u76f8\u540c\u7684\u7c7b\u578b", + "dev_not_config": "\u8bbe\u5907\u7c7b\u578b\u4e0d\u53ef\u914d\u7f6e", + "dev_not_found": "\u672a\u627e\u5230\u8bbe\u5907" + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u8bbe\u5907\u4f7f\u7528\u7684\u4eae\u5ea6\u8303\u56f4", + "max_kelvin": "\u6700\u9ad8\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", + "max_temp": "\u6700\u9ad8\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", + "min_kelvin": "\u6700\u4f4e\u652f\u6301\u8272\u6e29\uff08\u5f00\u6c0f\uff09", + "min_temp": "\u6700\u4f4e\u76ee\u6807\u6e29\u5ea6\uff08min \u548c max \u4e3a 0 \u65f6\u4f7f\u7528\u9ed8\u8ba4\uff09", + "support_color": "\u5f3a\u5236\u652f\u6301\u8c03\u8272", + "tuya_max_coltemp": "\u8bbe\u5907\u62a5\u544a\u7684\u6700\u9ad8\u8272\u6e29", + "unit_of_measurement": "\u8bbe\u5907\u4f7f\u7528\u7684\u6e29\u5ea6\u5355\u4f4d" + }, + "title": "\u914d\u7f6e\u6d82\u9e26\u8bbe\u5907" + }, + "init": { + "data": { + "discovery_interval": "\u53d1\u73b0\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09", + "list_devices": "\u8bf7\u9009\u62e9\u8981\u914d\u7f6e\u7684\u8bbe\u5907\uff0c\u6216\u7559\u7a7a\u4ee5\u4fdd\u5b58\u914d\u7f6e", + "query_device": "\u8bf7\u9009\u62e9\u4f7f\u7528\u67e5\u8be2\u65b9\u6cd5\u7684\u8bbe\u5907\uff0c\u4ee5\u4fbf\u66f4\u5feb\u5730\u66f4\u65b0\u72b6\u6001", + "query_interval": "\u67e5\u8be2\u8bbe\u5907\u8f6e\u8be2\u95f4\u9694\uff08\u4ee5\u79d2\u4e3a\u5355\u4f4d\uff09" + }, + "description": "\u8bf7\u4e0d\u8981\u5c06\u8f6e\u8be2\u95f4\u9694\u8bbe\u7f6e\u5f97\u592a\u4f4e\uff0c\u5426\u5219\u5c06\u8c03\u7528\u5931\u8d25\u5e76\u5728\u65e5\u5fd7\u751f\u6210\u9519\u8bef\u6d88\u606f", + "title": "\u914d\u7f6e\u6d82\u9e26\u9009\u9879" } } } diff --git a/homeassistant/components/tuya/translations/zh-Hant.json b/homeassistant/components/tuya/translations/zh-Hant.json index f2fb4c0926..08871c3108 100644 --- a/homeassistant/components/tuya/translations/zh-Hant.json +++ b/homeassistant/components/tuya/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" @@ -23,15 +23,18 @@ } }, "options": { + "abort": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, "error": { - "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u5099\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", - "dev_not_config": "\u8a2d\u5099\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", - "dev_not_found": "\u8a2d\u5099\u627e\u4e0d\u5230" + "dev_multi_type": "\u591a\u91cd\u9078\u64c7\u8a2d\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u4f7f\u7528\u76f8\u540c\u985e\u578b", + "dev_not_config": "\u88dd\u7f6e\u985e\u578b\u7121\u6cd5\u8a2d\u5b9a", + "dev_not_found": "\u627e\u4e0d\u5230\u88dd\u7f6e" }, "step": { "device": { "data": { - "brightness_range_mode": "\u8a2d\u5099\u6240\u4f7f\u7528\u4e4b\u4eae\u5ea6\u7bc4\u570d", + "brightness_range_mode": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u4eae\u5ea6\u7bc4\u570d", "curr_temp_divider": "\u76ee\u524d\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", "max_kelvin": "Kelvin \u652f\u63f4\u6700\u9ad8\u8272\u6eab", "max_temp": "\u6700\u9ad8\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", @@ -39,18 +42,18 @@ "min_temp": "\u6700\u4f4e\u76ee\u6a19\u8272\u6eab\uff08\u4f7f\u7528\u6700\u4f4e\u8207\u6700\u9ad8 = 0 \u4f7f\u7528\u9810\u8a2d\uff09", "support_color": "\u5f37\u5236\u8272\u6eab\u652f\u63f4", "temp_divider": "\u8272\u6eab\u503c\u5206\u914d\u5668\uff080 = \u4f7f\u7528\u9810\u8a2d\uff09", - "tuya_max_coltemp": "\u8a2d\u5099\u56de\u5831\u6700\u9ad8\u8272\u6eab", - "unit_of_measurement": "\u8a2d\u5099\u6240\u4f7f\u7528\u4e4b\u6eab\u5ea6\u55ae\u4f4d" + "tuya_max_coltemp": "\u88dd\u7f6e\u56de\u5831\u6700\u9ad8\u8272\u6eab", + "unit_of_measurement": "\u88dd\u7f6e\u6240\u4f7f\u7528\u4e4b\u6eab\u5ea6\u55ae\u4f4d" }, - "description": "\u8a2d\u5b9a\u9078\u9805\u4ee5\u8abf\u6574 {device_type} \u8a2d\u5099 `{device_name}` \u986f\u793a\u8cc7\u8a0a", - "title": "\u8a2d\u5b9a Tuya \u8a2d\u5099" + "description": "\u8a2d\u5b9a\u9078\u9805\u4ee5\u8abf\u6574 {device_type} \u88dd\u7f6e `{device_name}` \u986f\u793a\u8cc7\u8a0a", + "title": "\u8a2d\u5b9a Tuya \u88dd\u7f6e" }, "init": { "data": { - "discovery_interval": "\u63a2\u7d22\u8a2d\u5099\u66f4\u65b0\u79d2\u9593\u8ddd", - "list_devices": "\u9078\u64c7\u8a2d\u5099\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", - "query_device": "\u9078\u64c7\u8a2d\u5099\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", - "query_interval": "\u67e5\u8a62\u8a2d\u5099\u66f4\u65b0\u79d2\u9593\u8ddd" + "discovery_interval": "\u63a2\u7d22\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd", + "list_devices": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u8a2d\u5b9a\u3001\u6216\u4fdd\u6301\u7a7a\u767d\u4ee5\u5132\u5b58\u8a2d\u5b9a", + "query_device": "\u9078\u64c7\u88dd\u7f6e\u5c07\u4f7f\u7528\u67e5\u8a62\u65b9\u5f0f\u4ee5\u7372\u5f97\u66f4\u5feb\u7684\u72c0\u614b\u66f4\u65b0", + "query_interval": "\u67e5\u8a62\u88dd\u7f6e\u66f4\u65b0\u79d2\u9593\u8ddd" }, "description": "\u66f4\u65b0\u9593\u8ddd\u4e0d\u8981\u8a2d\u5b9a\u7684\u904e\u4f4e\u3001\u53ef\u80fd\u6703\u5c0e\u81f4\u65bc\u65e5\u8a8c\u4e2d\u7522\u751f\u932f\u8aa4\u8a0a\u606f", "title": "\u8a2d\u5b9a Tuya \u9078\u9805" diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 2ae8c2863a..27ba9bb29c 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "cannot_connect": "Verbindungsfehler", "invalid_address": "Adresse nicht im Einzugsgebiet von Twente Milieu gefunden." }, "step": { diff --git a/homeassistant/components/twentemilieu/translations/pt.json b/homeassistant/components/twentemilieu/translations/pt.json new file mode 100644 index 0000000000..451ff82e74 --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "A localiza\u00e7\u00e3o j\u00e1 est\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/pt.json b/homeassistant/components/twilio/translations/pt.json index a5a1d76bfb..997757d2bc 100644 --- a/homeassistant/components/twilio/translations/pt.json +++ b/homeassistant/components/twilio/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", + "webhook_not_internet_accessible": "O seu Home Assistant necessita estar acess\u00edvel a partir da Internet para receber mensagens do tipo webhook." + }, "create_entry": { "default": "Para enviar eventos para o Home Assistant, \u00e9 necess\u00e1rio configurar [Webhooks with Twilio] ({twilio_url}). \n\nPreencha as seguintes informa\u00e7\u00f5es: \n\n- URL: `{webhook_url}`\n- M\u00e9todo: POST \n- Tipo de Conte\u00fado: application/x-www-form-urlencoded \n\nVeja [a documenta\u00e7\u00e3o] ({docs_url}) sobre como configurar automa\u00e7\u00f5es para manipular dados de entrada." }, diff --git a/homeassistant/components/twilio/translations/zh-Hant.json b/homeassistant/components/twilio/translations/zh-Hant.json index 630afb0297..0776d7cb0e 100644 --- a/homeassistant/components/twilio/translations/zh-Hant.json +++ b/homeassistant/components/twilio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json new file mode 100644 index 0000000000..2b4c70a0ba --- /dev/null +++ b/homeassistant/components/twinkly/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, + "step": { + "user": { + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/nl.json b/homeassistant/components/twinkly/translations/nl.json new file mode 100644 index 0000000000..861ee57283 --- /dev/null +++ b/homeassistant/components/twinkly/translations/nl.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostnaam (of IP-adres van uw Twinkly apparaat" + }, + "description": "Uw Twinkly LED-string instellen", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/pt.json b/homeassistant/components/twinkly/translations/pt.json new file mode 100644 index 0000000000..abed97c893 --- /dev/null +++ b/homeassistant/components/twinkly/translations/pt.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "device_exists": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json new file mode 100644 index 0000000000..14365f988b --- /dev/null +++ b/homeassistant/components/twinkly/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Twinkly cihaz\u0131n\u0131z\u0131n ana bilgisayar\u0131 (veya IP adresi)" + }, + "description": "Twinkly led dizinizi ayarlay\u0131n", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/zh-Hant.json b/homeassistant/components/twinkly/translations/zh-Hant.json index a325d458ac..7e6a113e1e 100644 --- a/homeassistant/components/twinkly/translations/zh-Hant.json +++ b/homeassistant/components/twinkly/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "device_exists": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "device_exists": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -9,7 +9,7 @@ "step": { "user": { "data": { - "host": "Twinkly \u8a2d\u5099\u4e3b\u6a5f\u540d\u7a31\uff08\u6216 IP \u4f4d\u5740\uff09" + "host": "Twinkly \u88dd\u7f6e\u4e3b\u6a5f\u540d\u7a31\uff08\u6216 IP \u4f4d\u5740\uff09" }, "description": "\u8a2d\u5b9a Twinkly LED \u71c8\u4e32", "title": "Twinkly" diff --git a/homeassistant/components/unifi/translations/pt.json b/homeassistant/components/unifi/translations/pt.json index 354870a0d5..7a0a8e1a1f 100644 --- a/homeassistant/components/unifi/translations/pt.json +++ b/homeassistant/components/unifi/translations/pt.json @@ -41,6 +41,12 @@ "other": "Vazios" } }, + "simple_options": { + "data": { + "track_clients": "Acompanhar clientes da rede", + "track_devices": "Acompanhar dispositivos de rede (dispositivos Ubiquiti)" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Criar sensores de uso de largura de banda para clientes da rede" diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index b17ad0b551..d87f8cf51e 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -39,17 +39,17 @@ "ignore_wired_bug": "\u95dc\u9589 UniFi \u6709\u7dda\u932f\u8aa4\u908f\u8f2f", "ssid_filter": "\u9078\u64c7\u6240\u8981\u8ffd\u8e64\u7684\u7121\u7dda\u7db2\u8def", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", - "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" }, - "description": "\u8a2d\u5b9a\u8a2d\u5099\u8ffd\u8e64", + "description": "\u8a2d\u5b9a\u88dd\u7f6e\u8ffd\u8e64", "title": "UniFi \u9078\u9805 1/3" }, "simple_options": { "data": { "block_client": "\u7db2\u8def\u5b58\u53d6\u63a7\u5236\u5ba2\u6236\u7aef", "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", - "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09" + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09" }, "description": "\u8a2d\u5b9a UniFi \u6574\u5408" }, diff --git a/homeassistant/components/upb/translations/no.json b/homeassistant/components/upb/translations/no.json index 34295b718c..e280388eec 100644 --- a/homeassistant/components/upb/translations/no.json +++ b/homeassistant/components/upb/translations/no.json @@ -12,7 +12,7 @@ "user": { "data": { "address": "Adresse (se beskrivelse over)", - "file_path": "Sti og navn p\u00e5 UPStart UPB-eksportfilen.", + "file_path": "Bane og navn p\u00e5 UPStart UPB-eksportfilen.", "protocol": "Protokoll" }, "description": "Koble til en universal Powerline Bus Powerline Interface Module (UPB PIM). Adressestrengen m\u00e5 v\u00e6re i skjemaet 'adresse[:port]' for 'tcp'. Porten er valgfri og bruker som standard til 2101. Eksempel: '192.168.1.42'. For serieprotokollen m\u00e5 adressen v\u00e6re i skjemaet 'tty[:baud]'. Baud er valgfritt og standard til 4800. Eksempel: '/dev/ttyS1'.", diff --git a/homeassistant/components/upb/translations/pt.json b/homeassistant/components/upb/translations/pt.json index 0c5c776056..ae100e4584 100644 --- a/homeassistant/components/upb/translations/pt.json +++ b/homeassistant/components/upb/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, "error": { "unknown": "Erro inesperado" } diff --git a/homeassistant/components/upb/translations/zh-Hant.json b/homeassistant/components/upb/translations/zh-Hant.json index e4809c9b63..b121c005fa 100644 --- a/homeassistant/components/upb/translations/zh-Hant.json +++ b/homeassistant/components/upb/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/upcloud/translations/de.json b/homeassistant/components/upcloud/translations/de.json index ffdd1e0dd5..76bbc70569 100644 --- a/homeassistant/components/upcloud/translations/de.json +++ b/homeassistant/components/upcloud/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/upcloud/translations/pt.json b/homeassistant/components/upcloud/translations/pt.json new file mode 100644 index 0000000000..a2f3208768 --- /dev/null +++ b/homeassistant/components/upcloud/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index 008b007e2f..64423efed3 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -1,19 +1,19 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "incomplete_discovery": "\u672a\u5b8c\u6210\u63a2\u7d22", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "flow_title": "UPnP/IGD\uff1a{name}", "step": { "ssdp_confirm": { - "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD \u8a2d\u5099\uff1f" + "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD \u88dd\u7f6e\uff1f" }, "user": { "data": { "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09", - "usn": "\u8a2d\u5099" + "usn": "\u88dd\u7f6e" } } } diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 9a4ed9e778..6b25ec7d12 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -145,13 +145,7 @@ class UtilityMeterSensor(RestoreEntity): ): return - if ( - self._unit_of_measurement is None - and new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is not None - ): - self._unit_of_measurement = new_state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT - ) + self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) try: diff = Decimal(new_state.state) - Decimal(old_state.state) diff --git a/homeassistant/components/velbus/translations/de.json b/homeassistant/components/velbus/translations/de.json index d9013bea39..c6c872c85e 100644 --- a/homeassistant/components/velbus/translations/de.json +++ b/homeassistant/components/velbus/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/velbus/translations/pt.json b/homeassistant/components/velbus/translations/pt.json new file mode 100644 index 0000000000..94b13c6bc7 --- /dev/null +++ b/homeassistant/components/velbus/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/zh-Hant.json b/homeassistant/components/velbus/translations/zh-Hant.json index 28469cd1d9..f9bbe99d9c 100644 --- a/homeassistant/components/velbus/translations/zh-Hant.json +++ b/homeassistant/components/velbus/translations/zh-Hant.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index d9de9b9d55..68f762a54f 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -2,6 +2,6 @@ "domain": "venstar", "name": "Venstar", "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.12"], + "requirements": ["venstarcolortouch==0.13"], "codeowners": [] } diff --git a/homeassistant/components/vera/translations/et.json b/homeassistant/components/vera/translations/et.json index 9695048458..4afb098caa 100644 --- a/homeassistant/components/vera/translations/et.json +++ b/homeassistant/components/vera/translations/et.json @@ -22,7 +22,7 @@ "exclude": "Vera seadme ID-d mida Home Assistant'ist v\u00e4lja j\u00e4tta.", "lights": "Vera l\u00fclitite ID'd mida neid k\u00e4sitleda Home Assistantis tuledena." }, - "description": "Valikuliste parameetrite kohta leiad lisateavet Vera dokumentatsioonist: https://www.home-assistant.io/integrations/vera/. M\u00e4rkus: K\u00f5ikide muudatuste puhul on vaja taask\u00e4ivitada Home Assistant'i server. V\u00e4\u00e4rtuste kustutamiseks sisestage t\u00fchik.", + "description": "Valikuliste parameetrite kohta leiad lisateavet Vera dokumentatsioonist: https://www.home-assistant.io/integrations/vera/. M\u00e4rkus: K\u00f5ikide muudatuste puhul on vaja taask\u00e4ivitada Home Assistant'i server. V\u00e4\u00e4rtuste kustutamiseks sisesta t\u00fchik.", "title": "Vera kontrolleri valikud" } } diff --git a/homeassistant/components/vera/translations/zh-Hant.json b/homeassistant/components/vera/translations/zh-Hant.json index 7293ce9761..b8d7031ee1 100644 --- a/homeassistant/components/vera/translations/zh-Hant.json +++ b/homeassistant/components/vera/translations/zh-Hant.json @@ -6,8 +6,8 @@ "step": { "user": { "data": { - "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u8a2d\u5099 ID\u3002", - "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u8a2d\u5099 ID\u3002", + "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u88dd\u7f6e ID\u3002", + "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u88dd\u7f6e ID\u3002", "vera_controller_url": "\u63a7\u5236\u5668 URL" }, "description": "\u65bc\u4e0b\u65b9\u63d0\u4f9b Vera \u63a7\u5236\u5668 URL\u3002\u683c\u5f0f\u61c9\u8a72\u70ba\uff1ahttp://192.168.1.161:3480\u3002", @@ -19,8 +19,8 @@ "step": { "init": { "data": { - "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u8a2d\u5099 ID\u3002", - "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u8a2d\u5099 ID\u3002" + "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u88dd\u7f6e ID\u3002", + "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u88dd\u7f6e ID\u3002" }, "description": "\u8acb\u53c3\u95b1 Vera \u6587\u4ef6\u4ee5\u7372\u5f97\u8a73\u7d30\u7684\u9078\u9805\u53c3\u6578\u8cc7\u6599\uff1ahttps://www.home-assistant.io/integrations/vera/\u3002\u8acb\u6ce8\u610f\uff1a\u4efb\u4f55\u8b8a\u66f4\u90fd\u9700\u8981\u91cd\u555f Home Assistant\u3002\u6b32\u6e05\u9664\u8a2d\u5b9a\u503c\u3001\u8acb\u8f38\u5165\u7a7a\u683c\u3002", "title": "Vera \u63a7\u5236\u5668\u9078\u9805" diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 8cd8b0672c..2348d42a0d 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,7 +1,5 @@ """Support for Verisure devices.""" from datetime import timedelta -import logging -import threading from jsonpath import jsonpath import verisure @@ -18,30 +16,27 @@ from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -_LOGGER = logging.getLogger(__name__) - -ATTR_DEVICE_SERIAL = "device_serial" - -CONF_ALARM = "alarm" -CONF_CODE_DIGITS = "code_digits" -CONF_DOOR_WINDOW = "door_window" -CONF_GIID = "giid" -CONF_HYDROMETERS = "hygrometers" -CONF_LOCKS = "locks" -CONF_DEFAULT_LOCK_CODE = "default_lock_code" -CONF_MOUSE = "mouse" -CONF_SMARTPLUGS = "smartplugs" -CONF_THERMOMETERS = "thermometers" -CONF_SMARTCAM = "smartcam" - -DOMAIN = "verisure" - -MIN_SCAN_INTERVAL = timedelta(minutes=1) -DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) - -SERVICE_CAPTURE_SMARTCAM = "capture_smartcam" -SERVICE_DISABLE_AUTOLOCK = "disable_autolock" -SERVICE_ENABLE_AUTOLOCK = "enable_autolock" +from .const import ( + ATTR_DEVICE_SERIAL, + CONF_ALARM, + CONF_CODE_DIGITS, + CONF_DEFAULT_LOCK_CODE, + CONF_DOOR_WINDOW, + CONF_GIID, + CONF_HYDROMETERS, + CONF_LOCKS, + CONF_MOUSE, + CONF_SMARTCAM, + CONF_SMARTPLUGS, + CONF_THERMOMETERS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + LOGGER, + MIN_SCAN_INTERVAL, + SERVICE_CAPTURE_SMARTCAM, + SERVICE_DISABLE_AUTOLOCK, + SERVICE_ENABLE_AUTOLOCK, +) HUB = None @@ -101,9 +96,9 @@ def setup(hass, config): device_id = service.data[ATTR_DEVICE_SERIAL] try: await hass.async_add_executor_job(HUB.smartcam_capture, device_id) - _LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) + LOGGER.debug("Capturing new image from %s", ATTR_DEVICE_SERIAL) except verisure.Error as ex: - _LOGGER.error("Could not capture image, %s", ex) + LOGGER.error("Could not capture image, %s", ex) hass.services.register( DOMAIN, SERVICE_CAPTURE_SMARTCAM, capture_smartcam, schema=DEVICE_SERIAL_SCHEMA @@ -114,9 +109,9 @@ def setup(hass, config): device_id = service.data[ATTR_DEVICE_SERIAL] try: await hass.async_add_executor_job(HUB.disable_autolock, device_id) - _LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL) + LOGGER.debug("Disabling autolock on%s", ATTR_DEVICE_SERIAL) except verisure.Error as ex: - _LOGGER.error("Could not disable autolock, %s", ex) + LOGGER.error("Could not disable autolock, %s", ex) hass.services.register( DOMAIN, SERVICE_DISABLE_AUTOLOCK, disable_autolock, schema=DEVICE_SERIAL_SCHEMA @@ -127,9 +122,9 @@ def setup(hass, config): device_id = service.data[ATTR_DEVICE_SERIAL] try: await hass.async_add_executor_job(HUB.enable_autolock, device_id) - _LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL) + LOGGER.debug("Enabling autolock on %s", ATTR_DEVICE_SERIAL) except verisure.Error as ex: - _LOGGER.error("Could not enable autolock, %s", ex) + LOGGER.error("Could not enable autolock, %s", ex) hass.services.register( DOMAIN, SERVICE_ENABLE_AUTOLOCK, enable_autolock, schema=DEVICE_SERIAL_SCHEMA @@ -147,8 +142,6 @@ class VerisureHub: self.config = domain_config - self._lock = threading.Lock() - self.session = verisure.Session( domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD] ) @@ -160,7 +153,7 @@ class VerisureHub: try: self.session.login() except verisure.Error as ex: - _LOGGER.error("Could not log in to verisure, %s", ex) + LOGGER.error("Could not log in to verisure, %s", ex) return False if self.giid: return self.set_giid() @@ -171,7 +164,7 @@ class VerisureHub: try: self.session.logout() except verisure.Error as ex: - _LOGGER.error("Could not log out from verisure, %s", ex) + LOGGER.error("Could not log out from verisure, %s", ex) return False return True @@ -180,7 +173,7 @@ class VerisureHub: try: self.session.set_giid(self.giid) except verisure.Error as ex: - _LOGGER.error("Could not set installation GIID, %s", ex) + LOGGER.error("Could not set installation GIID, %s", ex) return False return True @@ -189,9 +182,9 @@ class VerisureHub: try: self.overview = self.session.get_overview() except verisure.ResponseError as ex: - _LOGGER.error("Could not read overview, %s", ex) + LOGGER.error("Could not read overview, %s", ex) if ex.status_code == HTTP_SERVICE_UNAVAILABLE: # Service unavailable - _LOGGER.info("Trying to log in again") + LOGGER.info("Trying to log in again") self.login() else: raise @@ -217,7 +210,7 @@ class VerisureHub: def get(self, jpath, *args): """Get values from the overview that matches the jsonpath.""" res = jsonpath(self.overview, jpath % args) - return res if res else [] + return res or [] def get_first(self, jpath, *args): """Get first value from the overview that matches the jsonpath.""" @@ -227,4 +220,4 @@ class VerisureHub: def get_image_info(self, jpath, *args): """Get values from the imageseries that matches the jsonpath.""" res = jsonpath(self.imageseries, jpath % args) - return res if res else [] + return res or [] diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index 239396b2d0..fff58433a9 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -1,5 +1,4 @@ """Support for Verisure alarm control panels.""" -import logging from time import sleep import homeassistant.components.alarm_control_panel as alarm @@ -13,9 +12,8 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) -from . import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, HUB as hub - -_LOGGER = logging.getLogger(__name__) +from . import HUB as hub +from .const import CONF_ALARM, CONF_CODE_DIGITS, CONF_GIID, LOGGER def setup_platform(hass, config, add_entities, discovery_info=None): @@ -32,12 +30,12 @@ def set_arm_state(state, code=None): transaction_id = hub.session.set_arm_state(code, state)[ "armStateChangeTransactionId" ] - _LOGGER.info("verisure set arm state %s", state) + LOGGER.info("verisure set arm state %s", state) transaction = {} while "result" not in transaction: sleep(0.5) transaction = hub.session.get_arm_state_transaction(transaction_id) - hub.update_overview(no_throttle=True) + hub.update_overview() class VerisureAlarm(alarm.AlarmControlPanelEntity): @@ -58,7 +56,7 @@ class VerisureAlarm(alarm.AlarmControlPanelEntity): if giid in aliass: return "{} alarm".format(aliass[giid]) - _LOGGER.error("Verisure installation giid not found: %s", giid) + LOGGER.error("Verisure installation giid not found: %s", giid) return "{} alarm".format(hub.session.installations[0]["alias"]) @@ -93,7 +91,7 @@ class VerisureAlarm(alarm.AlarmControlPanelEntity): elif status == "ARMED_AWAY": self._state = STATE_ALARM_ARMED_AWAY elif status != "PENDING": - _LOGGER.error("Unknown alarm state %s", status) + LOGGER.error("Unknown alarm state %s", status) self._changed_by = hub.get_first("$.armState.name") def alarm_disarm(self, code=None): diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index 73b27ee7d2..a69e1fb95d 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -1,14 +1,12 @@ """Support for Verisure cameras.""" import errno -import logging import os from homeassistant.components.camera import Camera from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from . import CONF_SMARTCAM, HUB as hub - -_LOGGER = logging.getLogger(__name__) +from . import HUB as hub +from .const import CONF_SMARTCAM, LOGGER def setup_platform(hass, config, add_entities, discovery_info=None): @@ -17,16 +15,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return False directory_path = hass.config.config_dir if not os.access(directory_path, os.R_OK): - _LOGGER.error("file path %s is not readable", directory_path) + LOGGER.error("file path %s is not readable", directory_path) return False hub.update_overview() - smartcams = [] - smartcams.extend( - [ - VerisureSmartcam(hass, device_label, directory_path) - for device_label in hub.get("$.customerImageCameras[*].deviceLabel") - ] - ) + smartcams = [ + VerisureSmartcam(hass, device_label, directory_path) + for device_label in hub.get("$.customerImageCameras[*].deviceLabel") + ] + add_entities(smartcams) @@ -47,9 +43,9 @@ class VerisureSmartcam(Camera): """Return image response.""" self.check_imagelist() if not self._image: - _LOGGER.debug("No image to display") + LOGGER.debug("No image to display") return - _LOGGER.debug("Trying to open %s", self._image) + LOGGER.debug("Trying to open %s", self._image) with open(self._image, "rb") as file: return file.read() @@ -63,14 +59,14 @@ class VerisureSmartcam(Camera): return new_image_id = image_ids[0] if new_image_id in ("-1", self._image_id): - _LOGGER.debug("The image is the same, or loading image_id") + LOGGER.debug("The image is the same, or loading image_id") return - _LOGGER.debug("Download new image %s", new_image_id) + LOGGER.debug("Download new image %s", new_image_id) new_image_path = os.path.join( self._directory_path, "{}{}".format(new_image_id, ".jpg") ) hub.session.download_image(self._device_label, new_image_id, new_image_path) - _LOGGER.debug("Old image_id=%s", self._image_id) + LOGGER.debug("Old image_id=%s", self._image_id) self.delete_image(self) self._image_id = new_image_id @@ -83,7 +79,7 @@ class VerisureSmartcam(Camera): ) try: os.remove(remove_image) - _LOGGER.debug("Deleting old image %s", remove_image) + LOGGER.debug("Deleting old image %s", remove_image) except OSError as error: if error.errno != errno.ENOENT: raise diff --git a/homeassistant/components/verisure/const.py b/homeassistant/components/verisure/const.py new file mode 100644 index 0000000000..89dcfa396a --- /dev/null +++ b/homeassistant/components/verisure/const.py @@ -0,0 +1,28 @@ +"""Constants for the Verisure integration.""" +from datetime import timedelta +import logging + +DOMAIN = "verisure" + +LOGGER = logging.getLogger(__package__) + +ATTR_DEVICE_SERIAL = "device_serial" + +CONF_ALARM = "alarm" +CONF_CODE_DIGITS = "code_digits" +CONF_DOOR_WINDOW = "door_window" +CONF_GIID = "giid" +CONF_HYDROMETERS = "hygrometers" +CONF_LOCKS = "locks" +CONF_DEFAULT_LOCK_CODE = "default_lock_code" +CONF_MOUSE = "mouse" +CONF_SMARTPLUGS = "smartplugs" +CONF_THERMOMETERS = "thermometers" +CONF_SMARTCAM = "smartcam" + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) +MIN_SCAN_INTERVAL = timedelta(minutes=1) + +SERVICE_CAPTURE_SMARTCAM = "capture_smartcam" +SERVICE_DISABLE_AUTOLOCK = "disable_autolock" +SERVICE_ENABLE_AUTOLOCK = "enable_autolock" diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index 28efb64c71..228c8c6c17 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -1,13 +1,11 @@ """Support for Verisure locks.""" -import logging from time import monotonic, sleep from homeassistant.components.lock import LockEntity from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED -from . import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, HUB as hub - -_LOGGER = logging.getLogger(__name__) +from . import HUB as hub +from .const import CONF_CODE_DIGITS, CONF_DEFAULT_LOCK_CODE, CONF_LOCKS, LOGGER def setup_platform(hass, config, add_entities, discovery_info=None): @@ -83,7 +81,7 @@ class VerisureDoorlock(LockEntity): elif status == "LOCKED": self._state = STATE_LOCKED elif status != "PENDING": - _LOGGER.error("Unknown lock state %s", status) + LOGGER.error("Unknown lock state %s", status) self._changed_by = hub.get_first( "$.doorLockStatusList[?(@.deviceLabel=='%s')].userString", self._device_label, @@ -101,7 +99,7 @@ class VerisureDoorlock(LockEntity): code = kwargs.get(ATTR_CODE, self._default_lock_code) if code is None: - _LOGGER.error("Code required but none provided") + LOGGER.error("Code required but none provided") return self.set_lock_state(code, STATE_UNLOCKED) @@ -113,7 +111,7 @@ class VerisureDoorlock(LockEntity): code = kwargs.get(ATTR_CODE, self._default_lock_code) if code is None: - _LOGGER.error("Code required but none provided") + LOGGER.error("Code required but none provided") return self.set_lock_state(code, STATE_LOCKED) @@ -124,7 +122,7 @@ class VerisureDoorlock(LockEntity): transaction_id = hub.session.set_lock_state( code, self._device_label, lock_state )["doorLockStateChangeTransactionId"] - _LOGGER.debug("Verisure doorlock %s", state) + LOGGER.debug("Verisure doorlock %s", state) transaction = {} attempts = 0 while "result" not in transaction: diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 13c2936497..6260f4a9ff 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -3,5 +3,5 @@ "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", "requirements": ["jsonpath==0.82", "vsure==1.5.4"], - "codeowners": [] + "codeowners": ["@frenck"] } diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 437e45ba72..ac7c8f40e8 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -2,7 +2,8 @@ from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS, HUB as hub +from . import HUB as hub +from .const import CONF_HYDROMETERS, CONF_MOUSE, CONF_THERMOMETERS def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/vesync/translations/pt.json b/homeassistant/components/vesync/translations/pt.json index 5cf1a0dcd0..fb4e459281 100644 --- a/homeassistant/components/vesync/translations/pt.json +++ b/homeassistant/components/vesync/translations/pt.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vesync/translations/zh-Hant.json b/homeassistant/components/vesync/translations/zh-Hant.json index 02cffeefc4..264ad237af 100644 --- a/homeassistant/components/vesync/translations/zh-Hant.json +++ b/homeassistant/components/vesync/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" diff --git a/homeassistant/components/vilfo/translations/pt.json b/homeassistant/components/vilfo/translations/pt.json index ce7cbc3f54..9f7a591855 100644 --- a/homeassistant/components/vilfo/translations/pt.json +++ b/homeassistant/components/vilfo/translations/pt.json @@ -1,8 +1,17 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, "step": { "user": { "data": { + "access_token": "Token de Acesso", "host": "Servidor" } } diff --git a/homeassistant/components/vilfo/translations/zh-Hant.json b/homeassistant/components/vilfo/translations/zh-Hant.json index abbc12e6d8..b266e25b39 100644 --- a/homeassistant/components/vilfo/translations/zh-Hant.json +++ b/homeassistant/components/vilfo/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 61c9ca5485..4c06c89692 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -184,10 +184,10 @@ class VizioDevice(MediaPlayerEntity): async def async_update(self) -> None: """Retrieve latest state of the device.""" if not self._model: - self._model = await self._device.get_model_name() + self._model = await self._device.get_model_name(log_api_exception=False) if not self._sw_version: - self._sw_version = await self._device.get_version() + self._sw_version = await self._device.get_version(log_api_exception=False) is_on = await self._device.get_power_state(log_api_exception=False) @@ -236,7 +236,9 @@ class VizioDevice(MediaPlayerEntity): if not self._available_sound_modes: self._available_sound_modes = ( await self._device.get_setting_options( - VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE + VIZIO_AUDIO_SETTINGS, + VIZIO_SOUND_MODE, + log_api_exception=False, ) ) else: @@ -306,6 +308,7 @@ class VizioDevice(MediaPlayerEntity): setting_type, setting_name, new_value, + log_api_exception=False, ) async def async_added_to_hass(self) -> None: @@ -453,52 +456,58 @@ class VizioDevice(MediaPlayerEntity): """Select sound mode.""" if sound_mode in self._available_sound_modes: await self._device.set_setting( - VIZIO_AUDIO_SETTINGS, VIZIO_SOUND_MODE, sound_mode + VIZIO_AUDIO_SETTINGS, + VIZIO_SOUND_MODE, + sound_mode, + log_api_exception=False, ) async def async_turn_on(self) -> None: """Turn the device on.""" - await self._device.pow_on() + await self._device.pow_on(log_api_exception=False) async def async_turn_off(self) -> None: """Turn the device off.""" - await self._device.pow_off() + await self._device.pow_off(log_api_exception=False) async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" if mute: - await self._device.mute_on() + await self._device.mute_on(log_api_exception=False) self._is_volume_muted = True else: - await self._device.mute_off() + await self._device.mute_off(log_api_exception=False) self._is_volume_muted = False async def async_media_previous_track(self) -> None: """Send previous channel command.""" - await self._device.ch_down() + await self._device.ch_down(log_api_exception=False) async def async_media_next_track(self) -> None: """Send next channel command.""" - await self._device.ch_up() + await self._device.ch_up(log_api_exception=False) async def async_select_source(self, source: str) -> None: """Select input source.""" if source in self._available_inputs: - await self._device.set_input(source) + await self._device.set_input(source, log_api_exception=False) elif source in self._get_additional_app_names(): await self._device.launch_app_config( **next( app["config"] for app in self._additional_app_configs if app["name"] == source - ) + ), + log_api_exception=False, ) elif source in self._available_apps: - await self._device.launch_app(source, self._all_apps) + await self._device.launch_app( + source, self._all_apps, log_api_exception=False + ) async def async_volume_up(self) -> None: """Increase volume of the device.""" - await self._device.vol_up(num=self._volume_step) + await self._device.vol_up(num=self._volume_step, log_api_exception=False) if self._volume_level is not None: self._volume_level = min( @@ -507,7 +516,7 @@ class VizioDevice(MediaPlayerEntity): async def async_volume_down(self) -> None: """Decrease volume of the device.""" - await self._device.vol_down(num=self._volume_step) + await self._device.vol_down(num=self._volume_step, log_api_exception=False) if self._volume_level is not None: self._volume_level = max( @@ -519,10 +528,10 @@ class VizioDevice(MediaPlayerEntity): if self._volume_level is not None: if volume > self._volume_level: num = int(self._max_volume * (volume - self._volume_level)) - await self._device.vol_up(num=num) + await self._device.vol_up(num=num, log_api_exception=False) self._volume_level = volume elif volume < self._volume_level: num = int(self._max_volume * (self._volume_level - volume)) - await self._device.vol_down(num=num) + await self._device.vol_down(num=num, log_api_exception=False) self._volume_level = volume diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index f2b24b2c55..ddb68ec09f 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "cannot_connect": "Verbindungsfehler", "updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." }, "error": { diff --git a/homeassistant/components/vizio/translations/no.json b/homeassistant/components/vizio/translations/no.json index c5e0b6386b..de00cf0fce 100644 --- a/homeassistant/components/vizio/translations/no.json +++ b/homeassistant/components/vizio/translations/no.json @@ -13,7 +13,7 @@ "step": { "pair_tv": { "data": { - "pin": "PIN-kode" + "pin": "PIN kode" }, "description": "TVen skal vise en kode. Fyll inn denne koden i skjemaet, og fortsett deretter til neste trinn for \u00e5 fullf\u00f8re paringen.", "title": "Fullf\u00f8r sammenkoblingsprosess" diff --git a/homeassistant/components/vizio/translations/pt.json b/homeassistant/components/vizio/translations/pt.json index b1a4f0d7b3..b8259aca07 100644 --- a/homeassistant/components/vizio/translations/pt.json +++ b/homeassistant/components/vizio/translations/pt.json @@ -1,17 +1,40 @@ { "config": { + "abort": { + "already_configured_device": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "pair_tv": { "data": { "pin": "PIN" } }, + "pairing_complete": { + "description": "O seu Dispositivo VIZIO SmartCast j\u00e1 se encontra ligado ao Home Assistant.", + "title": "Emparelhamento Completo" + }, + "pairing_complete_import": { + "title": "Emparelhamento Completo" + }, "user": { "data": { "access_token": "Token de Acesso", + "device_class": "Tipo de dispositivo", "host": "Servidor", "name": "Nome" - } + }, + "title": "Dispositivo VIZIO SmartCast" + } + } + }, + "options": { + "step": { + "init": { + "title": "Atualizar op\u00e7\u00f5es de Dispositivo VIZIO SmartCast" } } } diff --git a/homeassistant/components/vizio/translations/zh-Hant.json b/homeassistant/components/vizio/translations/zh-Hant.json index 74d6a858d8..257ed829b6 100644 --- a/homeassistant/components/vizio/translations/zh-Hant.json +++ b/homeassistant/components/vizio/translations/zh-Hant.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "already_configured_device": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured_device": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557", "updated_entry": "\u6b64\u5be6\u9ad4\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u5be6\u9ad4\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "complete_pairing_failed": "\u7121\u6cd5\u5b8c\u6210\u914d\u5c0d\uff0c\u50b3\u9001\u524d\u3001\u8acb\u78ba\u5b9a\u6240\u8f38\u5165\u7684 PIN \u78bc\u3001\u540c\u6642\u96fb\u8996\u5df2\u7d93\u958b\u555f\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002", - "existing_config_entry_found": "\u5df2\u6709\u4e00\u7d44\u4f7f\u7528\u76f8\u540c\u5e8f\u865f\u7684 VIZIO SmartCast \u8a2d\u5099 \u5df2\u8a2d\u5b9a\u3002\u5fc5\u9808\u5148\u9032\u884c\u522a\u9664\u5f8c\u624d\u80fd\u91cd\u65b0\u8a2d\u5b9a\u3002" + "existing_config_entry_found": "\u5df2\u6709\u4e00\u7d44\u4f7f\u7528\u76f8\u540c\u5e8f\u865f\u7684 VIZIO SmartCast \u88dd\u7f6e \u5df2\u8a2d\u5b9a\u3002\u5fc5\u9808\u5148\u9032\u884c\u522a\u9664\u5f8c\u624d\u80fd\u91cd\u65b0\u8a2d\u5b9a\u3002" }, "step": { "pair_tv": { @@ -19,22 +19,22 @@ "title": "\u5b8c\u6210\u914d\u5c0d\u904e\u7a0b" }, "pairing_complete": { - "description": "VIZIO SmartCast \u8a2d\u5099 \u5df2\u7d93\u9023\u7dda\u81f3 Home Assistant\u3002", + "description": "VIZIO SmartCast \u88dd\u7f6e \u5df2\u7d93\u9023\u7dda\u81f3 Home Assistant\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, "pairing_complete_import": { - "description": "VIZIO SmartCast \u8a2d\u5099 \u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba '**{access_token}**'\u3002", + "description": "VIZIO SmartCast \u88dd\u7f6e \u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba '**{access_token}**'\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, "user": { "data": { "access_token": "\u5b58\u53d6\u5bc6\u9470", - "device_class": "\u8a2d\u5099\u985e\u5225", + "device_class": "\u88dd\u7f6e\u985e\u5225", "host": "\u4e3b\u6a5f\u7aef", "name": "\u540d\u7a31" }, "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u5bc6\u9470\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5b58\u53d6\u5bc6\u9470 \uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002", - "title": "VIZIO SmartCast \u8a2d\u5099" + "title": "VIZIO SmartCast \u88dd\u7f6e" } } }, @@ -47,7 +47,7 @@ "volume_step": "\u97f3\u91cf\u5927\u5c0f" }, "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u904e\u6ffe\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002", - "title": "\u66f4\u65b0 VIZIO SmartCast \u8a2d\u5099 \u9078\u9805" + "title": "\u66f4\u65b0 VIZIO SmartCast \u88dd\u7f6e \u9078\u9805" } } } diff --git a/homeassistant/components/volumio/translations/de.json b/homeassistant/components/volumio/translations/de.json new file mode 100644 index 0000000000..ef455299de --- /dev/null +++ b/homeassistant/components/volumio/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindungsfehler" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/zh-Hant.json b/homeassistant/components/volumio/translations/zh-Hant.json index 48f3ad6d17..f557397372 100644 --- a/homeassistant/components/volumio/translations/zh-Hant.json +++ b/homeassistant/components/volumio/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u5df2\u63a2\u7d22\u5230\u7684 Volumio" }, "error": { diff --git a/homeassistant/components/water_heater/translations/pt.json b/homeassistant/components/water_heater/translations/pt.json new file mode 100644 index 0000000000..2278e7701a --- /dev/null +++ b/homeassistant/components/water_heater/translations/pt.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/translations/tr.json b/homeassistant/components/water_heater/translations/tr.json new file mode 100644 index 0000000000..3010c9e622 --- /dev/null +++ b/homeassistant/components/water_heater/translations/tr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kapat", + "turn_on": "{entity_name} a\u00e7\u0131n" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index c656926ecf..75ca322b9a 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -7,24 +7,32 @@ import requests import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_call_later from .const import DOMAIN -# Mapping from Wemo model_name to component. +# Mapping from Wemo model_name to domain. WEMO_MODEL_DISPATCH = { - "Bridge": "light", - "CoffeeMaker": "switch", - "Dimmer": "light", - "Humidifier": "fan", - "Insight": "switch", - "LightSwitch": "switch", - "Maker": "switch", - "Motion": "binary_sensor", - "Sensor": "binary_sensor", - "Socket": "switch", + "Bridge": LIGHT_DOMAIN, + "CoffeeMaker": SWITCH_DOMAIN, + "Dimmer": LIGHT_DOMAIN, + "Humidifier": FAN_DOMAIN, + "Insight": SWITCH_DOMAIN, + "LightSwitch": SWITCH_DOMAIN, + "Maker": SWITCH_DOMAIN, + "Motion": BINARY_SENSOR_DOMAIN, + "OutdoorPlug": SWITCH_DOMAIN, + "Sensor": BINARY_SENSOR_DOMAIN, + "Socket": SWITCH_DOMAIN, } _LOGGER = logging.getLogger(__name__) @@ -86,7 +94,7 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up a wemo config entry.""" config = hass.data[DOMAIN].pop("config") @@ -94,14 +102,16 @@ async def async_setup_entry(hass, entry): registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() await hass.async_add_executor_job(registry.start) - def stop_wemo(event): + wemo_dispatcher = WemoDispatcher(entry) + wemo_discovery = WemoDiscovery(hass, wemo_dispatcher) + + async def async_stop_wemo(event): """Shutdown Wemo subscriptions and subscription thread on exit.""" _LOGGER.debug("Shutting down WeMo event subscriptions") - registry.stop() + await hass.async_add_executor_job(registry.stop) + wemo_discovery.async_stop_discovery() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo) - - devices = {} + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo) static_conf = config.get(CONF_STATIC, []) if static_conf: @@ -112,41 +122,46 @@ async def async_setup_entry(hass, entry): for host, port in static_conf ] ): - if device is None: - continue - - devices.setdefault(device.serialnumber, device) + if device: + wemo_dispatcher.async_add_unique_device(hass, device) if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): - _LOGGER.debug("Scanning network for WeMo devices...") - for device in await hass.async_add_executor_job(pywemo.discover_devices): - devices.setdefault( - device.serialnumber, - device, - ) + await wemo_discovery.async_discover_and_schedule() - loaded_components = set() + return True - for device in devices.values(): - _LOGGER.debug( - "Adding WeMo device at %s:%i (%s)", - device.host, - device.port, - device.serialnumber, - ) - component = WEMO_MODEL_DISPATCH.get(device.model_name, "switch") +class WemoDispatcher: + """Dispatch WeMo devices to the correct platform.""" + + def __init__(self, config_entry: ConfigEntry): + """Initialize the WemoDispatcher.""" + self._config_entry = config_entry + self._added_serial_numbers = set() + self._loaded_components = set() + + @callback + def async_add_unique_device( + self, hass: HomeAssistant, device: pywemo.WeMoDevice + ) -> None: + """Add a WeMo device to hass if it has not already been added.""" + if device.serialnumber in self._added_serial_numbers: + return + + component = WEMO_MODEL_DISPATCH.get(device.model_name, SWITCH_DOMAIN) # Three cases: # - First time we see component, we need to load it and initialize the backlog # - Component is being loaded, add to backlog # - Component is loaded, backlog is gone, dispatch discovery - if component not in loaded_components: + if component not in self._loaded_components: hass.data[DOMAIN]["pending"][component] = [device] - loaded_components.add(component) + self._loaded_components.add(component) hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + hass.config_entries.async_forward_entry_setup( + self._config_entry, component + ) ) elif component in hass.data[DOMAIN]["pending"]: @@ -159,7 +174,48 @@ async def async_setup_entry(hass, entry): device, ) - return True + self._added_serial_numbers.add(device.serialnumber) + + +class WemoDiscovery: + """Use SSDP to discover WeMo devices.""" + + ADDITIONAL_SECONDS_BETWEEN_SCANS = 10 + MAX_SECONDS_BETWEEN_SCANS = 300 + + def __init__(self, hass: HomeAssistant, wemo_dispatcher: WemoDispatcher) -> None: + """Initialize the WemoDiscovery.""" + self._hass = hass + self._wemo_dispatcher = wemo_dispatcher + self._stop = None + self._scan_delay = 0 + + async def async_discover_and_schedule(self, *_) -> None: + """Periodically scan the network looking for WeMo devices.""" + _LOGGER.debug("Scanning network for WeMo devices...") + try: + for device in await self._hass.async_add_executor_job( + pywemo.discover_devices + ): + self._wemo_dispatcher.async_add_unique_device(self._hass, device) + finally: + # Run discovery more frequently after hass has just started. + self._scan_delay = min( + self._scan_delay + self.ADDITIONAL_SECONDS_BETWEEN_SCANS, + self.MAX_SECONDS_BETWEEN_SCANS, + ) + self._stop = async_call_later( + self._hass, + self._scan_delay, + self.async_discover_and_schedule, + ) + + @callback + def async_stop_discovery(self) -> None: + """Stop the periodic background scanning.""" + if self._stop: + self._stop() + self._stop = None def validate_static_config(host, port): diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index b5ef3dc528..b6690ed6d2 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -2,13 +2,13 @@ import asyncio import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DOMAIN as WEMO_DOMAIN +from .entity import WemoSubscriptionEntity _LOGGER = logging.getLogger(__name__) @@ -30,67 +30,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoBinarySensor(BinarySensorEntity): +class WemoBinarySensor(WemoSubscriptionEntity, BinarySensorEntity): """Representation a WeMo binary sensor.""" - def __init__(self, device): - """Initialize the WeMo sensor.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serial_number = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo sensor.""" - _LOGGER.debug("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Wemo sensor added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo sensor is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - def _update(self, force_update=True): """Update the sensor state.""" try: @@ -103,33 +45,3 @@ class WemoBinarySensor(BinarySensorEntity): _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False self.wemo.reconnect_with_device() - - @property - def unique_id(self): - """Return the id of this WeMo sensor.""" - return self._serial_number - - @property - def name(self): - """Return the name of the service if any.""" - return self._name - - @property - def is_on(self): - """Return true if sensor is on.""" - return self._state - - @property - def available(self): - """Return true if sensor is available.""" - return self._available - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serial_number)}, - "model": self._model_name, - "manufacturer": "Belkin", - } diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py new file mode 100644 index 0000000000..e7c0712272 --- /dev/null +++ b/homeassistant/components/wemo/entity.py @@ -0,0 +1,124 @@ +"""Classes shared among Wemo entities.""" +import asyncio +import logging +from typing import Any, Dict, Optional + +import async_timeout +from pywemo import WeMoDevice + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN as WEMO_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class WemoEntity(Entity): + """Common methods for Wemo entities. + + Requires that subclasses implement the _update method. + """ + + def __init__(self, device: WeMoDevice) -> None: + """Initialize the WeMo device.""" + self.wemo = device + self._state = None + self._available = True + self._update_lock = None + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return self.wemo.name + + @property + def available(self) -> bool: + """Return true if switch is available.""" + return self._available + + def _update(self, force_update: Optional[bool] = True): + """Update the device state.""" + raise NotImplementedError() + + async def async_added_to_hass(self) -> None: + """Wemo device added to Home Assistant.""" + # Define inside async context so we know our event loop + self._update_lock = asyncio.Lock() + + async def async_update(self) -> None: + """Update WeMo state. + + Wemo has an aggressive retry logic that sometimes can take over a + minute to return. If we don't get a state after 5 seconds, assume the + Wemo switch is unreachable. If update goes through, it will be made + available again. + """ + # If an update is in progress, we don't do anything + if self._update_lock.locked(): + return + + try: + with async_timeout.timeout(5): + await asyncio.shield(self._async_locked_update(True)) + except asyncio.TimeoutError: + _LOGGER.warning("Lost connection to %s", self.name) + self._available = False + + async def _async_locked_update(self, force_update: bool) -> None: + """Try updating within an async lock.""" + async with self._update_lock: + await self.hass.async_add_executor_job(self._update, force_update) + + +class WemoSubscriptionEntity(WemoEntity): + """Common methods for Wemo devices that register for update callbacks.""" + + @property + def unique_id(self) -> str: + """Return the id of this WeMo device.""" + return self.wemo.serialnumber + + @property + def device_info(self) -> Dict[str, Any]: + """Return the device info.""" + return { + "name": self.name, + "identifiers": {(WEMO_DOMAIN, self.unique_id)}, + "model": self.wemo.model_name, + "manufacturer": "Belkin", + } + + @property + def is_on(self) -> bool: + """Return true if the state is on. Standby is on.""" + return self._state + + async def async_added_to_hass(self) -> None: + """Wemo device added to Home Assistant.""" + await super().async_added_to_hass() + + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.register, self.wemo) + registry.on(self.wemo, None, self._subscription_callback) + + async def async_will_remove_from_hass(self) -> None: + """Wemo device removed from hass.""" + registry = self.hass.data[WEMO_DOMAIN]["registry"] + await self.hass.async_add_executor_job(registry.unregister, self.wemo) + + def _subscription_callback( + self, _device: WeMoDevice, _type: str, _params: str + ) -> None: + """Update the state by the Wemo device.""" + _LOGGER.info("Subscription update for %s", self.name) + updated = self.wemo.subscription_update(_type, _params) + self.hass.add_job(self._async_locked_subscription_callback(not updated)) + + async def _async_locked_subscription_callback(self, force_update: bool) -> None: + """Handle an update from a subscription.""" + # If an update is in progress, we don't do anything + if self._update_lock.locked(): + return + + await self._async_locked_update(force_update) + self.async_write_ha_state() diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 1bc477277c..0dca71a0d8 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException import voluptuous as vol @@ -15,8 +14,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import ATTR_ENTITY_ID -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( @@ -24,6 +22,7 @@ from .const import ( SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY, ) +from .entity import WemoSubscriptionEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -81,27 +80,19 @@ HASS_FAN_SPEED_TO_WEMO = { if k not in [WEMO_FAN_LOW, WEMO_FAN_HIGH] } -SET_HUMIDITY_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_TARGET_HUMIDITY): vol.All( - vol.Coerce(float), vol.Range(min=0, max=100) - ), - } -) - -RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_ids}) +SET_HUMIDITY_SCHEMA = { + vol.Required(ATTR_TARGET_HUMIDITY): vol.All( + vol.Coerce(float), vol.Range(min=0, max=100) + ), +} async def async_setup_entry(hass, config_entry, async_add_entities): """Set up WeMo binary sensors.""" - entities = [] async def _discovered_wemo(device): """Handle a discovered Wemo device.""" - entity = WemoHumidifier(device) - entities.append(entity) - async_add_entities([entity]) + async_add_entities([WemoHumidifier(device)]) async_dispatcher_connect(hass, f"{WEMO_DOMAIN}.fan", _discovered_wemo) @@ -112,46 +103,25 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ] ) - def service_handle(service): - """Handle the WeMo humidifier services.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) + platform = entity_platform.current_platform.get() - humidifiers = [entity for entity in entities if entity.entity_id in entity_ids] - - if service.service == SERVICE_SET_HUMIDITY: - target_humidity = service.data.get(ATTR_TARGET_HUMIDITY) - - for humidifier in humidifiers: - humidifier.set_humidity(target_humidity) - elif service.service == SERVICE_RESET_FILTER_LIFE: - for humidifier in humidifiers: - humidifier.reset_filter_life() - - # Register service(s) - hass.services.async_register( - WEMO_DOMAIN, - SERVICE_SET_HUMIDITY, - service_handle, - schema=SET_HUMIDITY_SCHEMA, + # This will call WemoHumidifier.set_humidity(target_humidity=VALUE) + platform.async_register_entity_service( + SERVICE_SET_HUMIDITY, SET_HUMIDITY_SCHEMA, WemoHumidifier.set_humidity.__name__ ) - hass.services.async_register( - WEMO_DOMAIN, - SERVICE_RESET_FILTER_LIFE, - service_handle, - schema=RESET_FILTER_LIFE_SCHEMA, + # This will call WemoHumidifier.reset_filter_life() + platform.async_register_entity_service( + SERVICE_RESET_FILTER_LIFE, {}, WemoHumidifier.reset_filter_life.__name__ ) -class WemoHumidifier(FanEntity): +class WemoHumidifier(WemoSubscriptionEntity, FanEntity): """Representation of a WeMo humidifier.""" def __init__(self, device): """Initialize the WeMo switch.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None + super().__init__(device) self._fan_mode = None self._target_humidity = None self._current_humidity = None @@ -159,54 +129,6 @@ class WemoHumidifier(FanEntity): self._filter_life = None self._filter_expired = None self._last_fan_on_mode = WEMO_FAN_MEDIUM - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - @property - def unique_id(self): - """Return the ID of this WeMo humidifier.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the humidifier if any.""" - return self._name - - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self._state - - @property - def available(self): - """Return true if switch is available.""" - return self._available - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def icon(self): @@ -240,39 +162,6 @@ class WemoHumidifier(FanEntity): """Flag supported features.""" return SUPPORTED_FEATURES - async def async_added_to_hass(self): - """Wemo humidifier added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo humidifier is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - def _update(self, force_update=True): """Update the device state.""" try: @@ -331,21 +220,21 @@ class WemoHumidifier(FanEntity): self.schedule_update_ha_state() - def set_humidity(self, humidity: float) -> None: + def set_humidity(self, target_humidity: float) -> None: """Set the target humidity level for the Humidifier.""" - if humidity < 50: - target_humidity = WEMO_HUMIDITY_45 - elif 50 <= humidity < 55: - target_humidity = WEMO_HUMIDITY_50 - elif 55 <= humidity < 60: - target_humidity = WEMO_HUMIDITY_55 - elif 60 <= humidity < 100: - target_humidity = WEMO_HUMIDITY_60 - elif humidity >= 100: - target_humidity = WEMO_HUMIDITY_100 + if target_humidity < 50: + pywemo_humidity = WEMO_HUMIDITY_45 + elif 50 <= target_humidity < 55: + pywemo_humidity = WEMO_HUMIDITY_50 + elif 55 <= target_humidity < 60: + pywemo_humidity = WEMO_HUMIDITY_55 + elif 60 <= target_humidity < 100: + pywemo_humidity = WEMO_HUMIDITY_60 + elif target_humidity >= 100: + pywemo_humidity = WEMO_HUMIDITY_100 try: - self.wemo.set_humidity(target_humidity) + self.wemo.set_humidity(pywemo_humidity) except ActionException as err: _LOGGER.warning( "Error while setting humidity of device: %s (%s)", self.name, err diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index 6aac2be6dd..1362c7d483 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant import util @@ -22,6 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util from .const import DOMAIN as WEMO_DOMAIN +from .entity import WemoEntity, WemoSubscriptionEntity MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(milliseconds=100) @@ -81,21 +81,17 @@ def setup_bridge(hass, bridge, async_add_entities): update_lights() -class WemoLight(LightEntity): +class WemoLight(WemoEntity, LightEntity): """Representation of a WeMo light.""" def __init__(self, device, update_lights): """Initialize the WeMo light.""" - self.wemo = device - self._state = None + super().__init__(device) self._update_lights = update_lights - self._available = True - self._update_lock = None self._brightness = None self._hs_color = None self._color_temp = None self._is_on = None - self._name = self.wemo.name self._unique_id = self.wemo.uniqueID self._model_name = type(self.wemo).__name__ @@ -107,18 +103,13 @@ class WemoLight(LightEntity): @property def unique_id(self): """Return the ID of this light.""" - return self._unique_id - - @property - def name(self): - """Return the name of the light.""" - return self._name + return self.wemo.uniqueID @property def device_info(self): """Return the device info.""" return { - "name": self._name, + "name": self.name, "identifiers": {(WEMO_DOMAIN, self._unique_id)}, "model": self._model_name, "manufacturer": "Belkin", @@ -149,11 +140,6 @@ class WemoLight(LightEntity): """Flag supported features.""" return SUPPORT_WEMO - @property - def available(self): - """Return if light is available.""" - return self._available - def turn_on(self, **kwargs): """Turn the light on.""" xy_color = None @@ -208,7 +194,7 @@ class WemoLight(LightEntity): except (AttributeError, ActionException) as err: _LOGGER.warning("Could not update status for %s (%s)", self.name, err) self._available = False - self.wemo.reconnect_with_device() + self.wemo.bridge.reconnect_with_device() else: self._is_on = self._state.get("onoff") != WEMO_OFF self._brightness = self._state.get("level", 255) @@ -222,106 +208,14 @@ class WemoLight(LightEntity): else: self._hs_color = None - async def async_update(self): - """Synchronize state with bridge.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - -class WemoDimmer(LightEntity): +class WemoDimmer(WemoSubscriptionEntity, LightEntity): """Representation of a WeMo dimmer.""" def __init__(self, device): """Initialize the WeMo dimmer.""" - self.wemo = device - self._state = None - self._available = True - self._update_lock = None + super().__init__(device) self._brightness = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.debug("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Wemo dimmer added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo dimmer is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - @property - def unique_id(self): - """Return the ID of this WeMo dimmer.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the dimmer if any.""" - return self._name - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def supported_features(self): @@ -333,11 +227,6 @@ class WemoDimmer(LightEntity): """Return the brightness of this light between 1 and 100.""" return self._brightness - @property - def is_on(self): - """Return true if dimmer is on. Standby is on.""" - return self._state - def _update(self, force_update=True): """Update the device state.""" try: @@ -385,8 +274,3 @@ class WemoDimmer(LightEntity): self._available = False self.schedule_update_ha_state() - - @property - def available(self): - """Return if dimmer is available.""" - return self._available diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index e986913fc7..dc04926004 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.5.3"], + "requirements": ["pywemo==0.5.6"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index fc00d4ea8b..50926e07a1 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -3,7 +3,6 @@ import asyncio from datetime import datetime, timedelta import logging -import async_timeout from pywemo.ouimeaux_device.api.service import ActionException from homeassistant.components.switch import SwitchEntity @@ -12,6 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import convert from .const import DOMAIN as WEMO_DOMAIN +from .entity import WemoSubscriptionEntity SCAN_INTERVAL = timedelta(seconds=10) PARALLEL_UPDATES = 0 @@ -49,57 +49,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class WemoSwitch(SwitchEntity): +class WemoSwitch(WemoSubscriptionEntity, SwitchEntity): """Representation of a WeMo switch.""" def __init__(self, device): """Initialize the WeMo switch.""" - self.wemo = device + super().__init__(device) self.insight_params = None self.maker_params = None self.coffeemaker_mode = None - self._state = None self._mode_string = None - self._available = True - self._update_lock = None - self._model_name = self.wemo.model_name - self._name = self.wemo.name - self._serialnumber = self.wemo.serialnumber - - def _subscription_callback(self, _device, _type, _params): - """Update the state by the Wemo device.""" - _LOGGER.info("Subscription update for %s", self.name) - updated = self.wemo.subscription_update(_type, _params) - self.hass.add_job(self._async_locked_subscription_callback(not updated)) - - async def _async_locked_subscription_callback(self, force_update): - """Handle an update from a subscription.""" - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - await self._async_locked_update(force_update) - self.async_write_ha_state() - - @property - def unique_id(self): - """Return the ID of this WeMo switch.""" - return self._serialnumber - - @property - def name(self): - """Return the name of the switch if any.""" - return self._name - - @property - def device_info(self): - """Return the device info.""" - return { - "name": self._name, - "identifiers": {(WEMO_DOMAIN, self._serialnumber)}, - "model": self._model_name, - "manufacturer": "Belkin", - } @property def device_state_attributes(self): @@ -172,20 +131,10 @@ class WemoSwitch(SwitchEntity): return STATE_STANDBY return STATE_UNKNOWN - @property - def is_on(self): - """Return true if switch is on. Standby is on.""" - return self._state - - @property - def available(self): - """Return true if switch is available.""" - return self._available - @property def icon(self): """Return the icon of device based on its type.""" - if self._model_name == "CoffeeMaker": + if self.wemo.model_name == "CoffeeMaker": return "mdi:coffee" return None @@ -211,50 +160,17 @@ class WemoSwitch(SwitchEntity): self.schedule_update_ha_state() - async def async_added_to_hass(self): - """Wemo switch added to Home Assistant.""" - # Define inside async context so we know our event loop - self._update_lock = asyncio.Lock() - - registry = self.hass.data[WEMO_DOMAIN]["registry"] - await self.hass.async_add_executor_job(registry.register, self.wemo) - registry.on(self.wemo, None, self._subscription_callback) - - async def async_update(self): - """Update WeMo state. - - Wemo has an aggressive retry logic that sometimes can take over a - minute to return. If we don't get a state after 5 seconds, assume the - Wemo switch is unreachable. If update goes through, it will be made - available again. - """ - # If an update is in progress, we don't do anything - if self._update_lock.locked(): - return - - try: - with async_timeout.timeout(5): - await asyncio.shield(self._async_locked_update(True)) - except asyncio.TimeoutError: - _LOGGER.warning("Lost connection to %s", self.name) - self._available = False - - async def _async_locked_update(self, force_update): - """Try updating within an async lock.""" - async with self._update_lock: - await self.hass.async_add_executor_job(self._update, force_update) - - def _update(self, force_update): + def _update(self, force_update=True): """Update the device state.""" try: self._state = self.wemo.get_state(force_update) - if self._model_name == "Insight": + if self.wemo.model_name == "Insight": self.insight_params = self.wemo.insight_params self.insight_params["standby_state"] = self.wemo.get_standby_state - elif self._model_name == "Maker": + elif self.wemo.model_name == "Maker": self.maker_params = self.wemo.maker_params - elif self._model_name == "CoffeeMaker": + elif self.wemo.model_name == "CoffeeMaker": self.coffeemaker_mode = self.wemo.mode self._mode_string = self.wemo.mode_string diff --git a/homeassistant/components/wemo/translations/pt.json b/homeassistant/components/wemo/translations/pt.json new file mode 100644 index 0000000000..7a4274b008 --- /dev/null +++ b/homeassistant/components/wemo/translations/pt.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/zh-Hant.json b/homeassistant/components/wemo/translations/zh-Hant.json index 0b9966135b..4be8350847 100644 --- a/homeassistant/components/wemo/translations/zh-Hant.json +++ b/homeassistant/components/wemo/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/avri/translations/pt.json b/homeassistant/components/wiffi/translations/pt.json similarity index 71% rename from homeassistant/components/avri/translations/pt.json rename to homeassistant/components/wiffi/translations/pt.json index 0b323a55dc..0077ceddd4 100644 --- a/homeassistant/components/avri/translations/pt.json +++ b/homeassistant/components/wiffi/translations/pt.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "zip_code": "C\u00f3digo postal" + "port": "Porta" } } } diff --git a/homeassistant/components/wiffi/translations/zh-Hant.json b/homeassistant/components/wiffi/translations/zh-Hant.json index ae2956cc5e..ea02e17933 100644 --- a/homeassistant/components/wiffi/translations/zh-Hant.json +++ b/homeassistant/components/wiffi/translations/zh-Hant.json @@ -9,7 +9,7 @@ "data": { "port": "\u901a\u8a0a\u57e0" }, - "title": "\u8a2d\u5b9a WIFFI \u8a2d\u5099 TCP \u4f3a\u670d\u5668" + "title": "\u8a2d\u5b9a WIFFI \u88dd\u7f6e TCP \u4f3a\u670d\u5668" } } }, diff --git a/homeassistant/components/wilight/translations/zh-Hant.json b/homeassistant/components/wilight/translations/zh-Hant.json index 8859e831d5..0a86501c8f 100644 --- a/homeassistant/components/wilight/translations/zh-Hant.json +++ b/homeassistant/components/wilight/translations/zh-Hant.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "not_supported_device": "\u4e0d\u652f\u63f4\u6b64\u6b3e WiLight \u8a2d\u5099\u3002", - "not_wilight_device": "\u6b64\u8a2d\u5099\u4e26\u975e WiLight" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "not_supported_device": "\u4e0d\u652f\u63f4\u6b64\u6b3e WiLight \u88dd\u7f6e\u3002", + "not_wilight_device": "\u6b64\u88dd\u7f6e\u4e26\u975e WiLight" }, "flow_title": "WiLight\uff1a{name}", "step": { diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 5fc7e1050a..2dd7407ad9 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_configured": "Konfigurasjon oppdatert for profil.", - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "no_url_available": "Ingen URL tilgjengelig. For informasjon om denne feilen, [sjekk hjelpseksjonen]({docs_url})" }, "create_entry": { @@ -26,7 +26,7 @@ }, "reauth": { "description": "Profilen {profile} m\u00e5 godkjennes p\u00e5 nytt for \u00e5 kunne fortsette \u00e5 motta Withings-data.", - "title": "Bekreft integrering p\u00e5 nytt" + "title": "Godkjenne integrering p\u00e5 nytt" } } } diff --git a/homeassistant/components/withings/translations/pt.json b/homeassistant/components/withings/translations/pt.json index b80d6630c3..1fe7083ecf 100644 --- a/homeassistant/components/withings/translations/pt.json +++ b/homeassistant/components/withings/translations/pt.json @@ -1,6 +1,17 @@ { "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})" + }, + "error": { + "already_configured": "Conta j\u00e1 configurada" + }, "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, "profile": { "data": { "profile": "Perfil" diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index 394a42c5fd..cd917f42b4 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -7,7 +7,7 @@ "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" + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" }, "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" diff --git a/homeassistant/components/wled/translations/pt.json b/homeassistant/components/wled/translations/pt.json index a6e5cd46cb..313c9057da 100644 --- a/homeassistant/components/wled/translations/pt.json +++ b/homeassistant/components/wled/translations/pt.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 37c74d07f5..0073bb2248 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "error": { @@ -16,8 +16,8 @@ "description": "\u8a2d\u5b9a WLED \u4ee5\u6574\u5408\u81f3 Home Assistant\u3002" }, "zeroconf_confirm": { - "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u8a2d\u5099\u81f3 Home Assistant\uff1f", - "title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u8a2d\u5099" + "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u88dd\u7f6e\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u88dd\u7f6e" } } } diff --git a/homeassistant/components/wolflink/__init__.py b/homeassistant/components/wolflink/__init__.py index 1bfae6cb90..39cd712740 100644 --- a/homeassistant/components/wolflink/__init__.py +++ b/homeassistant/components/wolflink/__init__.py @@ -38,7 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): device_id = entry.data[DEVICE_ID] gateway_id = entry.data[DEVICE_GATEWAY] _LOGGER.debug( - "Setting up wolflink integration for device: %s (id: %s, gateway: %s)", + "Setting up wolflink integration for device: %s (ID: %s, gateway: %s)", device_name, device_id, gateway_id, diff --git a/homeassistant/components/wolflink/translations/pt.json b/homeassistant/components/wolflink/translations/pt.json index 7953cf5625..308f60400a 100644 --- a/homeassistant/components/wolflink/translations/pt.json +++ b/homeassistant/components/wolflink/translations/pt.json @@ -9,6 +9,11 @@ "unknown": "Erro inesperado" }, "step": { + "device": { + "data": { + "device_name": "Dispositivo" + } + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/wolflink/translations/zh-Hant.json b/homeassistant/components/wolflink/translations/zh-Hant.json index 13eb90b55d..2a0dbc2544 100644 --- a/homeassistant/components/wolflink/translations/zh-Hant.json +++ b/homeassistant/components/wolflink/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", @@ -11,9 +11,9 @@ "step": { "device": { "data": { - "device_name": "\u8a2d\u5099" + "device_name": "\u88dd\u7f6e" }, - "title": "\u9078\u64c7 WOLF \u8a2d\u5099" + "title": "\u9078\u64c7 WOLF \u88dd\u7f6e" }, "user": { "data": { diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 70d6605320..4fb25c766c 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,7 +2,7 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": ["holidays==0.10.3"], + "requirements": ["holidays==0.10.4"], "codeowners": ["@fabaff"], "quality_scale": "internal" } diff --git a/homeassistant/components/xbox/translations/no.json b/homeassistant/components/xbox/translations/no.json index 49ccd378c1..4736fc91bf 100644 --- a/homeassistant/components/xbox/translations/no.json +++ b/homeassistant/components/xbox/translations/no.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "authorize_url_timeout": "Tidsavbrudd som genererer autorer URL-adresse.", - "missing_configuration": "Komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "create_entry": { diff --git a/homeassistant/components/xbox/translations/pt.json b/homeassistant/components/xbox/translations/pt.json new file mode 100644 index 0000000000..38f070ab3d --- /dev/null +++ b/homeassistant/components/xbox/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "create_entry": { + "default": "Autenticado com sucesso" + }, + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/zh-Hant.json b/homeassistant/components/xbox/translations/zh-Hant.json index 477bd13374..07fc710408 100644 --- a/homeassistant/components/xbox/translations/zh-Hant.json +++ b/homeassistant/components/xbox/translations/zh-Hant.json @@ -3,7 +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", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49" diff --git a/homeassistant/components/xiaomi_aqara/translations/pt.json b/homeassistant/components/xiaomi_aqara/translations/pt.json index a1340f587c..a800e4d57c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pt.json +++ b/homeassistant/components/xiaomi_aqara/translations/pt.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" + }, "error": { "invalid_host": "Endere\u00e7o IP Inv\u00e1lido" }, diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index cd1059436d..582aea354c 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", - "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u63a2\u7d22\u4e4b\u8a2d\u5099\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" + "not_xiaomi_aqara": "\u4e26\u975e\u5c0f\u7c73 Aqara \u7db2\u95dc\uff0c\u6240\u63a2\u7d22\u4e4b\u88dd\u7f6e\u8207\u5df2\u77e5\u7db2\u95dc\u4e0d\u7b26\u5408" }, "error": { - "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u8a2d\u5099\u7684 IP \u4f5c\u70ba\u4ecb\u9762", + "discovery_error": "\u63a2\u7d22\u5c0f\u7c73 Aqara \u7db2\u95dc\u5931\u6557\uff0c\u8acb\u5617\u8a66\u4f7f\u7528\u57f7\u884c Home Assistant \u88dd\u7f6e\u7684 IP \u4f5c\u70ba\u4ecb\u9762", "invalid_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u7a31\u6216 IP \u4f4d\u5740\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", "invalid_interface": "\u7db2\u8def\u4ecb\u9762\u7121\u6548", "invalid_key": "\u7db2\u95dc\u5bc6\u9470\u7121\u6548", @@ -26,7 +26,7 @@ "key": "\u7db2\u95dc\u5bc6\u9470", "name": "\u7db2\u95dc\u540d\u7a31" }, - "description": "\u5bc6\u9470\uff08\u5bc6\u78bc\uff09\u53d6\u5f97\u8acb\u53c3\u8003\u4e0b\u65b9\u6559\u5b78\uff1ahttps://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz\u3002\u5047\u5982\u672a\u63d0\u4f9b\u5bc6\u9470\u3001\u5247\u50c5\u6703\u6536\u5230\u50b3\u611f\u5668\u8a2d\u5099\u7684\u8cc7\u8a0a\u3002\uff3c", + "description": "\u5bc6\u9470\uff08\u5bc6\u78bc\uff09\u53d6\u5f97\u8acb\u53c3\u8003\u4e0b\u65b9\u6559\u5b78\uff1ahttps://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz\u3002\u5047\u5982\u672a\u63d0\u4f9b\u5bc6\u9470\u3001\u5247\u50c5\u6703\u6536\u5230\u50b3\u611f\u5668\u88dd\u7f6e\u7684\u8cc7\u8a0a", "title": "\u5c0f\u7c73 Aqara \u7db2\u95dc\u9078\u9805\u8a2d\u5b9a" }, "user": { diff --git a/homeassistant/components/xiaomi_miio/translations/pt.json b/homeassistant/components/xiaomi_miio/translations/pt.json index 5c127b797e..65edf2dbe3 100644 --- a/homeassistant/components/xiaomi_miio/translations/pt.json +++ b/homeassistant/components/xiaomi_miio/translations/pt.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, "step": { "gateway": { "data": { - "host": "Endere\u00e7o IP" + "host": "Endere\u00e7o IP", + "token": "API Token" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index fd77eb4df8..95499fb7b8 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", - "no_device_selected": "\u672a\u9078\u64c7\u8a2d\u5099\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u8a2d\u5099\u3002" + "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002" }, "flow_title": "Xiaomi Miio\uff1a{name}", "step": { @@ -23,7 +23,7 @@ "data": { "gateway": "\u9023\u7dda\u81f3\u5c0f\u7c73\u7db2\u95dc" }, - "description": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u8a2d\u5099\u3002", + "description": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684\u88dd\u7f6e\u3002", "title": "\u5c0f\u7c73 Miio" } } diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 2a9ae1187b..ab76d14a69 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -75,6 +75,7 @@ ATTR_STATUS = "status" ATTR_ZONE_ARRAY = "zone" ATTR_ZONE_REPEATER = "repeats" ATTR_TIMERS = "timers" +ATTR_MOP_ATTACHED = "mop_attached" SUPPORT_XIAOMI = ( SUPPORT_STATE @@ -326,6 +327,7 @@ class MiroboVacuum(StateVacuumEntity): self.consumable_state.sensor_dirty_left.total_seconds() / 3600 ), ATTR_STATUS: str(self.vacuum_state.state), + ATTR_MOP_ATTACHED: self.vacuum_state.is_water_box_attached, } ) diff --git a/homeassistant/components/yeelight/translations/cs.json b/homeassistant/components/yeelight/translations/cs.json index 4ede775331..8bab9bd19b 100644 --- a/homeassistant/components/yeelight/translations/cs.json +++ b/homeassistant/components/yeelight/translations/cs.json @@ -26,8 +26,10 @@ "init": { "data": { "model": "Model (voliteln\u00fd)", + "nightlight_switch": "Pou\u017e\u00edt p\u0159ep\u00edna\u010d no\u010dn\u00edho osv\u011btlen\u00ed", "save_on_change": "Ulo\u017eit stav p\u0159i zm\u011bn\u011b", - "transition": "\u010cas p\u0159echodu (v ms)" + "transition": "\u010cas p\u0159echodu (v ms)", + "use_music_mode": "Povolit hudebn\u00ed re\u017eim" }, "description": "Pokud ponech\u00e1te model pr\u00e1zdn\u00fd, bude automaticky rozpozn\u00e1n." } diff --git a/homeassistant/components/yeelight/translations/pt.json b/homeassistant/components/yeelight/translations/pt.json index e4a8cc8062..6d35018898 100644 --- a/homeassistant/components/yeelight/translations/pt.json +++ b/homeassistant/components/yeelight/translations/pt.json @@ -14,6 +14,9 @@ } }, "user": { + "data": { + "host": "Servidor" + }, "description": "Se voc\u00ea deixar o modelo vazio, ele ser\u00e1 detectado automaticamente." } } diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index b19ebdb40f..d9bf3c123b 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -10,14 +10,14 @@ "step": { "pick_device": { "data": { - "device": "\u8a2d\u5099" + "device": "\u88dd\u7f6e" } }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u8a2d\u5099\u3002" + "description": "\u5047\u5982\u4e3b\u6a5f\u7aef\u4f4d\u5740\u6b04\u4f4d\u70ba\u7a7a\u767d\uff0c\u5c07\u6703\u63a2\u7d22\u6240\u6709\u53ef\u7528\u88dd\u7f6e\u3002" } } }, diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 68300adbcf..fdf4b98faf 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -284,22 +284,30 @@ async def _async_start_zeroconf_browser(hass, zeroconf): # likely bad homekit data return + if "name" in info: + lowercase_name = info["name"].lower() + else: + lowercase_name = None + + if "macaddress" in info.get("properties", {}): + uppercase_mac = info["properties"]["macaddress"].upper() + else: + uppercase_mac = None + for entry in zeroconf_types[service_type]: if len(entry) > 1: - if "macaddress" in entry: - if "properties" not in info: - continue - if "macaddress" not in info["properties"]: - continue - if not fnmatch.fnmatch( - info["properties"]["macaddress"], entry["macaddress"] - ): - continue - if "name" in entry: - if "name" not in info: - continue - if not fnmatch.fnmatch(info["name"], entry["name"]): - continue + if ( + uppercase_mac is not None + and "macaddress" in entry + and not fnmatch.fnmatch(uppercase_mac, entry["macaddress"]) + ): + continue + if ( + lowercase_name is not None + and "name" in entry + and not fnmatch.fnmatch(lowercase_name, entry["name"]) + ): + continue hass.add_job( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 753ac2a244..654eec820c 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,7 +2,7 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.28.7"], + "requirements": ["zeroconf==0.28.8"], "dependencies": ["api"], "codeowners": ["@bdraco"], "quality_scale": "internal" diff --git a/homeassistant/components/zerproc/config_flow.py b/homeassistant/components/zerproc/config_flow.py index 28597b3859..6e3d70b081 100644 --- a/homeassistant/components/zerproc/config_flow.py +++ b/homeassistant/components/zerproc/config_flow.py @@ -14,7 +14,7 @@ _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass) -> bool: """Return if there are devices that can be discovered.""" try: - devices = await hass.async_add_executor_job(pyzerproc.discover) + devices = await pyzerproc.discover() return len(devices) > 0 except pyzerproc.ZerprocException: _LOGGER.error("Unable to discover nearby Zerproc devices", exc_info=True) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index 2ab4bc127c..89f60faf84 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -1,4 +1,5 @@ """Zerproc light platform.""" +import asyncio from datetime import timedelta import logging from typing import Callable, List, Optional @@ -28,25 +29,20 @@ SUPPORT_ZERPROC = SUPPORT_BRIGHTNESS | SUPPORT_COLOR DISCOVERY_INTERVAL = timedelta(seconds=60) -PARALLEL_UPDATES = 0 + +async def connect_light(light: pyzerproc.Light) -> Optional[pyzerproc.Light]: + """Return the given light if it connects successfully.""" + try: + await light.connect() + except pyzerproc.ZerprocException: + _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) + return None + return light -def connect_lights(lights: List[pyzerproc.Light]) -> List[pyzerproc.Light]: - """Attempt to connect to lights, and return the connected lights.""" - connected = [] - for light in lights: - try: - light.connect(auto_reconnect=True) - connected.append(light) - except pyzerproc.ZerprocException: - _LOGGER.debug("Unable to connect to '%s'", light.address, exc_info=True) - - return connected - - -def discover_entities(hass: HomeAssistant) -> List[Entity]: +async def discover_entities(hass: HomeAssistant) -> List[Entity]: """Attempt to discover new lights.""" - lights = pyzerproc.discover() + lights = await pyzerproc.discover() # Filter out already discovered lights new_lights = [ @@ -54,8 +50,11 @@ def discover_entities(hass: HomeAssistant) -> List[Entity]: ] entities = [] - for light in connect_lights(new_lights): - # Double-check the light hasn't been added in another thread + connected_lights = filter( + None, await asyncio.gather(*(connect_light(light) for light in new_lights)) + ) + for light in connected_lights: + # Double-check the light hasn't been added in the meantime if light.address not in hass.data[DOMAIN]["addresses"]: hass.data[DOMAIN]["addresses"].add(light.address) entities.append(ZerprocLight(light)) @@ -68,7 +67,7 @@ async def async_setup_entry( config_entry: ConfigEntry, async_add_entities: Callable[[List[Entity], bool], None], ) -> None: - """Set up Abode light devices.""" + """Set up Zerproc light devices.""" if DOMAIN not in hass.data: hass.data[DOMAIN] = {} if "addresses" not in hass.data[DOMAIN]: @@ -80,7 +79,7 @@ async def async_setup_entry( """Wrap discovery to include params.""" nonlocal warned try: - entities = await hass.async_add_executor_job(discover_entities, hass) + entities = await discover_entities(hass) async_add_entities(entities, update_before_add=True) warned = False except pyzerproc.ZerprocException: @@ -111,17 +110,18 @@ class ZerprocLight(LightEntity): """Run when entity about to be added to hass.""" self.async_on_remove( self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self.on_hass_shutdown + EVENT_HOMEASSISTANT_STOP, self.async_will_remove_from_hass ) ) - async def async_will_remove_from_hass(self) -> None: + async def async_will_remove_from_hass(self, *args) -> None: """Run when entity will be removed from hass.""" - await self.hass.async_add_executor_job(self._light.disconnect) - - def on_hass_shutdown(self, event): - """Execute when Home Assistant is shutting down.""" - self._light.disconnect() + try: + await self._light.disconnect() + except pyzerproc.ZerprocException: + _LOGGER.debug( + "Exception disconnected from %s", self.entity_id, exc_info=True + ) @property def name(self): @@ -172,7 +172,7 @@ class ZerprocLight(LightEntity): """Return True if entity is available.""" return self._available - def turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs: default_hs = (0, 0) if self._hs_color is None else self._hs_color @@ -182,18 +182,20 @@ class ZerprocLight(LightEntity): brightness = kwargs.get(ATTR_BRIGHTNESS, default_brightness) rgb = color_util.color_hsv_to_RGB(*hue_sat, brightness / 255 * 100) - self._light.set_color(*rgb) + await self._light.set_color(*rgb) else: - self._light.turn_on() + await self._light.turn_on() - def turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs): """Instruct the light to turn off.""" - self._light.turn_off() + await self._light.turn_off() - def update(self): + async def async_update(self): """Fetch new state data for this light.""" try: - state = self._light.get_state() + if not self._available: + await self._light.connect() + state = await self._light.get_state() except pyzerproc.ZerprocException: if self._available: _LOGGER.warning("Unable to connect to %s", self.entity_id) diff --git a/homeassistant/components/zerproc/manifest.json b/homeassistant/components/zerproc/manifest.json index 4f9b559bc1..54b70d7867 100644 --- a/homeassistant/components/zerproc/manifest.json +++ b/homeassistant/components/zerproc/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zerproc", "requirements": [ - "pyzerproc==0.2.5" + "pyzerproc==0.4.7" ], "codeowners": [ "@emlove" diff --git a/homeassistant/components/zerproc/translations/de.json b/homeassistant/components/zerproc/translations/de.json index fdbf897123..dfc337fc84 100644 --- a/homeassistant/components/zerproc/translations/de.json +++ b/homeassistant/components/zerproc/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/zerproc/translations/pt.json b/homeassistant/components/zerproc/translations/pt.json new file mode 100644 index 0000000000..e25888655a --- /dev/null +++ b/homeassistant/components/zerproc/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Nenhum dispositivo encontrado na rede", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "step": { + "confirm": { + "description": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/zh-Hant.json b/homeassistant/components/zerproc/translations/zh-Hant.json index 91a0dc60be..90c98e491d 100644 --- a/homeassistant/components/zerproc/translations/zh-Hant.json +++ b/homeassistant/components/zerproc/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u8a2d\u5099", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "step": { "confirm": { diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index ba95a0e4bc..48f35e035f 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -73,13 +73,9 @@ class BinarySensor(ZhaEntity, BinarySensorEntity): self._channel = channels[0] self._device_class = self.DEVICE_CLASS - async def get_device_class(self): - """Get the HA device class from the channel.""" - async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() - await self.get_device_class() self.async_accept_signal( self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) @@ -168,10 +164,10 @@ class IASZone(BinarySensor): SENSOR_ATTR = "zone_status" - async def get_device_class(self) -> None: - """Get the HA device class from the channel.""" - zone_type = await self._channel.get_attribute_value("zone_type") - self._device_class = CLASS_MAPPING.get(zone_type) + @property + def device_class(self) -> str: + """Return device class from component DEVICE_CLASSES.""" + return CLASS_MAPPING.get(self._channel.cluster.get("zone_type")) async def async_update(self): """Attempt to retrieve on off state from the binary sensor.""" diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index c6019c1084..2dbd162948 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -187,29 +187,36 @@ class ZigbeeChannel(LogMixin): str(ex), ) - async def async_configure(self): + async def async_configure(self) -> None: """Set cluster binding and attribute reporting.""" if not self._ch_pool.skip_configuration: await self.bind() if self.cluster.is_server: await self.configure_reporting() + ch_specific_cfg = getattr(self, "async_configure_channel_specific", None) + if ch_specific_cfg: + await ch_specific_cfg() self.debug("finished channel configuration") else: self.debug("skipping channel configuration") self._status = ChannelStatus.CONFIGURED - async def async_initialize(self, from_cache): + async def async_initialize(self, from_cache: bool) -> None: """Initialize channel.""" if not from_cache and self._ch_pool.skip_configuration: self._status = ChannelStatus.INITIALIZED return self.debug("initializing channel: from_cache: %s", from_cache) - attributes = [] - for report_config in self._report_config: - attributes.append(report_config["attr"]) + attributes = [cfg["attr"] for cfg in self._report_config] if attributes: await self.get_attributes(attributes, from_cache=from_cache) + + ch_specific_init = getattr(self, "async_initialize_channel_specific", None) + if ch_specific_init: + await ch_specific_init(from_cache=from_cache) + + self.debug("finished channel configuration") self._status = ChannelStatus.INITIALIZED @callback diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 0760427d46..0326f18ac6 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -35,11 +35,6 @@ class DoorLockChannel(ZigbeeChannel): f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value ) - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self._value_attribute, from_cache=from_cache) - await super().async_initialize(from_cache) - @registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id) class Shade(ZigbeeChannel): @@ -85,8 +80,3 @@ class WindowCovering(ZigbeeChannel): self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value ) - - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self._value_attribute, from_cache=from_cache) - await super().async_initialize(from_cache) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index dc06d01e59..d105572c18 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -1,9 +1,10 @@ """General channels module for Zigbee Home Automation.""" import asyncio -from typing import Any, List, Optional +from typing import Any, Coroutine, List, Optional import zigpy.exceptions import zigpy.zcl.clusters.general as general +from zigpy.zcl.foundation import Status from homeassistant.core import callback from homeassistant.helpers.event import async_call_later @@ -19,7 +20,8 @@ from ..const import ( SIGNAL_SET_LEVEL, SIGNAL_UPDATE_DEVICE, ) -from .base import ChannelStatus, ClientChannel, ZigbeeChannel, parse_and_log_command +from ..helpers import retryable_req +from .base import ClientChannel, ZigbeeChannel, parse_and_log_command @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) @@ -34,12 +36,85 @@ class AnalogInput(ZigbeeChannel): REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] +@registries.BINDABLE_CLUSTERS.register(general.AnalogOutput.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id) class AnalogOutput(ZigbeeChannel): """Analog Output channel.""" REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + @property + def present_value(self) -> Optional[float]: + """Return cached value of present_value.""" + return self.cluster.get("present_value") + + @property + def min_present_value(self) -> Optional[float]: + """Return cached value of min_present_value.""" + return self.cluster.get("min_present_value") + + @property + def max_present_value(self) -> Optional[float]: + """Return cached value of max_present_value.""" + return self.cluster.get("max_present_value") + + @property + def resolution(self) -> Optional[float]: + """Return cached value of resolution.""" + return self.cluster.get("resolution") + + @property + def relinquish_default(self) -> Optional[float]: + """Return cached value of relinquish_default.""" + return self.cluster.get("relinquish_default") + + @property + def description(self) -> Optional[str]: + """Return cached value of description.""" + return self.cluster.get("description") + + @property + def engineering_units(self) -> Optional[int]: + """Return cached value of engineering_units.""" + return self.cluster.get("engineering_units") + + @property + def application_type(self) -> Optional[int]: + """Return cached value of application_type.""" + return self.cluster.get("application_type") + + async def async_set_present_value(self, value: float) -> bool: + """Update present_value.""" + try: + res = await self.cluster.write_attributes({"present_value": value}) + except zigpy.exceptions.ZigbeeException as ex: + self.error("Could not set value: %s", ex) + return False + if isinstance(res, list) and all( + [record.status == Status.SUCCESS for record in res[0]] + ): + return True + return False + + @retryable_req(delays=(1, 1, 3)) + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: + """Initialize channel.""" + return self.fetch_config(from_cache) + + async def fetch_config(self, from_cache: bool) -> None: + """Get the channel configuration.""" + attributes = [ + "min_present_value", + "max_present_value", + "resolution", + "relinquish_default", + "description", + "engineering_units", + "application_type", + ] + # just populates the cache, if not already done + await self.get_attributes(attributes, from_cache=from_cache) + @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id) class AnalogValue(ZigbeeChannel): @@ -71,21 +146,6 @@ class BasicChannel(ZigbeeChannel): 6: "Emergency mains and transfer switch", } - async def async_configure(self): - """Configure this channel.""" - await super().async_configure() - await self.async_initialize(False) - - async def async_initialize(self, from_cache): - """Initialize channel.""" - if not self._ch_pool.skip_configuration or from_cache: - await self.get_attribute_value("power_source", from_cache=from_cache) - await super().async_initialize(from_cache) - - def get_power_source(self): - """Get the power source.""" - return self.cluster.get("power_source") - @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) class BinaryInput(ZigbeeChannel): @@ -189,11 +249,6 @@ class LevelControlChannel(ZigbeeChannel): """Dispatch level change.""" self.async_send_signal(f"{self.unique_id}_{command}", level) - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self.CURRENT_LEVEL, from_cache=from_cache) - await super().async_initialize(from_cache) - @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateInput.cluster_id) class MultistateInput(ZigbeeChannel): @@ -284,12 +339,9 @@ class OnOffChannel(ZigbeeChannel): ) self._state = bool(value) - async def async_initialize(self, from_cache): + async def async_initialize_channel_specific(self, from_cache: bool) -> None: """Initialize channel.""" - await super().async_initialize(from_cache) - state = await self.get_attribute_value(self.ON_OFF, from_cache=True) - if state is not None: - self._state = bool(state) + self._state = self.on_off async def async_update(self): """Initialize channel.""" @@ -338,7 +390,7 @@ class PollControl(ZigbeeChannel): CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s LONG_POLL = 6 * 4 # 6s - async def async_configure(self) -> None: + async def async_configure_channel_specific(self) -> None: """Configure channel: set check-in interval.""" try: res = await self.cluster.write_attributes( @@ -347,7 +399,6 @@ class PollControl(ZigbeeChannel): self.debug("%ss check-in interval set: %s", self.CHECKIN_INTERVAL / 4, res) except (asyncio.TimeoutError, zigpy.exceptions.ZigbeeException) as ex: self.debug("Couldn't set check-in interval: %s", ex) - await super().async_configure() @callback def cluster_command( @@ -375,16 +426,13 @@ class PowerConfigurationChannel(ZigbeeChannel): {"attr": "battery_percentage_remaining", "config": REPORT_CONFIG_BATTERY_SAVE}, ) - async def async_initialize(self, from_cache): - """Initialize channel.""" + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: + """Initialize channel specific attrs.""" attributes = [ "battery_size", - "battery_percentage_remaining", - "battery_voltage", "battery_quantity", ] - await self.get_attributes(attributes, from_cache=from_cache) - self._status = ChannelStatus.INITIALIZED + return self.get_attributes(attributes, from_cache=from_cache) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 03812be054..5b3a4778fc 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -1,5 +1,5 @@ """Home automation channels module for Zigbee Home Automation.""" -from typing import Optional +from typing import Coroutine, Optional import zigpy.zcl.clusters.homeautomation as homeautomation @@ -62,23 +62,17 @@ class ElectricalMeasurementChannel(ZigbeeChannel): result, ) - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.fetch_config(True) - await super().async_initialize(from_cache) + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: + """Initialize channel specific attributes.""" - async def fetch_config(self, from_cache): - """Fetch config from device and updates format specifier.""" - - # prime the cache - await self.get_attributes( + return self.get_attributes( [ "ac_power_divisor", "power_divisor", "ac_power_multiplier", "power_multiplier", ], - from_cache=from_cache, + from_cache=True, ) @property diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index ac832aacc6..1647c5ce52 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -43,17 +43,10 @@ class FanChannel(ZigbeeChannel): REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) - def __init__( - self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType - ): - """Init Thermostat channel instance.""" - super().__init__(cluster, ch_pool) - self._fan_mode = None - @property def fan_mode(self) -> Optional[int]: """Return current fan mode.""" - return self._fan_mode + return self.cluster.get("fan_mode") async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" @@ -66,12 +59,7 @@ class FanChannel(ZigbeeChannel): async def async_update(self) -> None: """Retrieve latest state.""" - result = await self.get_attribute_value("fan_mode", from_cache=True) - if result is not None: - self._fan_mode = result - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", 0, "fan_mode", result - ) + await self.get_attribute_value("fan_mode", from_cache=False) @callback def attribute_updated(self, attrid: int, value: Any) -> None: @@ -80,8 +68,7 @@ class FanChannel(ZigbeeChannel): self.debug( "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value ) - if attrid == self._value_attribute: - self._fan_mode = value + if attr_name == "fan_mode": self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value ) @@ -362,7 +349,7 @@ class ThermostatChannel(ZigbeeChannel): ) @retryable_req(delays=(1, 1, 3)) - async def async_initialize(self, from_cache): + async def async_initialize_channel_specific(self, from_cache: bool) -> None: """Initialize channel.""" cached = [a for a, cached in self._init_attrs.items() if cached] @@ -370,7 +357,6 @@ class ThermostatChannel(ZigbeeChannel): await self._chunk_attr_read(cached, cached=True) await self._chunk_attr_read(uncached, cached=False) - await super().async_initialize(from_cache) async def async_set_operation_mode(self, mode) -> bool: """Set Operation mode.""" diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 16223582c3..c8827e20e0 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -1,5 +1,5 @@ """Lighting channels module for Zigbee Home Automation.""" -from typing import Optional +from typing import Coroutine, Optional import zigpy.zcl.clusters.lighting as lighting @@ -75,15 +75,13 @@ class ColorChannel(ZigbeeChannel): """Return the warmest color_temp that this channel supports.""" return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS) - async def async_configure(self) -> None: + def async_configure_channel_specific(self) -> Coroutine: """Configure channel.""" - await self.fetch_color_capabilities(False) - await super().async_configure() + return self.fetch_color_capabilities(False) - async def async_initialize(self, from_cache: bool) -> None: + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: """Initialize channel.""" - await self.fetch_color_capabilities(True) - await super().async_initialize(from_cache) + return self.fetch_color_capabilities(True) async def fetch_color_capabilities(self, from_cache: bool) -> None: """Get the color configuration.""" diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 9a357d76eb..600493e8a1 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -1,11 +1,41 @@ """Lightlink channels module for Zigbee Home Automation.""" +import asyncio + +import zigpy.exceptions import zigpy.zcl.clusters.lightlink as lightlink from .. import registries -from .base import ZigbeeChannel +from .base import ChannelStatus, ZigbeeChannel @registries.CHANNEL_ONLY_CLUSTERS.register(lightlink.LightLink.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(lightlink.LightLink.cluster_id) class LightLink(ZigbeeChannel): """Lightlink channel.""" + + async def async_configure(self) -> None: + """Add Coordinator to LightLink group .""" + + if self._ch_pool.skip_configuration: + self._status = ChannelStatus.CONFIGURED + return + + application = self._ch_pool.endpoint.device.application + try: + coordinator = application.get_device(application.ieee) + except KeyError: + self.warning("Aborting - unable to locate required coordinator device.") + return + + try: + _, _, groups = await self.cluster.get_group_identifiers(0) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as exc: + self.warning("Couldn't get list of groups: %s", str(exc)) + return + + if groups: + for group in groups: + self.debug("Adding coordinator to 0x%04x group id", group.group_id) + await coordinator.add_to_group(group.group_id) + else: + await coordinator.add_to_group(0x0000, name="Default Lightlink Group") diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index e37987bc82..7c600d9840 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ import asyncio +from typing import Coroutine from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.security as security @@ -20,7 +21,7 @@ from ..const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .base import ZigbeeChannel +from .base import ChannelStatus, ZigbeeChannel @registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasAce.cluster_id) @@ -155,14 +156,10 @@ class IASZoneChannel(ZigbeeChannel): str(ex), ) - try: - self.debug("Sending pro-active IAS enroll response") - await self._cluster.enroll_response(0, 0) - except ZigbeeException as ex: - self.debug( - "Failed to send pro-active IAS enroll response: %s", - str(ex), - ) + self.debug("Sending pro-active IAS enroll response") + self._cluster.create_catching_task(self._cluster.enroll_response(0, 0)) + + self._status = ChannelStatus.CONFIGURED self.debug("finished IASZoneChannel configuration") @callback @@ -177,8 +174,7 @@ class IASZoneChannel(ZigbeeChannel): value, ) - async def async_initialize(self, from_cache): + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: """Initialize channel.""" - attributes = ["zone_status", "zone_state"] - await self.get_attributes(attributes, from_cache=from_cache) - await super().async_initialize(from_cache) + attributes = ["zone_status", "zone_state", "zone_type"] + return self.get_attributes(attributes, from_cache=from_cache) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index 87c22b160f..32e4902799 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -1,5 +1,5 @@ """Smart energy channels module for Zigbee Home Automation.""" -from typing import Union +from typing import Coroutine, Union import zigpy.zcl.clusters.smartenergy as smartenergy @@ -96,15 +96,13 @@ class Metering(ZigbeeChannel): """Return multiplier for the value.""" return self.cluster.get("multiplier") or 1 - async def async_configure(self) -> None: + def async_configure_channel_specific(self) -> Coroutine: """Configure channel.""" - await self.fetch_config(False) - await super().async_configure() + return self.fetch_config(False) - async def async_initialize(self, from_cache: bool) -> None: + def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine: """Initialize channel.""" - await self.fetch_config(True) - await super().async_initialize(from_cache) + return self.fetch_config(True) @callback def attribute_updated(self, attrid: int, value: int) -> None: diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 12d928e172..1d3f767353 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -18,6 +18,7 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK +from homeassistant.components.number import DOMAIN as NUMBER from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH @@ -71,6 +72,7 @@ BINDINGS = "bindings" CHANNEL_ACCELEROMETER = "accelerometer" CHANNEL_ANALOG_INPUT = "analog_input" +CHANNEL_ANALOG_OUTPUT = "analog_output" CHANNEL_ATTRIBUTE = "attribute" CHANNEL_BASIC = "basic" CHANNEL_COLOR = "light_color" @@ -110,6 +112,7 @@ COMPONENTS = ( FAN, LIGHT, LOCK, + NUMBER, SENSOR, SWITCH, ) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 05a12bc228..e071a52332 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -22,6 +22,7 @@ from .. import ( # noqa: F401 pylint: disable=unused-import, fan, light, lock, + number, sensor, switch, ) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 812ac168d4..c57c726972 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -634,7 +634,7 @@ class ZHAGateway: tasks = [] for member in members: _LOGGER.debug( - "Adding member with IEEE: %s and endpoint id: %s to group: %s:0x%04x", + "Adding member with IEEE: %s and endpoint ID: %s to group: %s:0x%04x", member.ieee, member.endpoint_id, name, diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index e2b4056cfa..4dcccc98c0 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -14,6 +14,7 @@ from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.lock import DOMAIN as LOCK +from homeassistant.components.number import DOMAIN as NUMBER from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH @@ -61,6 +62,7 @@ SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { zcl.clusters.closures.DoorLock.cluster_id: LOCK, zcl.clusters.closures.WindowCovering.cluster_id: COVER, zcl.clusters.general.AnalogInput.cluster_id: SENSOR, + zcl.clusters.general.AnalogOutput.cluster_id: NUMBER, zcl.clusters.general.MultistateInput.cluster_id: SENSOR, zcl.clusters.general.OnOff.cluster_id: SWITCH, zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR, diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 9983967f76..b25d1c1aa3 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,6 +1,6 @@ """Fans on Zigbee Home Automation networks.""" import functools -from typing import List +from typing import List, Optional from zigpy.exceptions import ZigbeeException import zigpy.zcl.clusters.hvac as hvac @@ -62,7 +62,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + discovery.async_add_entities, + async_add_entities, + entities_to_create, + update_before_add=False, ), ) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) @@ -87,13 +90,6 @@ class BaseFan(FanEntity): """Return the current speed.""" return self._state - @property - def is_on(self) -> bool: - """Return true if entity is on.""" - if self._state is None: - return False - return self._state != SPEED_OFF - @property def supported_features(self) -> int: """Flag supported features.""" @@ -136,25 +132,16 @@ class ZhaFan(BaseFan, ZhaEntity): self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - self._state = VALUE_TO_SPEED.get(last_state.state, last_state.state) + @property + def speed(self) -> Optional[str]: + """Return the current speed.""" + return VALUE_TO_SPEED.get(self._fan_channel.fan_mode) @callback def async_set_state(self, attr_id, attr_name, value): """Handle state update from channel.""" - self._state = VALUE_TO_SPEED.get(value, self._state) self.async_write_ha_state() - async def async_update(self): - """Attempt to retrieve on off state from the fan.""" - await super().async_update() - if self._fan_channel: - state = await self._fan_channel.get_attribute_value("fan_mode") - if state is not None: - self._state = VALUE_TO_SPEED.get(state, self._state) - @GROUP_MATCH() class FanGroup(BaseFan, ZhaGroupEntity): @@ -185,9 +172,15 @@ class FanGroup(BaseFan, ZhaGroupEntity): all_states = [self.hass.states.get(x) for x in self._entity_ids] states: List[State] = list(filter(None, all_states)) on_states: List[State] = [state for state in states if state.state != SPEED_OFF] + self._available = any(state.state != STATE_UNAVAILABLE for state in states) # for now just use first non off state since its kind of arbitrary if not on_states: self._state = SPEED_OFF else: - self._state = states[0].state + self._state = on_states[0].state + + async def async_added_to_hass(self) -> None: + """Run when about to be added to hass.""" + await self.async_update() + await super().async_added_to_hass() diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6b3a39d092..32b8a06405 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,6 +1,7 @@ """Lights on Zigbee Home Automation networks.""" from collections import Counter from datetime import timedelta +import enum import functools import itertools import logging @@ -88,6 +89,14 @@ SUPPORT_GROUP_LIGHT = ( ) +class LightColorMode(enum.IntEnum): + """ZCL light color mode enum.""" + + HS_COLOR = 0x00 + XY_COLOR = 0x01 + COLOR_TEMP = 0x02 + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation light from config entry.""" entities_to_create = hass.data[DATA_ZHA][light.DOMAIN] @@ -239,6 +248,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._color_temp = temperature + self._hs_color = None if ( light.ATTR_HS_COLOR in kwargs @@ -254,6 +264,7 @@ class BaseLight(LogMixin, light.LightEntity): self.debug("turned on: %s", t_log) return self._hs_color = hs_color + self._color_temp = None if ( effect == light.EFFECT_COLORLOOP @@ -440,6 +451,7 @@ class Light(BaseLight, ZhaEntity): self._brightness = level if self._color_channel: attributes = [ + "color_mode", "color_temperature", "current_x", "current_y", @@ -450,16 +462,21 @@ class Light(BaseLight, ZhaEntity): attributes, from_cache=False ) - color_temp = results.get("color_temperature") - if color_temp is not None: - self._color_temp = color_temp - - color_x = results.get("current_x") - color_y = results.get("current_y") - if color_x is not None and color_y is not None: - self._hs_color = color_util.color_xy_to_hs( - float(color_x / 65535), float(color_y / 65535) - ) + color_mode = results.get("color_mode") + if color_mode is not None: + if color_mode == LightColorMode.COLOR_TEMP: + color_temp = results.get("color_temperature") + if color_temp is not None and color_mode: + self._color_temp = color_temp + self._hs_color = None + else: + color_x = results.get("current_x") + color_y = results.get("current_y") + if color_x is not None and color_y is not None: + self._hs_color = color_util.color_xy_to_hs( + float(color_x / 65535), float(color_y / 65535) + ) + self._color_temp = None color_loop_active = results.get("color_loop_active") if color_loop_active is not None: diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index bcaa4038de..54fceda03a 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,12 +5,12 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows==0.21.0", - "pyserial==3.4", - "pyserial-asyncio==0.4", - "zha-quirks==0.0.49", + "pyserial==3.5", + "pyserial-asyncio==0.5", + "zha-quirks==0.0.51", "zigpy-cc==0.5.2", - "zigpy-deconz==0.11.0", - "zigpy==0.28.2", + "zigpy-deconz==0.11.1", + "zigpy==0.29.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.3.0" diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py new file mode 100644 index 0000000000..b4772e5174 --- /dev/null +++ b/homeassistant/components/zha/number.py @@ -0,0 +1,339 @@ +"""Support for ZHA AnalogOutput cluster.""" +import functools +import logging + +from homeassistant.components.number import DOMAIN, NumberEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .core import discovery +from .core.const import ( + CHANNEL_ANALOG_OUTPUT, + DATA_ZHA, + DATA_ZHA_DISPATCHERS, + SIGNAL_ADD_ENTITIES, + SIGNAL_ATTR_UPDATED, +) +from .core.registries import ZHA_ENTITIES +from .entity import ZhaEntity + +_LOGGER = logging.getLogger(__name__) + +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) + + +UNITS = { + 0: "Square-meters", + 1: "Square-feet", + 2: "Milliamperes", + 3: "Amperes", + 4: "Ohms", + 5: "Volts", + 6: "Kilo-volts", + 7: "Mega-volts", + 8: "Volt-amperes", + 9: "Kilo-volt-amperes", + 10: "Mega-volt-amperes", + 11: "Volt-amperes-reactive", + 12: "Kilo-volt-amperes-reactive", + 13: "Mega-volt-amperes-reactive", + 14: "Degrees-phase", + 15: "Power-factor", + 16: "Joules", + 17: "Kilojoules", + 18: "Watt-hours", + 19: "Kilowatt-hours", + 20: "BTUs", + 21: "Therms", + 22: "Ton-hours", + 23: "Joules-per-kilogram-dry-air", + 24: "BTUs-per-pound-dry-air", + 25: "Cycles-per-hour", + 26: "Cycles-per-minute", + 27: "Hertz", + 28: "Grams-of-water-per-kilogram-dry-air", + 29: "Percent-relative-humidity", + 30: "Millimeters", + 31: "Meters", + 32: "Inches", + 33: "Feet", + 34: "Watts-per-square-foot", + 35: "Watts-per-square-meter", + 36: "Lumens", + 37: "Luxes", + 38: "Foot-candles", + 39: "Kilograms", + 40: "Pounds-mass", + 41: "Tons", + 42: "Kilograms-per-second", + 43: "Kilograms-per-minute", + 44: "Kilograms-per-hour", + 45: "Pounds-mass-per-minute", + 46: "Pounds-mass-per-hour", + 47: "Watts", + 48: "Kilowatts", + 49: "Megawatts", + 50: "BTUs-per-hour", + 51: "Horsepower", + 52: "Tons-refrigeration", + 53: "Pascals", + 54: "Kilopascals", + 55: "Bars", + 56: "Pounds-force-per-square-inch", + 57: "Centimeters-of-water", + 58: "Inches-of-water", + 59: "Millimeters-of-mercury", + 60: "Centimeters-of-mercury", + 61: "Inches-of-mercury", + 62: "°C", + 63: "°K", + 64: "°F", + 65: "Degree-days-Celsius", + 66: "Degree-days-Fahrenheit", + 67: "Years", + 68: "Months", + 69: "Weeks", + 70: "Days", + 71: "Hours", + 72: "Minutes", + 73: "Seconds", + 74: "Meters-per-second", + 75: "Kilometers-per-hour", + 76: "Feet-per-second", + 77: "Feet-per-minute", + 78: "Miles-per-hour", + 79: "Cubic-feet", + 80: "Cubic-meters", + 81: "Imperial-gallons", + 82: "Liters", + 83: "Us-gallons", + 84: "Cubic-feet-per-minute", + 85: "Cubic-meters-per-second", + 86: "Imperial-gallons-per-minute", + 87: "Liters-per-second", + 88: "Liters-per-minute", + 89: "Us-gallons-per-minute", + 90: "Degrees-angular", + 91: "Degrees-Celsius-per-hour", + 92: "Degrees-Celsius-per-minute", + 93: "Degrees-Fahrenheit-per-hour", + 94: "Degrees-Fahrenheit-per-minute", + 95: None, + 96: "Parts-per-million", + 97: "Parts-per-billion", + 98: "%", + 99: "Percent-per-second", + 100: "Per-minute", + 101: "Per-second", + 102: "Psi-per-Degree-Fahrenheit", + 103: "Radians", + 104: "Revolutions-per-minute", + 105: "Currency1", + 106: "Currency2", + 107: "Currency3", + 108: "Currency4", + 109: "Currency5", + 110: "Currency6", + 111: "Currency7", + 112: "Currency8", + 113: "Currency9", + 114: "Currency10", + 115: "Square-inches", + 116: "Square-centimeters", + 117: "BTUs-per-pound", + 118: "Centimeters", + 119: "Pounds-mass-per-second", + 120: "Delta-Degrees-Fahrenheit", + 121: "Delta-Degrees-Kelvin", + 122: "Kilohms", + 123: "Megohms", + 124: "Millivolts", + 125: "Kilojoules-per-kilogram", + 126: "Megajoules", + 127: "Joules-per-degree-Kelvin", + 128: "Joules-per-kilogram-degree-Kelvin", + 129: "Kilohertz", + 130: "Megahertz", + 131: "Per-hour", + 132: "Milliwatts", + 133: "Hectopascals", + 134: "Millibars", + 135: "Cubic-meters-per-hour", + 136: "Liters-per-hour", + 137: "Kilowatt-hours-per-square-meter", + 138: "Kilowatt-hours-per-square-foot", + 139: "Megajoules-per-square-meter", + 140: "Megajoules-per-square-foot", + 141: "Watts-per-square-meter-Degree-Kelvin", + 142: "Cubic-feet-per-second", + 143: "Percent-obscuration-per-foot", + 144: "Percent-obscuration-per-meter", + 145: "Milliohms", + 146: "Megawatt-hours", + 147: "Kilo-BTUs", + 148: "Mega-BTUs", + 149: "Kilojoules-per-kilogram-dry-air", + 150: "Megajoules-per-kilogram-dry-air", + 151: "Kilojoules-per-degree-Kelvin", + 152: "Megajoules-per-degree-Kelvin", + 153: "Newton", + 154: "Grams-per-second", + 155: "Grams-per-minute", + 156: "Tons-per-hour", + 157: "Kilo-BTUs-per-hour", + 158: "Hundredths-seconds", + 159: "Milliseconds", + 160: "Newton-meters", + 161: "Millimeters-per-second", + 162: "Millimeters-per-minute", + 163: "Meters-per-minute", + 164: "Meters-per-hour", + 165: "Cubic-meters-per-minute", + 166: "Meters-per-second-per-second", + 167: "Amperes-per-meter", + 168: "Amperes-per-square-meter", + 169: "Ampere-square-meters", + 170: "Farads", + 171: "Henrys", + 172: "Ohm-meters", + 173: "Siemens", + 174: "Siemens-per-meter", + 175: "Teslas", + 176: "Volts-per-degree-Kelvin", + 177: "Volts-per-meter", + 178: "Webers", + 179: "Candelas", + 180: "Candelas-per-square-meter", + 181: "Kelvins-per-hour", + 182: "Kelvins-per-minute", + 183: "Joule-seconds", + 185: "Square-meters-per-Newton", + 186: "Kilogram-per-cubic-meter", + 187: "Newton-seconds", + 188: "Newtons-per-meter", + 189: "Watts-per-meter-per-degree-Kelvin", +} + +ICONS = { + 0: "mdi:temperature-celsius", + 1: "mdi:water-percent", + 2: "mdi:gauge", + 3: "mdi:speedometer", + 4: "mdi:percent", + 5: "mdi:air-filter", + 6: "mdi:fan", + 7: "mdi:flash", + 8: "mdi:current-ac", + 9: "mdi:flash", + 10: "mdi:flash", + 11: "mdi:flash", + 12: "mdi:counter", + 13: "mdi:thermometer-lines", + 14: "mdi:timer", +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Zigbee Home Automation Analog Output from config entry.""" + entities_to_create = hass.data[DATA_ZHA][DOMAIN] + + unsub = async_dispatcher_connect( + hass, + SIGNAL_ADD_ENTITIES, + functools.partial( + discovery.async_add_entities, + async_add_entities, + entities_to_create, + update_before_add=False, + ), + ) + hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) + + +@STRICT_MATCH(channel_names=CHANNEL_ANALOG_OUTPUT) +class ZhaNumber(ZhaEntity, NumberEntity): + """Representation of a ZHA Number entity.""" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Init this entity.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._analog_output_channel = self.cluster_channels.get(CHANNEL_ANALOG_OUTPUT) + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + self.async_accept_signal( + self._analog_output_channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) + + @property + def value(self): + """Return the current value.""" + return self._analog_output_channel.present_value + + @property + def min_value(self): + """Return the minimum value.""" + min_present_value = self._analog_output_channel.min_present_value + if min_present_value is not None: + return min_present_value + return 0 + + @property + def max_value(self): + """Return the maximum value.""" + max_present_value = self._analog_output_channel.max_present_value + if max_present_value is not None: + return max_present_value + return 1023 + + @property + def step(self): + """Return the value step.""" + resolution = self._analog_output_channel.resolution + if resolution is not None: + return resolution + return super().step + + @property + def name(self): + """Return the name of the number entity.""" + description = self._analog_output_channel.description + if description is not None and len(description) > 0: + return f"{super().name} {description}" + return super().name + + @property + def icon(self): + """Return the icon to be used for this entity.""" + application_type = self._analog_output_channel.application_type + if application_type is not None: + return ICONS.get(application_type >> 16, super().icon) + return super().icon + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + engineering_units = self._analog_output_channel.engineering_units + return UNITS.get(engineering_units) + + @callback + def async_set_state(self, attr_id, attr_name, value): + """Handle value update from channel.""" + self.async_write_ha_state() + + async def async_set_value(self, value): + """Update the current value from HA.""" + num_value = float(value) + if await self._analog_output_channel.async_set_present_value(num_value): + self.async_write_ha_state() + + async def async_update(self): + """Attempt to retrieve the state of the entity.""" + await super().async_update() + _LOGGER.debug("polling current state") + if self._analog_output_channel: + value = await self._analog_output_channel.get_attribute_value( + "present_value", from_cache=False + ) + _LOGGER.debug("read value=%s", value) diff --git a/homeassistant/components/zha/translations/zh-Hans.json b/homeassistant/components/zha/translations/zh-Hans.json index a594ffe354..1dd51cd7e6 100644 --- a/homeassistant/components/zha/translations/zh-Hans.json +++ b/homeassistant/components/zha/translations/zh-Hans.json @@ -7,34 +7,78 @@ "cannot_connect": "\u65e0\u6cd5\u8fde\u63a5\u5230 ZHA \u8bbe\u5907\u3002" }, "step": { + "pick_radio": { + "data": { + "radio_type": "\u65e0\u7ebf\u7535\u7c7b\u578b" + }, + "description": "\u8bf7\u9009\u62e9 Zigbee \u65e0\u7ebf\u7535\u7c7b\u578b", + "title": "\u65e0\u7ebf\u7535\u7c7b\u578b" + }, + "port_config": { + "data": { + "baudrate": "\u6ce2\u7279\u7387", + "flow_control": "\u6570\u636e\u6d41\u63a7\u5236", + "path": "\u4e32\u884c\u8bbe\u5907\u8def\u5f84" + }, + "description": "\u8f93\u5165\u7aef\u53e3\u7684\u7279\u5b9a\u8bbe\u7f6e", + "title": "\u8bbe\u7f6e" + }, "user": { + "data": { + "path": "\u4e32\u884c\u8bbe\u5907\u8def\u5f84" + }, + "description": "\u9009\u62e9 Zigbee \u7684\u4e32\u884c\u7aef\u53e3", "title": "ZHA" } } }, "device_automation": { "action_type": { - "warn": "\u8b66\u544a" + "squawk": "\u54cd\u94c3", + "warn": "\u544a\u8b66" }, "trigger_subtype": { - "both_buttons": "\u4e24\u4e2a\u6309\u94ae", - "button_1": "\u7b2c\u4e00\u4e2a\u6309\u94ae", - "button_2": "\u7b2c\u4e8c\u4e2a\u6309\u94ae", - "button_3": "\u7b2c\u4e09\u4e2a\u6309\u94ae", - "button_4": "\u7b2c\u56db\u4e2a\u6309\u94ae", - "button_5": "\u7b2c\u4e94\u4e2a\u6309\u94ae", - "button_6": "\u7b2c\u516d\u4e2a\u6309\u94ae", + "both_buttons": "\u4e24\u952e\u540c\u65f6", + "button_1": "\u7b2c\u4e00\u952e", + "button_2": "\u7b2c\u4e8c\u952e", + "button_3": "\u7b2c\u4e09\u952e", + "button_4": "\u7b2c\u56db\u952e", + "button_5": "\u7b2c\u4e94\u952e", + "button_6": "\u7b2c\u516d\u952e", + "close": "\u5173\u95ed", "dim_down": "\u8c03\u6697", "dim_up": "\u8c03\u4eae", "left": "\u5de6", "open": "\u5f00\u542f", "right": "\u53f3", - "turn_off": "\u5173\u95ed" + "turn_off": "\u5173\u95ed", + "turn_on": "\u5f00\u542f" }, "trigger_type": { + "device_dropped": "\u8bbe\u5907\u81ea\u7531\u843d\u4f53", + "device_flipped": "\u8bbe\u5907\u7ffb\u8f6c \"{subtype}\"", + "device_knocked": "\u8bbe\u5907\u8f7b\u6572 \"{subtype}\"", "device_offline": "\u8bbe\u5907\u79bb\u7ebf", - "device_tilted": "\u8bbe\u5907\u540d\u79f0", - "remote_button_short_press": "\"{subtype}\" \u6309\u94ae\u5df2\u6309\u4e0b" + "device_rotated": "\u8bbe\u5907\u65cb\u8f6c \"{subtype}\"", + "device_shaken": "\u8bbe\u5907\u6447\u4e00\u6447", + "device_slid": "\u8bbe\u5907\u5e73\u79fb \"{subtype}\"", + "device_tilted": "\u8bbe\u5907\u503e\u659c", + "remote_button_alt_double_press": "\"{subtype}\" \u53cc\u51fb(\u5907\u7528)", + "remote_button_alt_long_press": "\"{subtype}\" \u957f\u6309(\u5907\u7528)", + "remote_button_alt_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00(\u5907\u7528)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \u56db\u8fde\u51fb(\u5907\u7528)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \u4e94\u8fde\u51fb(\u5907\u7528)", + "remote_button_alt_short_press": "\"{subtype}\" \u5355\u51fb(\u5907\u7528)", + "remote_button_alt_short_release": "\"{subtype}\" \u677e\u5f00(\u5907\u7528)", + "remote_button_alt_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb(\u5907\u7528)", + "remote_button_double_press": "\"{subtype}\" \u53cc\u51fb", + "remote_button_long_press": "\"{subtype}\" \u957f\u6309", + "remote_button_long_release": "\"{subtype}\" \u957f\u6309\u540e\u677e\u5f00", + "remote_button_quadruple_press": "\"{subtype}\" \u56db\u8fde\u51fb", + "remote_button_quintuple_press": "\"{subtype}\" \u4e94\u8fde\u51fb", + "remote_button_short_press": "\"{subtype}\" \u5355\u51fb", + "remote_button_short_release": "\"{subtype}\" \u677e\u5f00", + "remote_button_triple_press": "\"{subtype}\" \u4e09\u8fde\u51fb" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index e62d61ac8e..6582507431 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" @@ -18,14 +18,14 @@ "data": { "baudrate": "\u901a\u8a0a\u57e0\u901f\u5ea6", "flow_control": "\u8cc7\u6599\u6d41\u91cf\u63a7\u5236", - "path": "\u5e8f\u5217\u8a2d\u5099\u8def\u5f91" + "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" }, "description": "\u8f38\u5165\u901a\u8a0a\u57e0\u7279\u5b9a\u8a2d\u5b9a", "title": "\u8a2d\u5b9a" }, "user": { "data": { - "path": "\u5e8f\u5217\u8a2d\u5099\u8def\u5f91" + "path": "\u5e8f\u5217\u88dd\u7f6e\u8def\u5f91" }, "description": "\u9078\u64c7 Zigbee \u7121\u7dda\u96fb\u5e8f\u5217\u57e0", "title": "ZHA" @@ -62,14 +62,14 @@ "turn_on": "\u958b\u555f" }, "trigger_type": { - "device_dropped": "\u8a2d\u5099\u6389\u843d", - "device_flipped": "\u7ffb\u52d5 \"{subtype}\" \u8a2d\u5099", - "device_knocked": "\u6572\u64ca \"{subtype}\" \u8a2d\u5099", - "device_offline": "\u8a2d\u5099\u96e2\u7dda", - "device_rotated": "\u65cb\u8f49 \"{subtype}\" \u8a2d\u5099", - "device_shaken": "\u8a2d\u5099\u6416\u6643", - "device_slid": "\u63a8\u52d5 \"{subtype}\" \u8a2d\u5099", - "device_tilted": "\u8a2d\u5099\u540d\u7a31", + "device_dropped": "\u88dd\u7f6e\u6389\u843d", + "device_flipped": "\u7ffb\u52d5 \"{subtype}\" \u88dd\u7f6e", + "device_knocked": "\u6572\u64ca \"{subtype}\" \u88dd\u7f6e", + "device_offline": "\u88dd\u7f6e\u96e2\u7dda", + "device_rotated": "\u65cb\u8f49 \"{subtype}\" \u88dd\u7f6e", + "device_shaken": "\u88dd\u7f6e\u6416\u6643", + "device_slid": "\u63a8\u52d5 \"{subtype}\" \u88dd\u7f6e", + "device_tilted": "\u88dd\u7f6e\u540d\u7a31", "remote_button_alt_double_press": "\"{subtype}\" \u6309\u9215\u96d9\u64ca\u9375\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", "remote_button_alt_long_press": "\"{subtype}\" \u6309\u9215\u6301\u7e8c\u6309\u4e0b\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", "remote_button_alt_long_release": "\"{subtype}\" \u6309\u9215\u9577\u6309\u5f8c\u91cb\u653e\uff08\u66ff\u4ee3\u6a21\u5f0f\uff09", diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index b3a87510e5..039513f100 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -2,6 +2,6 @@ "domain": "zoneminder", "name": "ZoneMinder", "documentation": "https://www.home-assistant.io/integrations/zoneminder", - "requirements": ["zm-py==0.4.0"], + "requirements": ["zm-py==0.5.2"], "codeowners": ["@rohankapoorcom"] } diff --git a/homeassistant/components/zoneminder/translations/no.json b/homeassistant/components/zoneminder/translations/no.json index 50a9b5fedb..b40ea7917f 100644 --- a/homeassistant/components/zoneminder/translations/no.json +++ b/homeassistant/components/zoneminder/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "auth_fail": "Brukernavn eller passord er feil.", + "auth_fail": "Brukernavn eller passord er feil", "cannot_connect": "Tilkobling mislyktes", "connection_error": "Kunne ikke koble til en ZoneMinder-server.", "invalid_auth": "Ugyldig godkjenning" @@ -10,7 +10,7 @@ "default": "ZoneMinder-serveren er lagt til." }, "error": { - "auth_fail": "Brukernavn eller passord er feil.", + "auth_fail": "Brukernavn eller passord er feil", "cannot_connect": "Tilkobling mislyktes", "connection_error": "Kunne ikke koble til en ZoneMinder-server.", "invalid_auth": "Ugyldig godkjenning" diff --git a/homeassistant/components/zoneminder/translations/pt.json b/homeassistant/components/zoneminder/translations/pt.json new file mode 100644 index 0000000000..f8fa0efe96 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/pt.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "ssl": "Utiliza um certificado SSL", + "username": "Nome de Utilizador", + "verify_ssl": "Verificar o certificado SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/translations/pt.json b/homeassistant/components/zwave/translations/pt.json index 49be02c195..7494208188 100644 --- a/homeassistant/components/zwave/translations/pt.json +++ b/homeassistant/components/zwave/translations/pt.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado" + "already_configured": "O Z-Wave j\u00e1 est\u00e1 configurado", + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." }, "error": { "option_error": "A valida\u00e7\u00e3o Z-Wave falhou. O caminho para o dispositivo USB est\u00e1 correto?" diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index fdb263fd5f..f5c07a9efc 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" }, "error": { "option_error": "Z-Wave \u9a57\u8b49\u5931\u6557\uff0c\u8acb\u78ba\u5b9a USB \u96a8\u8eab\u789f\u8def\u5f91\u6b63\u78ba\uff1f" @@ -11,7 +11,7 @@ "user": { "data": { "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", - "usb_path": "USB \u8a2d\u5099\u8def\u5f91" + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, "description": "\u95dc\u65bc\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/", "title": "\u8a2d\u5b9a Z-Wave" diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index af82db0ffb..601ce1efbf 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -13,12 +13,12 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.event import Event +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.setup import async_process_deps_reqs, async_setup_component from homeassistant.util.decorator import Registry import homeassistant.util.uuid as uuid_util _LOGGER = logging.getLogger(__name__) -_UNDEF: dict = {} SOURCE_DISCOVERY = "discovery" SOURCE_HASSIO = "hassio" @@ -760,12 +760,11 @@ class ConfigEntries: self, entry: ConfigEntry, *, - # pylint: disable=dangerous-default-value # _UNDEFs not modified - unique_id: Union[str, dict, None] = _UNDEF, - title: Union[str, dict] = _UNDEF, - data: dict = _UNDEF, - options: dict = _UNDEF, - system_options: dict = _UNDEF, + unique_id: Union[str, dict, None, UndefinedType] = UNDEFINED, + title: Union[str, dict, UndefinedType] = UNDEFINED, + data: Union[dict, UndefinedType] = UNDEFINED, + options: Union[dict, UndefinedType] = UNDEFINED, + system_options: Union[dict, UndefinedType] = UNDEFINED, ) -> bool: """Update a config entry. @@ -777,24 +776,24 @@ class ConfigEntries: """ changed = False - if unique_id is not _UNDEF and entry.unique_id != unique_id: + if unique_id is not UNDEFINED and entry.unique_id != unique_id: changed = True entry.unique_id = cast(Optional[str], unique_id) - if title is not _UNDEF and entry.title != title: + if title is not UNDEFINED and entry.title != title: changed = True entry.title = cast(str, title) - if data is not _UNDEF and entry.data != data: # type: ignore + if data is not UNDEFINED and entry.data != data: # type: ignore changed = True entry.data = MappingProxyType(data) - if options is not _UNDEF and entry.options != options: # type: ignore + if options is not UNDEFINED and entry.options != options: # type: ignore changed = True entry.options = MappingProxyType(options) if ( - system_options is not _UNDEF + system_options is not UNDEFINED and entry.system_options.as_dict() != system_options ): changed = True @@ -911,6 +910,9 @@ class ConfigFlow(data_entry_flow.FlowHandler): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) ) + # Allow ignored entries to be configured on manual user step + if entry.source == SOURCE_IGNORE and self.source == SOURCE_USER: + continue raise data_entry_flow.AbortFlow("already_configured") async def async_set_unique_id( diff --git a/homeassistant/const.py b/homeassistant/const.py index ac4f07a708..4baa7e7bc3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" -MAJOR_VERSION = 2020 -MINOR_VERSION = 12 -PATCH_VERSION = "2" +MAJOR_VERSION = 2021 +MINOR_VERSION = 1 +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 1) diff --git a/homeassistant/core.py b/homeassistant/core.py index 9eeaf6fccc..6b657f600d 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -89,7 +89,7 @@ block_async_io.enable() fix_threading_exception_logging() T = TypeVar("T") -_UNDEF: dict = {} +_UNDEF: dict = {} # Internal; not helpers.typing.UNDEFINED due to circular dependency # pylint: disable=invalid-name CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) CALLBACK_TYPE = Callable[[], None] @@ -863,7 +863,7 @@ class State: if not valid_state(state): raise InvalidStateError( - f"Invalid state encountered for entity id: {entity_id}. " + f"Invalid state encountered for entity ID: {entity_id}. " "State max length is 255 characters." ) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 833f11190b..9e204e91da 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -23,12 +23,12 @@ FLOWS = [ "atag", "august", "aurora", - "avri", "awair", "axis", "azure_devops", "blebox", "blink", + "bmw_connected_drive", "bond", "braviatv", "broadlink", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 6efa44e304..49527666f5 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -116,7 +116,7 @@ ZEROCONF = { "_printer._tcp.local.": [ { "domain": "brother", - "name": "Brother*" + "name": "brother*" } ], "_spotify-connect._tcp.local.": [ @@ -157,10 +157,14 @@ ZEROCONF = { } HOMEKIT = { + "3810X": "roku", + "4660X": "roku", + "7820X": "roku", "819LMB": "myq", "AC02": "tado", "Abode": "abode", "BSB002": "hue", + "C105X": "roku", "Healty Home Coach": "netatmo", "Iota": "abode", "LIFX": "lifx", diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 526a774cc3..c4d7de3839 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -191,6 +191,13 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): data["client_secret"] = self.client_secret resp = await session.post(self.token_url, data=data) + if resp.status >= 400 and _LOGGER.isEnabledFor(logging.DEBUG): + body = await resp.text() + _LOGGER.debug( + "Token request failed with status=%s, body=%s", + resp.status, + body, + ) resp.raise_for_status() return cast(dict, await resp.json()) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index cc8f9a1782..6e8c09bbd6 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -11,7 +11,7 @@ import homeassistant.util.uuid as uuid_util from .debounce import Debouncer from .singleton import singleton -from .typing import HomeAssistantType +from .typing import UNDEFINED, HomeAssistantType if TYPE_CHECKING: from . import entity_registry @@ -19,7 +19,6 @@ if TYPE_CHECKING: # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) -_UNDEF = object() DATA_REGISTRY = "device_registry" EVENT_DEVICE_REGISTRY_UPDATED = "device_registry_updated" @@ -224,17 +223,17 @@ class DeviceRegistry: config_entry_id, connections=None, identifiers=None, - manufacturer=_UNDEF, - model=_UNDEF, - name=_UNDEF, - default_manufacturer=_UNDEF, - default_model=_UNDEF, - default_name=_UNDEF, - sw_version=_UNDEF, - entry_type=_UNDEF, + manufacturer=UNDEFINED, + model=UNDEFINED, + name=UNDEFINED, + default_manufacturer=UNDEFINED, + default_model=UNDEFINED, + default_name=UNDEFINED, + sw_version=UNDEFINED, + entry_type=UNDEFINED, via_device=None, # To disable a device if it gets created - disabled_by=_UNDEF, + disabled_by=UNDEFINED, ): """Get device. Create if it doesn't exist.""" if not identifiers and not connections: @@ -261,27 +260,27 @@ class DeviceRegistry: ) self._add_device(device) - if default_manufacturer is not _UNDEF and device.manufacturer is None: + if default_manufacturer is not UNDEFINED and device.manufacturer is None: manufacturer = default_manufacturer - if default_model is not _UNDEF and device.model is None: + if default_model is not UNDEFINED and device.model is None: model = default_model - if default_name is not _UNDEF and device.name is None: + if default_name is not UNDEFINED and device.name is None: name = default_name if via_device is not None: via = self.async_get_device({via_device}, set()) - via_device_id = via.id if via else _UNDEF + via_device_id = via.id if via else UNDEFINED else: - via_device_id = _UNDEF + via_device_id = UNDEFINED return self._async_update_device( device.id, add_config_entry_id=config_entry_id, via_device_id=via_device_id, - merge_connections=connections or _UNDEF, - merge_identifiers=identifiers or _UNDEF, + merge_connections=connections or UNDEFINED, + merge_identifiers=identifiers or UNDEFINED, manufacturer=manufacturer, model=model, name=name, @@ -295,16 +294,16 @@ class DeviceRegistry: self, device_id, *, - area_id=_UNDEF, - manufacturer=_UNDEF, - model=_UNDEF, - name=_UNDEF, - name_by_user=_UNDEF, - new_identifiers=_UNDEF, - sw_version=_UNDEF, - via_device_id=_UNDEF, - remove_config_entry_id=_UNDEF, - disabled_by=_UNDEF, + area_id=UNDEFINED, + manufacturer=UNDEFINED, + model=UNDEFINED, + name=UNDEFINED, + name_by_user=UNDEFINED, + new_identifiers=UNDEFINED, + sw_version=UNDEFINED, + via_device_id=UNDEFINED, + remove_config_entry_id=UNDEFINED, + disabled_by=UNDEFINED, ): """Update properties of a device.""" return self._async_update_device( @@ -326,20 +325,20 @@ class DeviceRegistry: self, device_id, *, - add_config_entry_id=_UNDEF, - remove_config_entry_id=_UNDEF, - merge_connections=_UNDEF, - merge_identifiers=_UNDEF, - new_identifiers=_UNDEF, - manufacturer=_UNDEF, - model=_UNDEF, - name=_UNDEF, - sw_version=_UNDEF, - entry_type=_UNDEF, - via_device_id=_UNDEF, - area_id=_UNDEF, - name_by_user=_UNDEF, - disabled_by=_UNDEF, + add_config_entry_id=UNDEFINED, + remove_config_entry_id=UNDEFINED, + merge_connections=UNDEFINED, + merge_identifiers=UNDEFINED, + new_identifiers=UNDEFINED, + manufacturer=UNDEFINED, + model=UNDEFINED, + name=UNDEFINED, + sw_version=UNDEFINED, + entry_type=UNDEFINED, + via_device_id=UNDEFINED, + area_id=UNDEFINED, + name_by_user=UNDEFINED, + disabled_by=UNDEFINED, ): """Update device attributes.""" old = self.devices[device_id] @@ -349,13 +348,13 @@ class DeviceRegistry: config_entries = old.config_entries if ( - add_config_entry_id is not _UNDEF + add_config_entry_id is not UNDEFINED and add_config_entry_id not in old.config_entries ): config_entries = old.config_entries | {add_config_entry_id} if ( - remove_config_entry_id is not _UNDEF + remove_config_entry_id is not UNDEFINED and remove_config_entry_id in config_entries ): if config_entries == {remove_config_entry_id}: @@ -373,10 +372,10 @@ class DeviceRegistry: ): old_value = getattr(old, attr_name) # If not undefined, check if `value` contains new items. - if value is not _UNDEF and not value.issubset(old_value): + if value is not UNDEFINED and not value.issubset(old_value): changes[attr_name] = old_value | value - if new_identifiers is not _UNDEF: + if new_identifiers is not UNDEFINED: changes["identifiers"] = new_identifiers for attr_name, value in ( @@ -388,13 +387,13 @@ class DeviceRegistry: ("via_device_id", via_device_id), ("disabled_by", disabled_by), ): - if value is not _UNDEF and value != getattr(old, attr_name): + if value is not UNDEFINED and value != getattr(old, attr_name): changes[attr_name] = value - if area_id is not _UNDEF and area_id != old.area_id: + if area_id is not UNDEFINED and area_id != old.area_id: changes["area_id"] = area_id - if name_by_user is not _UNDEF and name_by_user != old.name_by_user: + if name_by_user is not UNDEFINED and name_by_user != old.name_by_user: changes["name_by_user"] = name_by_user if old.is_new: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ddd1847f6a..7b38c10225 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -464,7 +464,7 @@ class EntityPlatform: # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): entity.add_to_platform_abort() - raise HomeAssistantError(f"Invalid entity id: {entity.entity_id}") + raise HomeAssistantError(f"Invalid entity ID: {entity.entity_id}") already_exists = entity.entity_id in self.entities restored = False diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 4582fc5f3b..44f5c9c56f 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -39,7 +39,7 @@ from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml from .singleton import singleton -from .typing import HomeAssistantType +from .typing import UNDEFINED, HomeAssistantType if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry # noqa: F401 @@ -51,7 +51,6 @@ DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) -_UNDEF = object() DISABLED_CONFIG_ENTRY = "config_entry" DISABLED_DEVICE = "device" DISABLED_HASS = "hass" @@ -225,15 +224,15 @@ class EntityRegistry: if entity_id: return self._async_update_entity( # type: ignore entity_id, - config_entry_id=config_entry_id or _UNDEF, - device_id=device_id or _UNDEF, - area_id=area_id or _UNDEF, - capabilities=capabilities or _UNDEF, - supported_features=supported_features or _UNDEF, - device_class=device_class or _UNDEF, - unit_of_measurement=unit_of_measurement or _UNDEF, - original_name=original_name or _UNDEF, - original_icon=original_icon or _UNDEF, + config_entry_id=config_entry_id or UNDEFINED, + device_id=device_id or UNDEFINED, + area_id=area_id or UNDEFINED, + capabilities=capabilities or UNDEFINED, + supported_features=supported_features or UNDEFINED, + device_class=device_class or UNDEFINED, + unit_of_measurement=unit_of_measurement or UNDEFINED, + original_name=original_name or UNDEFINED, + original_icon=original_icon or UNDEFINED, # When we changed our slugify algorithm, we invalidated some # stored entity IDs with either a __ or ending in _. # Fix introduced in 0.86 (Jan 23, 2019). Next line can be @@ -333,12 +332,12 @@ class EntityRegistry: self, entity_id, *, - name=_UNDEF, - icon=_UNDEF, - area_id=_UNDEF, - new_entity_id=_UNDEF, - new_unique_id=_UNDEF, - disabled_by=_UNDEF, + name=UNDEFINED, + icon=UNDEFINED, + area_id=UNDEFINED, + new_entity_id=UNDEFINED, + new_unique_id=UNDEFINED, + disabled_by=UNDEFINED, ): """Update properties of an entity.""" return cast( # cast until we have _async_update_entity type hinted @@ -359,20 +358,20 @@ class EntityRegistry: self, entity_id, *, - name=_UNDEF, - icon=_UNDEF, - config_entry_id=_UNDEF, - new_entity_id=_UNDEF, - device_id=_UNDEF, - area_id=_UNDEF, - new_unique_id=_UNDEF, - disabled_by=_UNDEF, - capabilities=_UNDEF, - supported_features=_UNDEF, - device_class=_UNDEF, - unit_of_measurement=_UNDEF, - original_name=_UNDEF, - original_icon=_UNDEF, + name=UNDEFINED, + icon=UNDEFINED, + config_entry_id=UNDEFINED, + new_entity_id=UNDEFINED, + device_id=UNDEFINED, + area_id=UNDEFINED, + new_unique_id=UNDEFINED, + disabled_by=UNDEFINED, + capabilities=UNDEFINED, + supported_features=UNDEFINED, + device_class=UNDEFINED, + unit_of_measurement=UNDEFINED, + original_name=UNDEFINED, + original_icon=UNDEFINED, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -393,10 +392,10 @@ class EntityRegistry: ("original_name", original_name), ("original_icon", original_icon), ): - if value is not _UNDEF and value != getattr(old, attr_name): + if value is not UNDEFINED and value != getattr(old, attr_name): changes[attr_name] = value - if new_entity_id is not _UNDEF and new_entity_id != old.entity_id: + if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id: if self.async_is_registered(new_entity_id): raise ValueError("Entity is already registered") @@ -409,7 +408,7 @@ class EntityRegistry: self.entities.pop(entity_id) entity_id = changes["entity_id"] = new_entity_id - if new_unique_id is not _UNDEF: + if new_unique_id is not UNDEFINED: conflict_entity_id = self.async_get_entity_id( old.domain, old.platform, new_unique_id ) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 661e1a11b5..f06ac8aca3 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -715,9 +715,9 @@ def async_track_template( hass.async_run_hass_job( job, - event.data.get("entity_id"), - event.data.get("old_state"), - event.data.get("new_state"), + event and event.data.get("entity_id"), + event and event.data.get("old_state"), + event and event.data.get("new_state"), ) info = async_track_template_result( diff --git a/homeassistant/helpers/httpx_client.py b/homeassistant/helpers/httpx_client.py new file mode 100644 index 0000000000..0f1719b388 --- /dev/null +++ b/homeassistant/helpers/httpx_client.py @@ -0,0 +1,88 @@ +"""Helper for httpx.""" +import sys +from typing import Any, Callable, Optional + +import httpx + +from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.core import Event, callback +from homeassistant.helpers.frame import warn_use +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import bind_hass + +DATA_ASYNC_CLIENT = "httpx_async_client" +DATA_ASYNC_CLIENT_NOVERIFY = "httpx_async_client_noverify" +SERVER_SOFTWARE = "HomeAssistant/{0} httpx/{1} Python/{2[0]}.{2[1]}".format( + __version__, httpx.__version__, sys.version_info +) +USER_AGENT = "User-Agent" + + +@callback +@bind_hass +def get_async_client( + hass: HomeAssistantType, verify_ssl: bool = True +) -> httpx.AsyncClient: + """Return default httpx AsyncClient. + + This method must be run in the event loop. + """ + key = DATA_ASYNC_CLIENT if verify_ssl else DATA_ASYNC_CLIENT_NOVERIFY + + client: Optional[httpx.AsyncClient] = hass.data.get(key) + + if client is None: + client = hass.data[key] = create_async_httpx_client(hass, verify_ssl) + + return client + + +@callback +def create_async_httpx_client( + hass: HomeAssistantType, + verify_ssl: bool = True, + auto_cleanup: bool = True, + **kwargs: Any, +) -> httpx.AsyncClient: + """Create a new httpx.AsyncClient with kwargs, i.e. for cookies. + + If auto_cleanup is False, the client will be + automatically closed on homeassistant_stop. + + This method must be run in the event loop. + """ + + client = httpx.AsyncClient( + verify=verify_ssl, + headers={USER_AGENT: SERVER_SOFTWARE}, + **kwargs, + ) + + original_aclose = client.aclose + + client.aclose = warn_use( # type: ignore + client.aclose, "closes the Home Assistant httpx client" + ) + + if auto_cleanup: + _async_register_async_client_shutdown(hass, client, original_aclose) + + return client + + +@callback +def _async_register_async_client_shutdown( + hass: HomeAssistantType, + client: httpx.AsyncClient, + original_aclose: Callable[..., Any], +) -> None: + """Register httpx AsyncClient aclose on Home Assistant shutdown. + + This method must be run in the event loop. + """ + + async def _async_close_client(event: Event) -> None: + """Close httpx client.""" + await original_aclose() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_client) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 48a662e3a8..77c842a27f 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -62,7 +62,11 @@ from homeassistant.core import ( callback, ) from homeassistant.helpers import condition, config_validation as cv, service, template -from homeassistant.helpers.event import async_call_later, async_track_template +from homeassistant.helpers.event import ( + TrackTemplate, + async_call_later, + async_track_template_result, +) from homeassistant.helpers.script_variables import ScriptVariables from homeassistant.helpers.trigger import ( async_initialize_triggers, @@ -355,7 +359,7 @@ class _ScriptRun: return @callback - def async_script_wait(entity_id, from_s, to_s): + def _async_script_wait(event, updates): """Handle script after template condition is true.""" self._variables["wait"] = { "remaining": to_context.remaining if to_context else delay, @@ -364,9 +368,12 @@ class _ScriptRun: done.set() to_context = None - unsub = async_track_template( - self._hass, wait_template, async_script_wait, self._variables + info = async_track_template_result( + self._hass, + [TrackTemplate(wait_template, self._variables)], + _async_script_wait, ) + unsub = info.async_remove self._changed() done = asyncio.Event() diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index bed0d2b8d1..279bc0f686 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -1,4 +1,5 @@ """Typing Helpers for Home Assistant.""" +from enum import Enum from typing import Any, Dict, Mapping, Optional, Tuple, Union import homeassistant.core @@ -16,3 +17,12 @@ TemplateVarsType = Optional[Mapping[str, Any]] # Custom type for recorder Queries QueryType = Any + + +class UndefinedType(Enum): + """Singleton type for use with not set sentinel values.""" + + _singleton = 0 + + +UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 6dabfdf044..ba29ff4a8d 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -48,7 +48,7 @@ CUSTOM_WARNING = ( "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant." ) -_UNDEF = object() +_UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency MAX_LOAD_CONCURRENTLY = 4 diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7aa59bd583..11e7dd8991 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 hass-nabucasa==0.39.0 -home-assistant-frontend==20201212.0 +home-assistant-frontend==20201229.1 httpx==0.16.1 importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 @@ -28,15 +28,18 @@ requests==2.25.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.20 voluptuous-serialize==2.4.0 -voluptuous==0.12.0 +voluptuous==0.12.1 yarl==1.4.2 -zeroconf==0.28.7 +zeroconf==0.28.8 pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 urllib3>=1.24.3 +# Constrain H11 to ensure we get a new enough version to support non-rfc line endings +h11>=0.12.0 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index b3af06ad07..cebfd95591 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -5,6 +5,7 @@ from typing import Any, Dict, Iterable, List, Optional, Set, Union, cast from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.loader import Integration, IntegrationNotFound, async_get_integration import homeassistant.util.package as pkg_util @@ -17,7 +18,6 @@ DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = { "ssdp": ("ssdp",), "zeroconf": ("zeroconf", "homekit"), } -_UNDEF = object() class RequirementsNotFound(HomeAssistantError): @@ -53,19 +53,21 @@ async def async_get_integration_with_requirements( if cache is None: cache = hass.data[DATA_INTEGRATIONS_WITH_REQS] = {} - int_or_evt: Union[Integration, asyncio.Event, None] = cache.get(domain, _UNDEF) + int_or_evt: Union[Integration, asyncio.Event, None, UndefinedType] = cache.get( + domain, UNDEFINED + ) if isinstance(int_or_evt, asyncio.Event): await int_or_evt.wait() - int_or_evt = cache.get(domain, _UNDEF) + int_or_evt = cache.get(domain, UNDEFINED) - # When we have waited and it's _UNDEF, it doesn't exist + # When we have waited and it's UNDEFINED, it doesn't exist # We don't cache that it doesn't exist, or else people can't fix it # and then restart, because their config will never be valid. - if int_or_evt is _UNDEF: + if int_or_evt is UNDEFINED: raise IntegrationNotFound(domain) - if int_or_evt is not _UNDEF: + if int_or_evt is not UNDEFINED: return cast(Integration, int_or_evt) event = cache[domain] = asyncio.Event() diff --git a/requirements.txt b/requirements.txt index ece1877ea7..cbe339fd83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,6 @@ pytz>=2020.1 pyyaml==5.3.1 requests==2.25.0 ruamel.yaml==0.15.100 -voluptuous==0.12.0 +voluptuous==0.12.1 voluptuous-serialize==2.4.0 yarl==1.4.2 diff --git a/requirements_all.txt b/requirements_all.txt index f1426cebca..8e3b891c2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -245,7 +245,7 @@ ambiclimate==0.2.1 amcrest==1.7.0 # homeassistant.components.androidtv -androidtv[async]==0.0.56 +androidtv[async]==0.0.57 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -305,9 +305,6 @@ av==8.0.2 # homeassistant.components.avion # avion==0.10 -# homeassistant.components.avri -avri-api==0.1.7 - # homeassistant.components.axis axis==41 @@ -413,7 +410,7 @@ caldav==0.6.1 circuit-webhook==1.0.1 # homeassistant.components.cisco_mobility_express -ciscomobilityexpress==0.3.3 +ciscomobilityexpress==0.3.9 # homeassistant.components.cppm_tracker clearpasspy==1.0.2 @@ -481,7 +478,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.8 +denonavr==0.9.9 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 @@ -559,7 +556,7 @@ env_canada==0.2.5 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.17.3 +envoy_reader==0.18.3 # homeassistant.components.season ephem==3.7.7.0 @@ -619,7 +616,7 @@ freesms==0.1.2 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -fritzconnection==1.3.4 +fritzconnection==1.4.0 # homeassistant.components.google_translate gTTS==2.2.1 @@ -684,7 +681,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.2.0 +google-nest-sdm==0.2.5 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -762,10 +759,10 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.3 +holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201212.0 +home-assistant-frontend==20201229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -774,7 +771,7 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.12.1 +homematicip==0.13.0 # homeassistant.components.horizon horimote==0.4.1 @@ -784,13 +781,13 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.12 +huawei-lte-api==1.4.17 # homeassistant.components.hydrawise hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.6.0 +hyperion-py==0.6.1 # homeassistant.components.bh1750 # homeassistant.components.bme280 @@ -810,7 +807,7 @@ ibm-watson==4.0.1 ibmiotf==0.3.4 # homeassistant.components.ping -icmplib==1.2.2 +icmplib==2.0 # homeassistant.components.iglo iglo==1.2.7 @@ -934,7 +931,7 @@ messagebird==1.2.0 meteoalertapi==0.1.6 # homeassistant.components.meteo_france -meteofrance-api==0.1.1 +meteofrance-api==1.0.1 # homeassistant.components.mfi mficlient==0.3.0 @@ -952,7 +949,7 @@ minio==4.0.9 mitemp_bt==0.0.3 # homeassistant.components.motion_blinds -motionblinds==0.1.6 +motionblinds==0.4.7 # homeassistant.components.tts mutagen==1.45.1 @@ -1061,7 +1058,7 @@ openhomedevice==0.7.2 opensensemap-api==0.1.5 # homeassistant.components.enigma2 -openwebifpy==3.1.1 +openwebifpy==3.2.7 # homeassistant.components.luci openwrt-luci-rpc==1.1.6 @@ -1149,9 +1146,6 @@ plumlightpad==0.0.11 # homeassistant.components.serial_pm pmsensor==0.4 -# homeassistant.components.pocketcasts -pocketcasts==0.1 - # homeassistant.components.poolsense poolsense==0.0.8 @@ -1174,7 +1168,7 @@ prometheus_client==0.7.1 proxmoxer==1.1.1 # homeassistant.components.systemmonitor -psutil==5.7.2 +psutil==5.8.0 # homeassistant.components.ptvsd ptvsd==4.3.2 @@ -1198,10 +1192,10 @@ pushover_complete==1.1.1 pwmled==1.6.7 # homeassistant.components.august -py-august==0.25.0 +py-august==0.25.2 # homeassistant.components.canary -py-canary==0.5.0 +py-canary==0.5.1 # homeassistant.components.cpuspeed py-cpuinfo==7.0.0 @@ -1295,7 +1289,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.17 +pybotvac==0.0.19 # homeassistant.components.nissan_leaf pycarwings2==2.10 @@ -1307,7 +1301,10 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.5.1 +pychromecast==7.6.0 + +# homeassistant.components.pocketcasts +pycketcasts==1.0.0 # homeassistant.components.cmus pycmus==0.1.1 @@ -1321,9 +1318,6 @@ pycomfoconnect==0.3 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 -# homeassistant.components.avri -pycountry==19.8.18 - # homeassistant.components.microsoft pycsspeechtts==1.0.4 @@ -1331,7 +1325,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.3.1 +pydaikin==2.4.0 # homeassistant.components.danfoss_air pydanfossair==0.1.0 @@ -1521,7 +1515,7 @@ pymediaroom==0.6.4.1 pymelcloud==2.5.2 # homeassistant.components.somfy -pymfy==0.9.1 +pymfy==0.9.3 # homeassistant.components.xiaomi_tv pymitv==1.4.3 @@ -1593,7 +1587,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.6b1 +pyotgw==1.0b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -1622,7 +1616,7 @@ pypoint==2.0.0 pyprof2calltree==1.4.5 # homeassistant.components.ps4 -pyps4-2ndscreen==1.1.1 +pyps4-2ndscreen==1.2.0 # homeassistant.components.qvr_pro pyqvrpro==0.52 @@ -1662,11 +1656,11 @@ pysensibo==1.0.3 # homeassistant.components.serial # homeassistant.components.zha -pyserial-asyncio==0.4 +pyserial-asyncio==0.5 # homeassistant.components.acer_projector # homeassistant.components.zha -pyserial==3.4 +pyserial==3.5 # homeassistant.components.sesame pysesame2==1.0.1 @@ -1744,7 +1738,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.7 +python-ecobee-api==0.2.8 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.11 @@ -1786,7 +1780,7 @@ python-juicenet==1.0.1 python-miio==0.5.4 # homeassistant.components.mpd -python-mpd2==1.0.0 +python-mpd2==3.0.1 # homeassistant.components.mystrom python-mystrom==1.1.2 @@ -1801,7 +1795,7 @@ python-nmap==0.6.1 python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.qbittorrent -python-qbittorrent==0.4.1 +python-qbittorrent==0.4.2 # homeassistant.components.ripple python-ripple-api==0.0.3 @@ -1816,7 +1810,7 @@ python-songpal==0.12 python-tado==0.8.1 # homeassistant.components.telegram_bot -python-telegram-bot==11.1.0 +python-telegram-bot==13.1 # homeassistant.components.vlc_telnet python-telnet-vlc==1.0.4 @@ -1858,7 +1852,7 @@ pytraccar==0.9.0 pytrackr==0.0.5 # homeassistant.components.tradfri -pytradfri[async]==7.0.5 +pytradfri[async]==7.0.6 # homeassistant.components.trafikverket_train # homeassistant.components.trafikverket_weatherstation @@ -1892,7 +1886,7 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.5.3 +pywemo==0.5.6 # homeassistant.components.wilight pywilight==0.0.65 @@ -1904,7 +1898,7 @@ pyxeoma==1.4.1 pyzbar==0.1.7 # homeassistant.components.zerproc -pyzerproc==0.2.5 +pyzerproc==0.4.7 # homeassistant.components.qnap qnapstats==0.3.0 @@ -1931,7 +1925,7 @@ raspyrfm-client==1.2.8 regenmaschine==3.0.0 # homeassistant.components.python_script -restrictedpython==5.0 +restrictedpython==5.1 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -2028,7 +2022,7 @@ simplisafe-python==9.6.2 sisyphus-control==3.0 # homeassistant.components.skybell -skybellpy==0.6.1 +skybellpy==0.6.3 # homeassistant.components.slack slackclient==2.5.0 @@ -2087,7 +2081,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.2 # homeassistant.components.spider -spiderpy==1.3.1 +spiderpy==1.4.2 # homeassistant.components.spotcrime spotcrime==1.0.4 @@ -2245,7 +2239,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.4.0 # homeassistant.components.venstar -venstarcolortouch==0.12 +venstarcolortouch==0.13 # homeassistant.components.vilfo vilfo-api-client==0.3.2 @@ -2333,7 +2327,7 @@ yeelight==0.5.4 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.11.12 +youtube_dl==2020.12.29 # homeassistant.components.onvif zeep[async]==4.0.0 @@ -2342,10 +2336,10 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.28.7 +zeroconf==0.28.8 # homeassistant.components.zha -zha-quirks==0.0.49 +zha-quirks==0.0.51 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2357,7 +2351,7 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.11.0 +zigpy-deconz==0.11.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -2369,7 +2363,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.2 +zigpy==0.29.0 # homeassistant.components.zoneminder -zm-py==0.4.0 +zm-py==0.5.2 diff --git a/requirements_test.txt b/requirements_test.txt index 8ec5a611f1..e4553a2498 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -24,6 +24,6 @@ pytest-xdist==2.1.0 pytest==6.1.2 requests_mock==1.8.0 responses==0.12.0 -respx==0.14.0 +respx==0.16.2 stdlib-list==0.7.0 tqdm==4.49.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bd9819319f..466c072f4a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -152,7 +152,7 @@ airly==1.0.0 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv[async]==0.0.56 +androidtv[async]==0.0.57 # homeassistant.components.apns apns2==0.3.0 @@ -176,9 +176,6 @@ auroranoaa==0.0.2 # homeassistant.components.stream av==8.0.2 -# homeassistant.components.avri -avri-api==0.1.7 - # homeassistant.components.axis axis==41 @@ -191,6 +188,9 @@ base36==0.1.1 # homeassistant.components.zha bellows==0.21.0 +# homeassistant.components.bmw_connected_drive +bimmer_connected==0.7.13 + # homeassistant.components.blebox blebox_uniapi==1.3.2 @@ -254,7 +254,7 @@ debugpy==1.2.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.8 +denonavr==0.9.9 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 @@ -355,7 +355,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.0 +google-nest-sdm==0.2.5 # homeassistant.components.gree greeclimate==0.10.3 @@ -391,10 +391,10 @@ hlk-sw16==0.0.9 hole==0.5.1 # homeassistant.components.workday -holidays==0.10.3 +holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201212.0 +home-assistant-frontend==20201229.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -403,23 +403,23 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.12.1 +homematicip==0.13.0 # homeassistant.components.google # homeassistant.components.remember_the_milk httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.12 +huawei-lte-api==1.4.17 # homeassistant.components.hyperion -hyperion-py==0.6.0 +hyperion-py==0.6.1 # homeassistant.components.iaqualink iaqualink==0.3.4 # homeassistant.components.ping -icmplib==1.2.2 +icmplib==2.0 # homeassistant.components.influxdb influxdb-client==1.8.0 @@ -462,7 +462,7 @@ mbddns==0.1.2 mcstatus==2.3.0 # homeassistant.components.meteo_france -meteofrance-api==0.1.1 +meteofrance-api==1.0.1 # homeassistant.components.mfi mficlient==0.3.0 @@ -474,7 +474,7 @@ millheater==0.4.0 minio==4.0.9 # homeassistant.components.motion_blinds -motionblinds==0.1.6 +motionblinds==0.4.7 # homeassistant.components.tts mutagen==1.45.1 @@ -597,10 +597,10 @@ pure-python-adb[async]==0.3.0.dev0 pushbullet.py==0.11.0 # homeassistant.components.august -py-august==0.25.0 +py-august==0.25.2 # homeassistant.components.canary -py-canary==0.5.0 +py-canary==0.5.1 # homeassistant.components.melissa py-melissa-climate==2.1.4 @@ -655,22 +655,19 @@ pyatv==0.7.5 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.17 +pybotvac==0.0.19 # homeassistant.components.cloudflare pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==7.5.1 +pychromecast==7.6.0 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 -# homeassistant.components.avri -pycountry==19.8.18 - # homeassistant.components.daikin -pydaikin==2.3.1 +pydaikin==2.4.0 # homeassistant.components.deconz pydeconz==77 @@ -770,7 +767,7 @@ pymata-express==1.19 pymelcloud==2.5.2 # homeassistant.components.somfy -pymfy==0.9.1 +pymfy==0.9.3 # homeassistant.components.mochad pymochad==0.2.0 @@ -803,7 +800,7 @@ pyopenuv==1.0.9 pyopnsense==0.2.0 # homeassistant.components.opentherm_gw -pyotgw==0.6b1 +pyotgw==1.0b1 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp @@ -823,7 +820,7 @@ pypoint==2.0.0 pyprof2calltree==1.4.5 # homeassistant.components.ps4 -pyps4-2ndscreen==1.1.1 +pyps4-2ndscreen==1.2.0 # homeassistant.components.qwikswitch pyqwikswitch==0.93 @@ -836,11 +833,11 @@ pyruckus==0.12 # homeassistant.components.serial # homeassistant.components.zha -pyserial-asyncio==0.4 +pyserial-asyncio==0.5 # homeassistant.components.acer_projector # homeassistant.components.zha -pyserial==3.4 +pyserial==3.5 # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 @@ -873,7 +870,7 @@ pysqueezebox==0.5.5 pysyncthru==0.7.0 # homeassistant.components.ecobee -python-ecobee-api==0.2.7 +python-ecobee-api==0.2.8 # homeassistant.components.darksky python-forecastio==1.4.0 @@ -915,7 +912,7 @@ pytile==4.0.0 pytraccar==0.9.0 # homeassistant.components.tradfri -pytradfri[async]==7.0.5 +pytradfri[async]==7.0.6 # homeassistant.components.vera pyvera==0.3.11 @@ -932,11 +929,14 @@ pyvolumio==0.1.3 # homeassistant.components.html5 pywebpush==1.9.2 +# homeassistant.components.wemo +pywemo==0.5.6 + # homeassistant.components.wilight pywilight==0.0.65 # homeassistant.components.zerproc -pyzerproc==0.2.5 +pyzerproc==0.4.7 # homeassistant.components.rachio rachiopy==1.0.3 @@ -945,7 +945,7 @@ rachiopy==1.0.3 regenmaschine==3.0.0 # homeassistant.components.python_script -restrictedpython==5.0 +restrictedpython==5.1 # homeassistant.components.rflink rflink==0.0.55 @@ -1021,7 +1021,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.2 # homeassistant.components.spider -spiderpy==1.3.1 +spiderpy==1.4.2 # homeassistant.components.spotify spotipy==2.16.1 @@ -1141,16 +1141,16 @@ yeelight==0.5.4 zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.28.7 +zeroconf==0.28.8 # homeassistant.components.zha -zha-quirks==0.0.49 +zha-quirks==0.0.51 # homeassistant.components.zha zigpy-cc==0.5.2 # homeassistant.components.zha -zigpy-deconz==0.11.0 +zigpy-deconz==0.11.1 # homeassistant.components.zha zigpy-xbee==0.13.0 @@ -1162,4 +1162,4 @@ zigpy-zigate==0.7.3 zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.28.2 +zigpy==0.29.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index a8b10eb450..e479f5e9ac 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,6 +1,6 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit -bandit==1.6.2 +bandit==1.7.0 black==20.8b1 codespell==1.17.1 flake8-docstrings==1.5.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index f627346c67..130fd2cc24 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -65,6 +65,9 @@ pycryptodome>=3.6.6 # Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324 urllib3>=1.24.3 +# Constrain H11 to ensure we get a new enough version to support non-rfc line endings +h11>=0.12.0 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 389e380af8..7500483ec5 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -33,6 +33,22 @@ def documentation_url(value: str) -> str: return value +def verify_lowercase(value: str): + """Verify a value is lowercase.""" + if value.lower() != value: + raise vol.Invalid("Value needs to be lowercase") + + return value + + +def verify_uppercase(value: str): + """Verify a value is uppercase.""" + if value.upper() != value: + raise vol.Invalid("Value needs to be uppercase") + + return value + + MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, @@ -45,8 +61,8 @@ MANIFEST_SCHEMA = vol.Schema( vol.Schema( { vol.Required("type"): str, - vol.Optional("macaddress"): str, - vol.Optional("name"): str, + vol.Optional("macaddress"): vol.All(str, verify_uppercase), + vol.Optional("name"): vol.All(str, verify_lowercase), } ), ) diff --git a/setup.py b/setup.py index d5d133d4a3..c9acb4d82d 100755 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ REQUIRES = [ "pyyaml==5.3.1", "requests==2.25.0", "ruamel.yaml==0.15.100", - "voluptuous==0.12.0", + "voluptuous==0.12.1", "voluptuous-serialize==2.4.0", "yarl==1.4.2", ] diff --git a/tests/bandit.yaml b/tests/bandit.yaml index ebd284eaa0..568f77d622 100644 --- a/tests/bandit.yaml +++ b/tests/bandit.yaml @@ -1,6 +1,7 @@ # https://bandit.readthedocs.io/en/latest/config.html tests: + - B103 - B108 - B306 - B307 @@ -13,5 +14,8 @@ tests: - B319 - B320 - B325 + - B601 - B602 - B604 + - B608 + - B609 diff --git a/tests/common.py b/tests/common.py index 66303ad96b..ce07f5ab61 100644 --- a/tests/common.py +++ b/tests/common.py @@ -54,7 +54,7 @@ from homeassistant.helpers import ( storage, ) from homeassistant.helpers.json import JSONEncoder -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component, setup_component from homeassistant.util.async_ import run_callback_threadsafe import homeassistant.util.dt as date_util from homeassistant.util.unit_system import METRIC_SYSTEM @@ -801,6 +801,19 @@ def init_recorder_component(hass, add_config=None): _LOGGER.info("In-memory recorder successfully started") +async def async_init_recorder_component(hass, add_config=None): + """Initialize the recorder asynchronously.""" + config = dict(add_config) if add_config else {} + config[recorder.CONF_DB_URL] = "sqlite://" + + with patch("homeassistant.components.recorder.migration.migrate_schema"): + assert await async_setup_component( + hass, recorder.DOMAIN, {recorder.DOMAIN: config} + ) + assert recorder.DOMAIN in hass.config.components + _LOGGER.info("In-memory recorder successfully started") + + def mock_restore_cache(hass, states): """Mock the DATA_RESTORE_CACHE.""" key = restore_state.DATA_RESTORE_STATE_TASK diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index d4aae9a94f..185f602488 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -222,6 +222,13 @@ async def test_sensor_enabled_without_forecast(hass): suggested_object_id="home_wet_bulb_temperature", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-wind", + suggested_object_id="home_wind", + disabled_by=None, + ) registry.async_get_or_create( SENSOR_DOMAIN, DOMAIN, @@ -313,6 +320,20 @@ async def test_sensor_enabled_without_forecast(hass): suggested_object_id="home_wind_gust_night_0d", disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-windday-0", + suggested_object_id="home_wind_day_0d", + disabled_by=None, + ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "0123456-windnight-0", + suggested_object_id="home_wind_night_0d", + disabled_by=None, + ) await init_integration(hass, forecast=True) @@ -393,6 +414,17 @@ async def test_sensor_enabled_without_forecast(hass): assert entry assert entry.unique_id == "0123456-windgust" + state = hass.states.get("sensor.home_wind") + assert state + assert state.state == "14.5" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind") + assert entry + assert entry.unique_id == "0123456-wind" + state = hass.states.get("sensor.home_cloud_cover_day_0d") assert state assert state.state == "58" @@ -507,6 +539,30 @@ async def test_sensor_enabled_without_forecast(hass): assert entry assert entry.unique_id == "0123456-tree-0" + state = hass.states.get("sensor.home_wind_day_0d") + assert state + assert state.state == "13.0" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get("direction") == "SSE" + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind_day_0d") + assert entry + assert entry.unique_id == "0123456-windday-0" + + state = hass.states.get("sensor.home_wind_night_0d") + assert state + assert state.state == "7.4" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SPEED_KILOMETERS_PER_HOUR + assert state.attributes.get("direction") == "WNW" + assert state.attributes.get(ATTR_ICON) == "mdi:weather-windy" + + entry = registry.async_get("sensor.home_wind_night_0d") + assert entry + assert entry.unique_id == "0123456-windnight-0" + state = hass.states.get("sensor.home_wind_gust_day_0d") assert state assert state.state == "29.6" diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 29828bddc1..197864b807 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -1,32 +1,30 @@ """Tests for Airly.""" -import json - from homeassistant.components.airly.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture +API_POINT_URL = ( + "https://airapi.airly.eu/v2/measurements/point?lat=123.000000&lng=456.000000" +) -async def init_integration(hass, forecast=False) -> MockConfigEntry: + +async def init_integration(hass, aioclient_mock) -> MockConfigEntry: """Set up the Airly integration in Home Assistant.""" entry = MockConfigEntry( domain=DOMAIN, title="Home", - unique_id="55.55-122.12", + unique_id="123-456", data={ "api_key": "foo", - "latitude": 55.55, - "longitude": 122.12, + "latitude": 123, + "longitude": 456, "name": "Home", }, ) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() return entry diff --git a/tests/components/airly/test_air_quality.py b/tests/components/airly/test_air_quality.py index fca2761f2f..24a98cbf15 100644 --- a/tests/components/airly/test_air_quality.py +++ b/tests/components/airly/test_air_quality.py @@ -1,6 +1,5 @@ """Test air_quality of Airly integration.""" from datetime import timedelta -import json from airly.exceptions import AirlyError @@ -21,19 +20,21 @@ from homeassistant.const import ( ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + HTTP_INTERNAL_SERVER_ERROR, STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch +from . import API_POINT_URL + from tests.common import async_fire_time_changed, load_fixture from tests.components.airly import init_integration -async def test_air_quality(hass): +async def test_air_quality(hass, aioclient_mock): """Test states of the air_quality.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) registry = await hass.helpers.entity_registry.async_get_registry() state = hass.states.get("air_quality.home") @@ -58,56 +59,55 @@ async def test_air_quality(hass): entry = registry.async_get("air_quality.home") assert entry - assert entry.unique_id == "55.55-122.12" + assert entry.unique_id == "123-456" -async def test_availability(hass): +async def test_availability(hass, aioclient_mock): """Ensure that we mark the entities unavailable correctly when service causes an error.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) state = hass.states.get("air_quality.home") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "14" + aioclient_mock.clear_requests() + aioclient_mock.get( + API_POINT_URL, exc=AirlyError(HTTP_INTERNAL_SERVER_ERROR, "Unexpected error") + ) future = utcnow() + timedelta(minutes=60) - with patch( - "airly._private._RequestsHandler.get", - side_effect=AirlyError(500, "Unexpected error"), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - state = hass.states.get("air_quality.home") - assert state - assert state.state == STATE_UNAVAILABLE + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + state = hass.states.get("air_quality.home") + assert state + assert state.state == STATE_UNAVAILABLE + + aioclient_mock.clear_requests() + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) future = utcnow() + timedelta(minutes=120) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - state = hass.states.get("air_quality.home") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "14" + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.home") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "14" -async def test_manual_update_entity(hass): +async def test_manual_update_entity(hass, aioclient_mock): """Test manual update entity via service homeasasistant/update_entity.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) + call_count = aioclient_mock.call_count await async_setup_component(hass, "homeassistant", {}) - with patch( - "homeassistant.components.airly.AirlyDataUpdateCoordinator._async_update_data" - ) as mock_update: - await hass.services.async_call( - "homeassistant", - "update_entity", - {ATTR_ENTITY_ID: ["air_quality.home"]}, - blocking=True, - ) - assert mock_update.call_count == 1 + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["air_quality.home"]}, + blocking=True, + ) + + assert aioclient_mock.call_count == call_count + 1 diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index d7d45bbd7e..46dc5510b1 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -1,6 +1,4 @@ """Define tests for the Airly config flow.""" -import json - from airly.exceptions import AirlyError from homeassistant import data_entry_flow @@ -11,14 +9,15 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - HTTP_FORBIDDEN, + HTTP_UNAUTHORIZED, ) -from tests.async_mock import patch -from tests.common import MockConfigEntry, load_fixture +from . import API_POINT_URL + +from tests.common import MockConfigEntry, load_fixture, patch CONFIG = { - CONF_NAME: "abcd", + CONF_NAME: "Home", CONF_API_KEY: "foo", CONF_LATITUDE: 123, CONF_LONGITUDE: 456, @@ -35,69 +34,57 @@ async def test_show_form(hass): assert result["step_id"] == SOURCE_USER -async def test_invalid_api_key(hass): +async def test_invalid_api_key(hass, aioclient_mock): """Test that errors are shown when API key is invalid.""" - with patch( - "airly._private._RequestsHandler.get", - side_effect=AirlyError( - HTTP_FORBIDDEN, {"message": "Invalid authentication credentials"} + aioclient_mock.get( + API_POINT_URL, + exc=AirlyError( + HTTP_UNAUTHORIZED, {"message": "Invalid authentication credentials"} ), - ): + ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) - assert result["errors"] == {"base": "invalid_api_key"} + assert result["errors"] == {"base": "invalid_api_key"} -async def test_invalid_location(hass): +async def test_invalid_location(hass, aioclient_mock): """Test that errors are shown when location is invalid.""" - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_no_station.json")), - ): + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) - assert result["errors"] == {"base": "wrong_location"} + assert result["errors"] == {"base": "wrong_location"} -async def test_duplicate_error(hass): +async def test_duplicate_error(hass, aioclient_mock): """Test that errors are shown when duplicates are added.""" + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + MockConfigEntry(domain=DOMAIN, unique_id="123-456", data=CONFIG).add_to_hass(hass) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - MockConfigEntry(domain=DOMAIN, unique_id="123-456", data=CONFIG).add_to_hass( - hass - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONFIG - ) - - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + assert result["type"] == "abort" + assert result["reason"] == "already_configured" -async def test_create_entry(hass): +async def test_create_entry(hass, aioclient_mock): """Test that the user step works.""" + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - + with patch("homeassistant.components.airly.async_setup_entry", return_value=True): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == CONFIG[CONF_NAME] - assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] - assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] - assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index 28f2aca4fb..cb0ccf268f 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -1,6 +1,5 @@ """Test init of Airly integration.""" from datetime import timedelta -import json from homeassistant.components.airly.const import DOMAIN from homeassistant.config_entries import ( @@ -10,14 +9,15 @@ from homeassistant.config_entries import ( ) from homeassistant.const import STATE_UNAVAILABLE -from tests.async_mock import patch +from . import API_POINT_URL + from tests.common import MockConfigEntry, load_fixture from tests.components.airly import init_integration -async def test_async_setup_entry(hass): +async def test_async_setup_entry(hass, aioclient_mock): """Test a successful setup entry.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) state = hass.states.get("air_quality.home") assert state is not None @@ -25,75 +25,69 @@ async def test_async_setup_entry(hass): assert state.state == "14" -async def test_config_not_ready(hass): +async def test_config_not_ready(hass, aioclient_mock): """Test for setup failure if connection to Airly is missing.""" entry = MockConfigEntry( domain=DOMAIN, title="Home", - unique_id="55.55-122.12", + unique_id="123-456", data={ "api_key": "foo", - "latitude": 55.55, - "longitude": 122.12, + "latitude": 123, + "longitude": 456, "name": "Home", }, ) - with patch("airly._private._RequestsHandler.get", side_effect=ConnectionError()): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY -async def test_config_without_unique_id(hass): +async def test_config_without_unique_id(hass, aioclient_mock): """Test for setup entry without unique_id.""" entry = MockConfigEntry( domain=DOMAIN, title="Home", data={ "api_key": "foo", - "latitude": 55.55, - "longitude": 122.12, + "latitude": 123, + "longitude": 456, "name": "Home", }, ) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_LOADED - assert entry.unique_id == "55.55-122.12" + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_LOADED + assert entry.unique_id == "123-456" -async def test_config_with_turned_off_station(hass): +async def test_config_with_turned_off_station(hass, aioclient_mock): """Test for setup entry for a turned off measuring station.""" entry = MockConfigEntry( domain=DOMAIN, title="Home", - unique_id="55.55-122.12", + unique_id="123-456", data={ "api_key": "foo", - "latitude": 55.55, - "longitude": 122.12, + "latitude": 123, + "longitude": 456, "name": "Home", }, ) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_no_station.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY -async def test_update_interval(hass): +async def test_update_interval(hass, aioclient_mock): """Test correct update interval when the number of configured instances changes.""" - entry = await init_integration(hass) + entry = await init_integration(hass, aioclient_mock) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED @@ -112,13 +106,13 @@ async def test_update_interval(hass): }, ) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - entry.add_to_hass(hass) - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + aioclient_mock.get( + "https://airapi.airly.eu/v2/measurements/point?lat=66.660000&lng=111.110000", + text=load_fixture("airly_valid_station.json"), + ) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 2 assert entry.state == ENTRY_STATE_LOADED @@ -126,9 +120,9 @@ async def test_update_interval(hass): assert instance.update_interval == timedelta(minutes=30) -async def test_unload_entry(hass): +async def test_unload_entry(hass, aioclient_mock): """Test successful unload of entry.""" - entry = await init_integration(hass) + entry = await init_integration(hass, aioclient_mock) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED diff --git a/tests/components/airly/test_sensor.py b/tests/components/airly/test_sensor.py index 45b98d7c27..abc53294bb 100644 --- a/tests/components/airly/test_sensor.py +++ b/tests/components/airly/test_sensor.py @@ -1,6 +1,5 @@ """Test sensor of Airly integration.""" from datetime import timedelta -import json from homeassistant.components.airly.sensor import ATTRIBUTION from homeassistant.const import ( @@ -21,14 +20,15 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch +from . import API_POINT_URL + from tests.common import async_fire_time_changed, load_fixture from tests.components.airly import init_integration -async def test_sensor(hass): +async def test_sensor(hass, aioclient_mock): """Test states of the sensor.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) registry = await hass.helpers.entity_registry.async_get_registry() state = hass.states.get("sensor.home_humidity") @@ -40,7 +40,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_humidity") assert entry - assert entry.unique_id == "55.55-122.12-humidity" + assert entry.unique_id == "123-456-humidity" state = hass.states.get("sensor.home_pm1") assert state @@ -54,7 +54,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_pm1") assert entry - assert entry.unique_id == "55.55-122.12-pm1" + assert entry.unique_id == "123-456-pm1" state = hass.states.get("sensor.home_pressure") assert state @@ -65,7 +65,7 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_pressure") assert entry - assert entry.unique_id == "55.55-122.12-pressure" + assert entry.unique_id == "123-456-pressure" state = hass.states.get("sensor.home_temperature") assert state @@ -76,53 +76,51 @@ async def test_sensor(hass): entry = registry.async_get("sensor.home_temperature") assert entry - assert entry.unique_id == "55.55-122.12-temperature" + assert entry.unique_id == "123-456-temperature" -async def test_availability(hass): +async def test_availability(hass, aioclient_mock): """Ensure that we mark the entities unavailable correctly when service is offline.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) state = hass.states.get("sensor.home_humidity") assert state assert state.state != STATE_UNAVAILABLE assert state.state == "92.8" + aioclient_mock.clear_requests() + aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) future = utcnow() + timedelta(minutes=60) - with patch("airly._private._RequestsHandler.get", side_effect=ConnectionError()): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = hass.states.get("sensor.home_humidity") - assert state - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("sensor.home_humidity") + assert state + assert state.state == STATE_UNAVAILABLE + aioclient_mock.clear_requests() + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) future = utcnow() + timedelta(minutes=120) - with patch( - "airly._private._RequestsHandler.get", - return_value=json.loads(load_fixture("airly_valid_station.json")), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - state = hass.states.get("sensor.home_humidity") - assert state - assert state.state != STATE_UNAVAILABLE - assert state.state == "92.8" + state = hass.states.get("sensor.home_humidity") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "92.8" -async def test_manual_update_entity(hass): +async def test_manual_update_entity(hass, aioclient_mock): """Test manual update entity via service homeasasistant/update_entity.""" - await init_integration(hass) + await init_integration(hass, aioclient_mock) + call_count = aioclient_mock.call_count await async_setup_component(hass, "homeassistant", {}) - with patch( - "homeassistant.components.airly.AirlyDataUpdateCoordinator._async_update_data" - ) as mock_update: - await hass.services.async_call( - "homeassistant", - "update_entity", - {ATTR_ENTITY_ID: ["sensor.home_humidity"]}, - blocking=True, - ) - assert mock_update.call_count == 1 + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.home_humidity"]}, + blocking=True, + ) + + assert aioclient_mock.call_count == call_count + 1 diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index d9c1a5a40c..bc007fefb8 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -2,7 +2,7 @@ from uuid import uuid4 from homeassistant.components.alexa import config, smart_home -from homeassistant.core import Context +from homeassistant.core import Context, callback from tests.common import async_mock_service @@ -37,6 +37,11 @@ class MockConfig(config.AbstractConfig): """Return config locale.""" return TEST_LOCALE + @callback + def user_identifier(self): + """Return an identifier for the user that represents this config.""" + return "mock-user-id" + def should_expose(self, entity_id): """If an entity should be exposed.""" return True diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 6b48c313fc..45991375ba 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -1,5 +1,6 @@ """Test Alexa entity representation.""" from homeassistant.components.alexa import smart_home +from homeassistant.const import __version__ from . import DEFAULT_CONFIG, get_new_request @@ -20,6 +21,26 @@ async def test_unsupported_domain(hass): assert not msg["payload"]["endpoints"] +async def test_serialize_discovery(hass): + """Test we handle an interface raising unexpectedly during serialize discovery.""" + request = get_new_request("Alexa.Discovery", "Discover") + + hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"}) + + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request) + + assert "event" in msg + msg = msg["event"] + endpoint = msg["payload"]["endpoints"][0] + + assert endpoint["additionalAttributes"] == { + "manufacturer": "Home Assistant", + "model": "switch", + "softwareVersion": __version__, + "customIdentifier": "mock-user-id-switch.bla", + } + + async def test_serialize_discovery_recovers(hass, caplog): """Test we handle an interface raising unexpectedly during serialize discovery.""" request = get_new_request("Alexa.Discovery", "Discover") diff --git a/tests/components/avri/__init__.py b/tests/components/avri/__init__.py deleted file mode 100644 index c521285503..0000000000 --- a/tests/components/avri/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the Avri integration.""" diff --git a/tests/components/avri/test_config_flow.py b/tests/components/avri/test_config_flow.py deleted file mode 100644 index 2dba3c3c11..0000000000 --- a/tests/components/avri/test_config_flow.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Test the Avri config flow.""" -from homeassistant import config_entries, setup -from homeassistant.components.avri.const import DOMAIN - -from tests.async_mock import patch - - -async def test_form(hass): - """Test we get the form.""" - await setup.async_setup_component(hass, "avri", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - with patch( - "homeassistant.components.avri.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "zip_code": "1234AB", - "house_number": 42, - "house_number_extension": "", - "country_code": "NL", - }, - ) - - assert result2["type"] == "create_entry" - assert result2["title"] == "1234AB 42" - assert result2["data"] == { - "id": "1234AB 42", - "zip_code": "1234AB", - "house_number": 42, - "house_number_extension": "", - "country_code": "NL", - } - await hass.async_block_till_done() - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_invalid_house_number(hass): - """Test we handle invalid house number.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "zip_code": "1234AB", - "house_number": -1, - "house_number_extension": "", - "country_code": "NL", - }, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"house_number": "invalid_house_number"} - - -async def test_form_invalid_country_code(hass): - """Test we handle invalid county code.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "zip_code": "1234AB", - "house_number": 42, - "house_number_extension": "", - "country_code": "foo", - }, - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"country_code": "invalid_country_code"} diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py new file mode 100644 index 0000000000..e1243fe2c0 --- /dev/null +++ b/tests/components/bmw_connected_drive/__init__.py @@ -0,0 +1 @@ +"""Tests for the for the BMW Connected Drive integration.""" diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py new file mode 100644 index 0000000000..ae32feec7b --- /dev/null +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -0,0 +1,153 @@ +"""Test the for the BMW Connected Drive config flow.""" +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN +from homeassistant.components.bmw_connected_drive.const import ( + CONF_READ_ONLY, + CONF_REGION, + CONF_USE_LOCATION, +) +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +FIXTURE_USER_INPUT = { + CONF_USERNAME: "user@domain.com", + CONF_PASSWORD: "p4ssw0rd", + CONF_REGION: "rest_of_world", +} +FIXTURE_COMPLETE_ENTRY = FIXTURE_USER_INPUT.copy() +FIXTURE_IMPORT_ENTRY = FIXTURE_USER_INPUT.copy() + +FIXTURE_CONFIG_ENTRY = { + "entry_id": "1", + "domain": DOMAIN, + "title": FIXTURE_USER_INPUT[CONF_USERNAME], + "data": { + CONF_USERNAME: FIXTURE_USER_INPUT[CONF_USERNAME], + CONF_PASSWORD: FIXTURE_USER_INPUT[CONF_PASSWORD], + CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], + }, + "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + "system_options": {"disable_new_entities": False}, + "source": "user", + "connection_class": config_entries.CONN_CLASS_CLOUD_POLL, + "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", +} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_connection_error(hass): + """Test we show user form on BMW connected drive connection error.""" + + def _mock_get_oauth_token(*args, **kwargs): + pass + + with patch( + "bimmer_connected.account.ConnectedDriveAccount._get_oauth_token", + side_effect=OSError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_full_user_flow_implementation(hass): + """Test registering an integration and finishing flow works.""" + with patch( + "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", + return_value=[], + ), patch( + "homeassistant.components.bmw_connected_drive.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.bmw_connected_drive.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, + ) + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] + assert result2["data"] == FIXTURE_COMPLETE_ENTRY + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_full_config_flow_implementation(hass): + """Test registering an integration and finishing flow works.""" + with patch( + "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", + return_value=[], + ), patch( + "homeassistant.components.bmw_connected_drive.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.bmw_connected_drive.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=FIXTURE_USER_INPUT, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == FIXTURE_IMPORT_ENTRY[CONF_USERNAME] + assert result["data"] == FIXTURE_IMPORT_ENTRY + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_options_flow_implementation(hass): + """Test config flow options.""" + with patch( + "bimmer_connected.account.ConnectedDriveAccount._get_vehicles", + return_value=[], + ), patch( + "homeassistant.components.bmw_connected_drive.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.bmw_connected_drive.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + config_entry = MockConfigEntry(**FIXTURE_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "account_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_READ_ONLY: False, + CONF_USE_LOCATION: False, + } + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 0e9f6c13e3..4f75e93fae 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -787,50 +787,6 @@ async def test_entity_media_states(hass: HomeAssistantType): assert state.state == "unknown" -async def test_url_replace(hass: HomeAssistantType): - """Test functionality of replacing URL for HTTPS.""" - entity_id = "media_player.speaker" - reg = await hass.helpers.entity_registry.async_get_registry() - - info = get_fake_chromecast_info() - full_info = attr.evolve( - info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID - ) - - chromecast = await async_setup_media_player_cast(hass, info) - _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) - - connection_status = MagicMock() - connection_status.status = "CONNECTED" - conn_status_cb(connection_status) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.name == "Speaker" - assert state.state == "unknown" - assert entity_id == reg.async_get_entity_id("media_player", "cast", full_info.uuid) - - class FakeHTTPImage: - url = "http://example.com/test.png" - - class FakeHTTPSImage: - url = "https://example.com/test.png" - - media_status = MagicMock(images=[FakeHTTPImage()]) - media_status.player_is_playing = True - media_status_cb(media_status) - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.attributes.get("entity_picture") == "//example.com/test.png" - - media_status.images = [FakeHTTPSImage()] - media_status_cb(media_status) - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state.attributes.get("entity_picture") == "https://example.com/test.png" - - async def test_group_media_states(hass, mz_mock): """Test media states are read from group if entity has no state.""" entity_id = "media_player.speaker" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index e54a5dcde0..ce76195292 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -16,7 +16,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): alexa_entity_configs={"light.kitchen": entity_conf}, alexa_default_expose=["light"], ) - conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None + ) assert not conf.should_expose("light.kitchen") entity_conf["should_expose"] = True @@ -33,7 +35,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs): async def test_alexa_config_report_state(hass, cloud_prefs): """Test Alexa config should expose using prefs.""" - conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + conf = alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None + ) assert cloud_prefs.alexa_report_state is False assert conf.should_report_state is False @@ -68,6 +72,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock): conf = alexa_config.AlexaConfig( hass, ALEXA_SCHEMA({}), + "mock-user-id", cloud_prefs, Mock( alexa_access_token_url="http://example/alexa_token", @@ -114,7 +119,7 @@ def patch_sync_helper(): async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): """Test Alexa config responds to updating exposed entities.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None) with patch_sync_helper() as (to_update, to_remove): await cloud_prefs.async_update_alexa_entity_config( @@ -147,7 +152,9 @@ async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): """Test Alexa config responds to entity registry.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data["cloud"]) + alexa_config.AlexaConfig( + hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"] + ) with patch_sync_helper() as (to_update, to_remove): hass.bus.async_fire( @@ -197,7 +204,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs): async def test_alexa_update_report_state(hass, cloud_prefs): """Test Alexa config responds to reporting state.""" - alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None) + alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None) with patch( "homeassistant.components.cloud.alexa_config.AlexaConfig.async_sync_entities", diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index ad99f13e8f..060188d65a 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -26,8 +26,8 @@ async def test_form(hass): "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", return_value=True, ), patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", - return_value=["123456"], + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", + return_value="123456", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -71,13 +71,13 @@ async def test_form_invalid_credentials(hass): async def test_form_already_configured(hass): """Test if we get the error message on already configured.""" with patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", - return_value=["1234567"], + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", + return_value="123456", ), patch( "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", return_value=True, ): - MockConfigEntry(domain=DOMAIN, unique_id="1234567", data={}).add_to_hass(hass) + MockConfigEntry(domain=DOMAIN, unique_id="123456", data={}).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -105,8 +105,8 @@ async def test_form_advanced_options(hass): "homeassistant.components.devolo_home_control.config_flow.Mydevolo.credentials_valid", return_value=True, ), patch( - "homeassistant.components.devolo_home_control.config_flow.Mydevolo.get_gateway_ids", - return_value=["123456"], + "homeassistant.components.devolo_home_control.config_flow.Mydevolo.uuid", + return_value="123456", ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index d2cec93df9..d57828fdfa 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -2,7 +2,11 @@ import asyncio from dsmr_parser.clients.protocol import DSMRProtocol -from dsmr_parser.obis_references import EQUIPMENT_IDENTIFIER, EQUIPMENT_IDENTIFIER_GAS +from dsmr_parser.obis_references import ( + EQUIPMENT_IDENTIFIER, + EQUIPMENT_IDENTIFIER_GAS, + LUXEMBOURG_EQUIPMENT_IDENTIFIER, +) from dsmr_parser.objects import CosemObject import pytest @@ -38,17 +42,27 @@ async def dsmr_connection_send_validate_fixture(hass): transport = MagicMock(spec=asyncio.Transport) protocol = MagicMock(spec=DSMRProtocol) - async def connection_factory(*args, **kwargs): - """Return mocked out Asyncio classes.""" - return (transport, protocol) - - connection_factory = MagicMock(wraps=connection_factory) - protocol.telegram = { EQUIPMENT_IDENTIFIER: CosemObject([{"value": "12345678", "unit": ""}]), EQUIPMENT_IDENTIFIER_GAS: CosemObject([{"value": "123456789", "unit": ""}]), } + async def connection_factory(*args, **kwargs): + """Return mocked out Asyncio classes.""" + if args[1] == "5L": + protocol.telegram = { + LUXEMBOURG_EQUIPMENT_IDENTIFIER: CosemObject( + [{"value": "12345678", "unit": ""}] + ), + EQUIPMENT_IDENTIFIER_GAS: CosemObject( + [{"value": "123456789", "unit": ""}] + ), + } + + return (transport, protocol) + + connection_factory = MagicMock(wraps=connection_factory) + async def wait_closed(): if isinstance(connection_factory.call_args_list[0][0][2], str): # TCP diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 039002ca7a..9ae49419bf 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -242,3 +242,26 @@ async def test_options_flow(hass): await hass.async_block_till_done() assert entry.options == {"time_between_update": 15} + + +async def test_import_luxembourg(hass, dsmr_connection_send_validate_fixture): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5L", + "precision": 4, + "reconnect_interval": 30, + } + + with patch("homeassistant.components.dsmr.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=entry_data, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "/dev/ttyUSB0" + assert result["data"] == {**entry_data, **SERIAL_DATA} diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index ceccc7d8c3..76a9a5bb07 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -337,6 +337,75 @@ async def test_v5_meter(hass, dsmr_connection_fixture): assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS +async def test_luxembourg_meter(hass, dsmr_connection_fixture): + """Test if v5 meter is correctly parsed.""" + (connection_factory, transport, protocol) = dsmr_connection_fixture + + from dsmr_parser.obis_references import ( + HOURLY_GAS_METER_READING, + LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL, + LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL, + ) + from dsmr_parser.objects import CosemObject, MBusObject + + entry_data = { + "port": "/dev/ttyUSB0", + "dsmr_version": "5L", + "precision": 4, + "reconnect_interval": 30, + "serial_id": "1234", + "serial_id_gas": "5678", + } + entry_options = { + "time_between_update": 0, + } + + telegram = { + HOURLY_GAS_METER_READING: MBusObject( + [ + {"value": datetime.datetime.fromtimestamp(1551642213)}, + {"value": Decimal(745.695), "unit": VOLUME_CUBIC_METERS}, + ] + ), + LUXEMBOURG_ELECTRICITY_USED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(123.456), "unit": ENERGY_KILO_WATT_HOUR}] + ), + LUXEMBOURG_ELECTRICITY_DELIVERED_TARIFF_GLOBAL: CosemObject( + [{"value": Decimal(654.321), "unit": ENERGY_KILO_WATT_HOUR}] + ), + } + + mock_entry = MockConfigEntry( + domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options + ) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + telegram_callback = connection_factory.call_args_list[0][0][2] + + # simulate a telegram pushed from the smartmeter and parsed by dsmr_parser + telegram_callback(telegram) + + # after receiving telegram entities need to have the chance to update + await asyncio.sleep(0) + + power_tariff = hass.states.get("sensor.energy_consumption_total") + assert power_tariff.state == "123.456" + assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + + power_tariff = hass.states.get("sensor.energy_production_total") + assert power_tariff.state == "654.321" + assert power_tariff.attributes.get("unit_of_measurement") == ENERGY_KILO_WATT_HOUR + + # check if gas consumption is parsed correctly + gas_consumption = hass.states.get("sensor.gas_consumption") + assert gas_consumption.state == "745.695" + assert gas_consumption.attributes.get("unit_of_measurement") == VOLUME_CUBIC_METERS + + async def test_belgian_meter(hass, dsmr_connection_fixture): """Test if Belgian meter is correctly parsed.""" (connection_factory, transport, protocol) = dsmr_connection_fixture diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index 77105dc73d..c4e4c91087 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -677,8 +677,7 @@ async def test_purehotcool_set_fan_mode(devices, login, hass): {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_AUTO}, True, ) - assert device.set_fan_speed.call_count == 4 - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_AUTO) + assert device.enable_auto_mode.call_count == 1 @patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 0aa390223c..1c3b4b0d67 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,7 +1,8 @@ """The test for the data filter sensor platform.""" from datetime import timedelta from os import path -import unittest + +from pytest import fixture from homeassistant import config as hass_config from homeassistant.components.filter.sensor import ( @@ -13,311 +14,436 @@ from homeassistant.components.filter.sensor import ( TimeSMAFilter, TimeThrottleFilter, ) -from homeassistant.const import SERVICE_RELOAD +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import SERVICE_RELOAD, STATE_UNAVAILABLE, STATE_UNKNOWN import homeassistant.core as ha -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.async_mock import patch -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - init_recorder_component, -) +from tests.common import assert_setup_component, async_init_recorder_component -class TestFilterSensor(unittest.TestCase): - """Test the Data Filter sensor.""" +@fixture +def values(): + """Fixture for a list of test States.""" + values = [] + raw_values = [20, 19, 18, 21, 22, 0] + timestamp = dt_util.utcnow() + for val in raw_values: + values.append(ha.State("sensor.test_monitored", val, last_updated=timestamp)) + timestamp += timedelta(minutes=1) + return values - def setup_method(self, method): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.hass.config.components.add("history") - raw_values = [20, 19, 18, 21, 22, 0] - self.values = [] - timestamp = dt_util.utcnow() - for val in raw_values: - self.values.append( - ha.State("sensor.test_monitored", val, last_updated=timestamp) - ) - timestamp += timedelta(minutes=1) - - def teardown_method(self, method): - """Stop everything that was started.""" - self.hass.stop() - - def init_recorder(self): - """Initialize the recorder.""" - init_recorder_component(self.hass) - self.hass.start() - - def test_setup_fail(self): - """Test if filter doesn't exist.""" - config = { - "sensor": { - "platform": "filter", - "entity_id": "sensor.test_monitored", - "filters": [{"filter": "nonexisting"}], - } +async def test_setup_fail(hass): + """Test if filter doesn't exist.""" + config = { + "sensor": { + "platform": "filter", + "entity_id": "sensor.test_monitored", + "filters": [{"filter": "nonexisting"}], } - with assert_setup_component(0): - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + } + with assert_setup_component(0): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() - def test_chain(self): - """Test if filter chaining works.""" - config = { - "sensor": { - "platform": "filter", - "name": "test", - "entity_id": "sensor.test_monitored", - "filters": [ - {"filter": "outlier", "window_size": 10, "radius": 4.0}, - {"filter": "lowpass", "time_constant": 10, "precision": 2}, - {"filter": "throttle", "window_size": 1}, - ], - } + +async def test_chain(hass, values): + """Test if filter chaining works.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + {"filter": "lowpass", "time_constant": 10, "precision": 2}, + {"filter": "throttle", "window_size": 1}, + ], } + } + await async_init_recorder_component(hass) - with assert_setup_component(1, "sensor"): - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() - for value in self.values: - self.hass.states.set(config["sensor"]["entity_id"], value.state) - self.hass.block_till_done() + for value in values: + hass.states.async_set(config["sensor"]["entity_id"], value.state) + await hass.async_block_till_done() - state = self.hass.states.get("sensor.test") - assert "18.05" == state.state + state = hass.states.get("sensor.test") + assert "18.05" == state.state - def test_chain_history(self, missing=False): - """Test if filter chaining works.""" - self.init_recorder() - config = { - "history": {}, - "sensor": { - "platform": "filter", - "name": "test", - "entity_id": "sensor.test_monitored", - "filters": [ - {"filter": "outlier", "window_size": 10, "radius": 4.0}, - {"filter": "lowpass", "time_constant": 10, "precision": 2}, - {"filter": "throttle", "window_size": 1}, - ], - }, - } - t_0 = dt_util.utcnow() - timedelta(minutes=1) - t_1 = dt_util.utcnow() - timedelta(minutes=2) - t_2 = dt_util.utcnow() - timedelta(minutes=3) - t_3 = dt_util.utcnow() - timedelta(minutes=4) - if missing: - fake_states = {} - else: - fake_states = { - "sensor.test_monitored": [ - ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", "unknown", last_changed=t_1), - ha.State("sensor.test_monitored", 19.0, last_changed=t_2), - ha.State("sensor.test_monitored", 18.2, last_changed=t_3), - ] - } +async def test_chain_history(hass, values, missing=False): + """Test if filter chaining works.""" + config = { + "history": {}, + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + {"filter": "lowpass", "time_constant": 10, "precision": 2}, + {"filter": "throttle", "window_size": 1}, + ], + }, + } + await async_init_recorder_component(hass) + assert_setup_component(1, "history") - with patch( - "homeassistant.components.history.state_changes_during_period", - return_value=fake_states, - ): - with patch( - "homeassistant.components.history.get_last_state_changes", - return_value=fake_states, - ): - with assert_setup_component(1, "sensor"): - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() - - for value in self.values: - self.hass.states.set(config["sensor"]["entity_id"], value.state) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.test") - if missing: - assert "18.05" == state.state - else: - assert "17.05" == state.state - - def test_chain_history_missing(self): - """Test if filter chaining works when recorder is enabled but the source is not recorded.""" - return self.test_chain_history(missing=True) - - def test_history_time(self): - """Test loading from history based on a time window.""" - self.init_recorder() - config = { - "history": {}, - "sensor": { - "platform": "filter", - "name": "test", - "entity_id": "sensor.test_monitored", - "filters": [{"filter": "time_throttle", "window_size": "00:01"}], - }, - } - t_0 = dt_util.utcnow() - timedelta(minutes=1) - t_1 = dt_util.utcnow() - timedelta(minutes=2) - t_2 = dt_util.utcnow() - timedelta(minutes=3) + t_0 = dt_util.utcnow() - timedelta(minutes=1) + t_1 = dt_util.utcnow() - timedelta(minutes=2) + t_2 = dt_util.utcnow() - timedelta(minutes=3) + t_3 = dt_util.utcnow() - timedelta(minutes=4) + if missing: + fake_states = {} + else: fake_states = { "sensor.test_monitored": [ ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", 19.0, last_changed=t_1), - ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + ha.State("sensor.test_monitored", "unknown", last_changed=t_1), + ha.State("sensor.test_monitored", 19.0, last_changed=t_2), + ha.State("sensor.test_monitored", 18.2, last_changed=t_3), ] } + + with patch( + "homeassistant.components.history.state_changes_during_period", + return_value=fake_states, + ): with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.history.get_last_state_changes", return_value=fake_states, ): - with patch( - "homeassistant.components.history.get_last_state_changes", - return_value=fake_states, - ): - with assert_setup_component(1, "sensor"): - assert setup_component(self.hass, "sensor", config) - self.hass.block_till_done() + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() - self.hass.block_till_done() - state = self.hass.states.get("sensor.test") - assert "18.0" == state.state + for value in values: + hass.states.async_set(config["sensor"]["entity_id"], value.state) + await hass.async_block_till_done() - def test_outlier(self): - """Test if outlier filter works.""" - filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - for state in self.values: - filtered = filt.filter_state(state) - assert 21 == filtered.state - - def test_outlier_step(self): - """ - Test step-change handling in outlier. - - Test if outlier filter handles long-running step-changes correctly. - It should converge to no longer filter once just over half the - window_size is occupied by the new post step-change values. - """ - filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=1.1) - self.values[-1].state = 22 - for state in self.values: - filtered = filt.filter_state(state) - assert 22 == filtered.state - - def test_initial_outlier(self): - """Test issue #13363.""" - filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - out = ha.State("sensor.test_monitored", 4000) - for state in [out] + self.values: - filtered = filt.filter_state(state) - assert 21 == filtered.state - - def test_unknown_state_outlier(self): - """Test issue #32395.""" - filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) - out = ha.State("sensor.test_monitored", "unknown") - for state in [out] + self.values + [out]: - try: - filtered = filt.filter_state(state) - except ValueError: - assert state.state == "unknown" - assert 21 == filtered.state - - def test_precision_zero(self): - """Test if precision of zero returns an integer.""" - filt = LowPassFilter(window_size=10, precision=0, entity=None, time_constant=10) - for state in self.values: - filtered = filt.filter_state(state) - assert isinstance(filtered.state, int) - - def test_lowpass(self): - """Test if lowpass filter works.""" - filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) - out = ha.State("sensor.test_monitored", "unknown") - for state in [out] + self.values + [out]: - try: - filtered = filt.filter_state(state) - except ValueError: - assert state.state == "unknown" - assert 18.05 == filtered.state - - def test_range(self): - """Test if range filter works.""" - lower = 10 - upper = 20 - filt = RangeFilter( - entity=None, precision=2, lower_bound=lower, upper_bound=upper - ) - for unf_state in self.values: - unf = float(unf_state.state) - filtered = filt.filter_state(unf_state) - if unf < lower: - assert lower == filtered.state - elif unf > upper: - assert upper == filtered.state + state = hass.states.get("sensor.test") + if missing: + assert "18.05" == state.state else: - assert unf == filtered.state + assert "17.05" == state.state - def test_range_zero(self): - """Test if range filter works with zeroes as bounds.""" - lower = 0 - upper = 0 - filt = RangeFilter( - entity=None, precision=2, lower_bound=lower, upper_bound=upper + +async def test_source_state_none(hass, values): + """Test is source sensor state is null and sets state to STATE_UNKNOWN.""" + await async_init_recorder_component(hass) + + config = { + "sensor": [ + { + "platform": "template", + "sensors": { + "template_test": { + "value_template": "{{ states.sensor.test_state.state }}" + } + }, + }, + { + "platform": "filter", + "name": "test", + "entity_id": "sensor.template_test", + "filters": [ + { + "filter": "time_simple_moving_average", + "window_size": "00:01", + "precision": "2", + } + ], + }, + ] + } + await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_state", 0) + + await hass.async_block_till_done() + state = hass.states.get("sensor.template_test") + assert state.state == "0" + + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state.state == "0.0" + + # Force Template Reload + yaml_path = path.join( + _get_fixtures_base_path(), + "fixtures", + "template/sensor_configuration.yaml", + ) + with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): + await hass.services.async_call( + "template", + SERVICE_RELOAD, + {}, + blocking=True, ) - for unf_state in self.values: - unf = float(unf_state.state) - filtered = filt.filter_state(unf_state) - if unf < lower: - assert lower == filtered.state - elif unf > upper: - assert upper == filtered.state - else: - assert unf == filtered.state + await hass.async_block_till_done() - def test_throttle(self): - """Test if lowpass filter works.""" - filt = ThrottleFilter(window_size=3, precision=2, entity=None) - filtered = [] - for state in self.values: - new_state = filt.filter_state(state) - if not filt.skip_processing: - filtered.append(new_state) - assert [20, 21] == [f.state for f in filtered] + # Template state gets to None + state = hass.states.get("sensor.template_test") + assert state is None - def test_time_throttle(self): - """Test if lowpass filter works.""" - filt = TimeThrottleFilter( - window_size=timedelta(minutes=2), precision=2, entity=None + # Filter sensor ignores None state setting state to STATE_UNKNOWN + state = hass.states.get("sensor.test") + assert state.state == STATE_UNKNOWN + + +async def test_chain_history_missing(hass, values): + """Test if filter chaining works when recorder is enabled but the source is not recorded.""" + await test_chain_history(hass, values, missing=True) + + +async def test_history_time(hass): + """Test loading from history based on a time window.""" + config = { + "history": {}, + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [{"filter": "time_throttle", "window_size": "00:01"}], + }, + } + await async_init_recorder_component(hass) + assert_setup_component(1, "history") + + t_0 = dt_util.utcnow() - timedelta(minutes=1) + t_1 = dt_util.utcnow() - timedelta(minutes=2) + t_2 = dt_util.utcnow() - timedelta(minutes=3) + + fake_states = { + "sensor.test_monitored": [ + ha.State("sensor.test_monitored", 18.0, last_changed=t_0), + ha.State("sensor.test_monitored", 19.0, last_changed=t_1), + ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + ] + } + with patch( + "homeassistant.components.history.state_changes_during_period", + return_value=fake_states, + ): + with patch( + "homeassistant.components.history.get_last_state_changes", + return_value=fake_states, + ): + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert "18.0" == state.state + + +async def test_setup(hass): + """Test if filter attributes are inherited.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + ], + } + } + + await async_init_recorder_component(hass) + + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set( + "sensor.test_monitored", + 1, + {"icon": "mdi:test", "device_class": DEVICE_CLASS_TEMPERATURE}, ) - filtered = [] - for state in self.values: - new_state = filt.filter_state(state) - if not filt.skip_processing: - filtered.append(new_state) - assert [20, 18, 22] == [f.state for f in filtered] + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state.attributes["icon"] == "mdi:test" + assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE + assert state.state == "1.0" - def test_time_sma(self): - """Test if time_sma filter works.""" - filt = TimeSMAFilter( - window_size=timedelta(minutes=2), precision=2, entity=None, type="last" - ) - for state in self.values: + +async def test_invalid_state(hass): + """Test if filter attributes are inherited.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + ], + } + } + + await async_init_recorder_component(hass) + + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_monitored", STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set("sensor.test_monitored", "invalid") + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == STATE_UNAVAILABLE + + +async def test_outlier(values): + """Test if outlier filter works.""" + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) + for state in values: + filtered = filt.filter_state(state) + assert 21 == filtered.state + + +def test_outlier_step(values): + """ + Test step-change handling in outlier. + + Test if outlier filter handles long-running step-changes correctly. + It should converge to no longer filter once just over half the + window_size is occupied by the new post step-change values. + """ + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=1.1) + values[-1].state = 22 + for state in values: + filtered = filt.filter_state(state) + assert 22 == filtered.state + + +def test_initial_outlier(values): + """Test issue #13363.""" + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) + out = ha.State("sensor.test_monitored", 4000) + for state in [out] + values: + filtered = filt.filter_state(state) + assert 21 == filtered.state + + +def test_unknown_state_outlier(values): + """Test issue #32395.""" + filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) + out = ha.State("sensor.test_monitored", "unknown") + for state in [out] + values + [out]: + try: filtered = filt.filter_state(state) - assert 21.5 == filtered.state + except ValueError: + assert state.state == "unknown" + assert 21 == filtered.state + + +def test_precision_zero(values): + """Test if precision of zero returns an integer.""" + filt = LowPassFilter(window_size=10, precision=0, entity=None, time_constant=10) + for state in values: + filtered = filt.filter_state(state) + assert isinstance(filtered.state, int) + + +def test_lowpass(values): + """Test if lowpass filter works.""" + filt = LowPassFilter(window_size=10, precision=2, entity=None, time_constant=10) + out = ha.State("sensor.test_monitored", "unknown") + for state in [out] + values + [out]: + try: + filtered = filt.filter_state(state) + except ValueError: + assert state.state == "unknown" + assert 18.05 == filtered.state + + +def test_range(values): + """Test if range filter works.""" + lower = 10 + upper = 20 + filt = RangeFilter(entity=None, precision=2, lower_bound=lower, upper_bound=upper) + for unf_state in values: + unf = float(unf_state.state) + filtered = filt.filter_state(unf_state) + if unf < lower: + assert lower == filtered.state + elif unf > upper: + assert upper == filtered.state + else: + assert unf == filtered.state + + +def test_range_zero(values): + """Test if range filter works with zeroes as bounds.""" + lower = 0 + upper = 0 + filt = RangeFilter(entity=None, precision=2, lower_bound=lower, upper_bound=upper) + for unf_state in values: + unf = float(unf_state.state) + filtered = filt.filter_state(unf_state) + if unf < lower: + assert lower == filtered.state + elif unf > upper: + assert upper == filtered.state + else: + assert unf == filtered.state + + +def test_throttle(values): + """Test if lowpass filter works.""" + filt = ThrottleFilter(window_size=3, precision=2, entity=None) + filtered = [] + for state in values: + new_state = filt.filter_state(state) + if not filt.skip_processing: + filtered.append(new_state) + assert [20, 21] == [f.state for f in filtered] + + +def test_time_throttle(values): + """Test if lowpass filter works.""" + filt = TimeThrottleFilter( + window_size=timedelta(minutes=2), precision=2, entity=None + ) + filtered = [] + for state in values: + new_state = filt.filter_state(state) + if not filt.skip_processing: + filtered.append(new_state) + assert [20, 18, 22] == [f.state for f in filtered] + + +def test_time_sma(values): + """Test if time_sma filter works.""" + filt = TimeSMAFilter( + window_size=timedelta(minutes=2), precision=2, entity=None, type="last" + ) + for state in values: + filtered = filt.filter_state(state) + assert 21.5 == filtered.state async def test_reload(hass): """Verify we can reload filter sensors.""" - await hass.async_add_executor_job( - init_recorder_component, hass - ) # force in memory db + await async_init_recorder_component(hass) hass.states.async_set("sensor.test_monitored", 12345) await async_setup_component( diff --git a/tests/components/gios/test_system_health.py b/tests/components/gios/test_system_health.py new file mode 100644 index 0000000000..c58b8b12b5 --- /dev/null +++ b/tests/components/gios/test_system_health.py @@ -0,0 +1,39 @@ +"""Test GIOS system health.""" +import asyncio + +from aiohttp import ClientError + +from homeassistant.components.gios.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import get_system_health_info + + +async def test_gios_system_health(hass, aioclient_mock): + """Test GIOS system health.""" + aioclient_mock.get("http://api.gios.gov.pl/", text="") + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": "ok"} + + +async def test_gios_system_health_fail(hass, aioclient_mock): + """Test GIOS system health.""" + aioclient_mock.get("http://api.gios.gov.pl/", exc=ClientError) + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info == {"can_reach_server": {"type": "failed", "error": "unreachable"}} diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 27e62fafc7..ebe34c2aa9 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -152,7 +152,8 @@ async def test_sync_message(hass): # pylint: disable=redefined-outer-name -async def test_sync_in_area(hass, registries): +@pytest.mark.parametrize("area_on_device", [True, False]) +async def test_sync_in_area(area_on_device, hass, registries): """Test a sync message where room hint comes from area.""" area = registries.area.async_create("Living Room") @@ -160,10 +161,17 @@ async def test_sync_in_area(hass, registries): config_entry_id="1234", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - registries.device.async_update_device(device.id, area_id=area.id) + registries.device.async_update_device( + device.id, area_id=area.id if area_on_device else None + ) entity = registries.entity.async_get_or_create( - "light", "test", "1235", suggested_object_id="demo_light", device_id=device.id + "light", + "test", + "1235", + suggested_object_id="demo_light", + device_id=device.id, + area_id=area.id if not area_on_device else None, ) light = DemoLight( diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 9f3946e6ad..534168fa78 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -159,10 +159,15 @@ async def test_update_connection_failure(hass, discovery, device, mock_now): async def test_update_connection_failure_recovery(hass, discovery, device, mock_now): """Testing update hvac connection failure recovery.""" - device().update_state.side_effect = [DeviceTimeoutError, DEFAULT_MOCK] + device().update_state.side_effect = [ + DeviceTimeoutError, + DeviceTimeoutError, + DEFAULT_MOCK, + ] await async_setup_gree(hass) + # First update becomes unavailable next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) @@ -172,6 +177,7 @@ async def test_update_connection_failure_recovery(hass, discovery, device, mock_ assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE + # Second update restores the connection next_update = mock_now + timedelta(minutes=10) with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) @@ -188,11 +194,6 @@ async def test_update_unhandled_exception(hass, discovery, device, mock_now): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE @@ -221,21 +222,9 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now): assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - device().update_state.side_effect = DeviceTimeoutError device().push_state_update.side_effect = DeviceTimeoutError - # Second update to make an initial error (device is still available) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.name == "fake-device-1" - assert state.state != STATE_UNAVAILABLE - - # Second attempt should make the device unavailable + # Send failure should not raise exceptions or change device state assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -246,47 +235,13 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now): state = hass.states.get(ENTITY_ID) assert state is not None - assert state.state == STATE_UNAVAILABLE - - -async def test_send_command_device_unknown_error(hass, discovery, device, mock_now): - """Test for sending power on command to the device with a device timeout.""" - device().update_state.side_effect = [DEFAULT_MOCK, Exception] - device().push_state_update.side_effect = Exception - - await async_setup_gree(hass) - - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - # First update to make the device available - state = hass.states.get(ENTITY_ID) - assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - assert await hass.services.async_call( - DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, - blocking=True, - ) - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.state == STATE_UNAVAILABLE - async def test_send_power_on(hass, discovery, device, mock_now): """Test for sending power on command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -305,11 +260,6 @@ async def test_send_power_on_device_timeout(hass, discovery, device, mock_now): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -326,11 +276,6 @@ async def test_send_target_temperature(hass, discovery, device, mock_now): """Test for sending target temperature command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, @@ -351,11 +296,6 @@ async def test_send_target_temperature_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, @@ -374,11 +314,6 @@ async def test_update_target_temperature(hass, discovery, device, mock_now): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_TEMPERATURE) == 32 @@ -391,11 +326,6 @@ async def test_send_preset_mode(hass, discovery, device, mock_now, preset): """Test for sending preset mode command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_PRESET_MODE, @@ -412,11 +342,6 @@ async def test_send_invalid_preset_mode(hass, discovery, device, mock_now): """Test for sending preset mode command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -441,11 +366,6 @@ async def test_send_preset_mode_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_PRESET_MODE, @@ -470,11 +390,6 @@ async def test_update_preset_mode(hass, discovery, device, mock_now, preset): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_PRESET_MODE) == preset @@ -495,11 +410,6 @@ async def test_send_hvac_mode(hass, discovery, device, mock_now, hvac_mode): """Test for sending hvac mode command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -524,11 +434,6 @@ async def test_send_hvac_mode_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -559,11 +464,6 @@ async def test_update_hvac_mode(hass, discovery, device, mock_now, hvac_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.state == hvac_mode @@ -577,11 +477,6 @@ async def test_send_fan_mode(hass, discovery, device, mock_now, fan_mode): """Test for sending fan mode command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_MODE, @@ -598,11 +493,6 @@ async def test_send_invalid_fan_mode(hass, discovery, device, mock_now): """Test for sending fan mode command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -628,11 +518,6 @@ async def test_send_fan_mode_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_MODE, @@ -655,11 +540,6 @@ async def test_update_fan_mode(hass, discovery, device, mock_now, fan_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_FAN_MODE) == fan_mode @@ -672,11 +552,6 @@ async def test_send_swing_mode(hass, discovery, device, mock_now, swing_mode): """Test for sending swing mode command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_SWING_MODE, @@ -693,11 +568,6 @@ async def test_send_invalid_swing_mode(hass, discovery, device, mock_now): """Test for sending swing mode command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -722,11 +592,6 @@ async def test_send_swing_mode_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_SWING_MODE, @@ -757,11 +622,6 @@ async def test_update_swing_mode(hass, discovery, device, mock_now, swing_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_SWING_MODE) == swing_mode diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py new file mode 100644 index 0000000000..89a8b224f1 --- /dev/null +++ b/tests/components/gree/test_switch.py @@ -0,0 +1,124 @@ +"""Tests for gree component.""" +from greeclimate.exceptions import DeviceTimeoutError + +from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN +from homeassistant.components.switch import DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +ENTITY_ID = f"{DOMAIN}.fake_device_1_panel_light" + + +async def async_setup_gree(hass): + """Set up the gree switch platform.""" + MockConfigEntry(domain=GREE_DOMAIN).add_to_hass(hass) + await async_setup_component(hass, GREE_DOMAIN, {GREE_DOMAIN: {DOMAIN: {}}}) + await hass.async_block_till_done() + + +async def test_send_panel_light_on(hass, discovery, device): + """Test for sending power on command to the device.""" + await async_setup_gree(hass) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + +async def test_send_panel_light_on_device_timeout(hass, discovery, device): + """Test for sending power on command to the device with a device timeout.""" + device().push_state_update.side_effect = DeviceTimeoutError + + await async_setup_gree(hass) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + +async def test_send_panel_light_off(hass, discovery, device): + """Test for sending power on command to the device.""" + await async_setup_gree(hass) + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_OFF + + +async def test_send_panel_light_toggle(hass, discovery, device): + """Test for sending power on command to the device.""" + await async_setup_gree(hass) + + # Turn the service on first + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + # Toggle it off + assert await hass.services.async_call( + DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_OFF + + # Toggle is back on + assert await hass.services.async_call( + DOMAIN, + SERVICE_TOGGLE, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + + +async def test_panel_light_name(hass, discovery, device): + """Test for name property.""" + await async_setup_gree(hass) + state = hass.states.get(ENTITY_ID) + assert state.attributes[ATTR_FRIENDLY_NAME] == "fake-device-1 Panel Light" diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index c8a9cd6d50..326990e12c 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -61,6 +61,9 @@ async def test_if_not_fires_on_entity_removal(hass, calls): async def test_if_fires_on_entity_change_below(hass, calls): """Test the firing with changed entity.""" + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + context = Context() assert await async_setup_component( hass, @@ -270,6 +273,9 @@ async def test_if_fires_on_initial_entity_above(hass, calls): async def test_if_fires_on_entity_change_above(hass, calls): """Test the firing with changed entity.""" + hass.states.async_set("test.entity", 9) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -378,6 +384,9 @@ async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): async def test_if_fires_on_entity_change_below_range(hass, calls): """Test the firing with changed entity.""" + hass.states.async_set("test.entity", 11) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -500,6 +509,9 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): """Test attributes change.""" + hass.states.async_set("test.entity", 11, {"test_attribute": 11}) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -544,6 +556,9 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, call async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): """Test attributes change.""" + hass.states.async_set("test.entity", "entity", {"test_attribute": 11}) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -636,6 +651,10 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, call async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, calls): """Test attributes change.""" + hass.states.async_set( + "test.entity", "entity", {"test_attribute": 11, "not_test_attribute": 11} + ) + await hass.async_block_till_done() assert await async_setup_component( hass, automation.DOMAIN, @@ -661,6 +680,8 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, async def test_template_list(hass, calls): """Test template list.""" + hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]}) + await hass.async_block_till_done() assert await async_setup_component( hass, automation.DOMAIN, @@ -791,6 +812,9 @@ async def test_if_action(hass, calls): async def test_if_fails_setup_bad_for(hass, calls): """Test for setup failure for bad for.""" + hass.states.async_set("test.entity", 5) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -863,6 +887,10 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): """Test for not firing on entities change with for after stop.""" + hass.states.async_set("test.entity_1", 0) + hass.states.async_set("test.entity_2", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -906,6 +934,9 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): """Test for firing on entity change with for and attribute change.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -941,6 +972,9 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): async def test_if_fires_on_entity_change_with_for(hass, calls): """Test for firing on entity change with for.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -967,6 +1001,9 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): async def test_wait_template_with_trigger(hass, calls): """Test using wait template with 'trigger.entity_id'.""" + hass.states.async_set("test.entity", "0") + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1004,6 +1041,10 @@ async def test_wait_template_with_trigger(hass, calls): async def test_if_fires_on_entities_change_no_overlap(hass, calls): """Test for firing on entities change with no overlap.""" + hass.states.async_set("test.entity_1", 0) + hass.states.async_set("test.entity_2", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1047,6 +1088,10 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): async def test_if_fires_on_entities_change_overlap(hass, calls): """Test for firing on entities change with overlap.""" + hass.states.async_set("test.entity_1", 0) + hass.states.async_set("test.entity_2", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1101,6 +1146,9 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): async def test_if_fires_on_change_with_for_template_1(hass, calls): """Test for firing on change with for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1128,6 +1176,9 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): async def test_if_fires_on_change_with_for_template_2(hass, calls): """Test for firing on change with for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1155,6 +1206,9 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): async def test_if_fires_on_change_with_for_template_3(hass, calls): """Test for firing on change with for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1182,6 +1236,9 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): async def test_invalid_for_template(hass, calls): """Test for invalid for template.""" + hass.states.async_set("test.entity", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, @@ -1207,6 +1264,10 @@ async def test_invalid_for_template(hass, calls): async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): """Test for firing on entities change with overlap and for template.""" + hass.states.async_set("test.entity_1", 0) + hass.states.async_set("test.entity_2", 0) + await hass.async_block_till_done() + assert await async_setup_component( hass, automation.DOMAIN, diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 60dc293c4f..59d6597706 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1,4 +1,6 @@ """Test the HomeKit config flow.""" +import pytest + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.homekit.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT @@ -25,7 +27,6 @@ def _mock_config_entry_with_options_populated(): ], "exclude_entities": ["climate.front_gate"], }, - "auto_start": False, "safe_mode": False, }, ) @@ -46,7 +47,7 @@ async def test_user_form(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {"auto_start": True, "include_domains": ["light"]}, + {"include_domains": ["light"]}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -68,7 +69,6 @@ async def test_user_form(hass): assert result3["title"][:11] == "HASS Bridge" bridge_name = (result3["title"].split(":"))[0] assert result3["data"] == { - "auto_start": True, "filter": { "exclude_domains": [], "exclude_entities": [], @@ -123,7 +123,8 @@ async def test_import(hass): assert len(mock_setup_entry.mock_calls) == 2 -async def test_options_flow_exclude_mode_advanced(hass): +@pytest.mark.parametrize("auto_start", [True, False]) +async def test_options_flow_exclude_mode_advanced(auto_start, hass): """Test config flow options in exclude mode with advanced options.""" config_entry = _mock_config_entry_with_options_populated() @@ -157,12 +158,12 @@ async def test_options_flow_exclude_mode_advanced(hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"auto_start": True, "safe_mode": True}, + user_input={"auto_start": auto_start, "safe_mode": True}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": True, + "auto_start": auto_start, "mode": "bridge", "filter": { "exclude_domains": [], @@ -213,7 +214,7 @@ async def test_options_flow_exclude_mode_basic(hass): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -266,7 +267,7 @@ async def test_options_flow_include_mode_basic(hass): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -332,7 +333,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -387,7 +388,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -454,7 +455,7 @@ async def test_options_flow_include_mode_with_cameras(hass): assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -509,7 +510,7 @@ async def test_options_flow_include_mode_with_cameras(hass): assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "bridge", "filter": { "exclude_domains": [], @@ -603,7 +604,7 @@ async def test_options_flow_include_mode_basic_accessory(hass): assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { - "auto_start": False, + "auto_start": True, "mode": "accessory", "filter": { "exclude_domains": [], diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index e371fa6fe2..acb45bca85 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,6 +1,4 @@ """Test different accessory types: Thermostats.""" -from collections import namedtuple - from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -42,6 +40,14 @@ from homeassistant.components.homekit.const import ( PROP_MIN_STEP, PROP_MIN_VALUE, ) +from homeassistant.components.homekit.type_thermostats import ( + HC_HEAT_COOL_AUTO, + HC_HEAT_COOL_COOL, + HC_HEAT_COOL_HEAT, + HC_HEAT_COOL_OFF, + Thermostat, + WaterHeater, +) from homeassistant.components.water_heater import DOMAIN as DOMAIN_WATER_HEATER from homeassistant.const import ( ATTR_ENTITY_ID, @@ -57,24 +63,9 @@ from homeassistant.helpers import entity_registry from tests.async_mock import patch from tests.common import async_mock_service -from tests.components.homekit.common import patch_debounce -@pytest.fixture(scope="module") -def cls(): - """Patch debounce decorator during import of type_thermostats.""" - patcher = patch_debounce() - patcher.start() - _import = __import__( - "homeassistant.components.homekit.type_thermostats", - fromlist=["WaterHeater", "Thermostat"], - ) - patcher_tuple = namedtuple("Cls", ["water_heater", "thermostat"]) - yield patcher_tuple(thermostat=_import.Thermostat, water_heater=_import.WaterHeater) - patcher.stop() - - -async def test_thermostat(hass, hk_driver, cls, events): +async def test_thermostat(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -94,7 +85,7 @@ async def test_thermostat(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -414,7 +405,7 @@ async def test_thermostat(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "TargetHeatingCoolingState to 3" -async def test_thermostat_auto(hass, hk_driver, cls, events): +async def test_thermostat_auto(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -436,7 +427,7 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -568,14 +559,14 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): ) -async def test_thermostat_humidity(hass, hk_driver, cls, events): +async def test_thermostat_humidity(hass, hk_driver, events): """Test if accessory and HA are updated accordingly with humidity.""" entity_id = "climate.test" # support_auto = True hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -627,7 +618,7 @@ async def test_thermostat_humidity(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "35%" -async def test_thermostat_power_state(hass, hk_driver, cls, events): +async def test_thermostat_power_state(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -650,7 +641,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -747,7 +738,7 @@ async def test_thermostat_power_state(hass, hk_driver, cls, events): assert acc.char_target_heat_cool.value == 2 -async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): +async def test_thermostat_fahrenheit(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" @@ -762,7 +753,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): ) await hass.async_block_till_done() with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() await hass.async_block_till_done() @@ -856,13 +847,13 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "TargetTemperature to 24.0°C" -async def test_thermostat_get_temperature_range(hass, hk_driver, cls): +async def test_thermostat_get_temperature_range(hass, hk_driver): """Test if temperature range is evaluated correctly.""" entity_id = "climate.test" hass.states.async_set(entity_id, HVAC_MODE_OFF) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 2, None) hass.states.async_set( entity_id, HVAC_MODE_OFF, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} @@ -878,13 +869,13 @@ async def test_thermostat_get_temperature_range(hass, hk_driver, cls): assert acc.get_temperature_range() == (15.5, 21.0) -async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): +async def test_thermostat_temperature_step_whole(hass, hk_driver): """Test climate device with single digit precision.""" entity_id = "climate.test" hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_TARGET_TEMP_STEP: 1}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -893,7 +884,7 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1 -async def test_thermostat_restore(hass, hk_driver, cls, events): +async def test_thermostat_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -919,7 +910,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", "climate.simple", 2, None) + acc = Thermostat(hass, hk_driver, "Climate", "climate.simple", 2, None) assert acc.category == 9 assert acc.get_temperature_range() == (7, 35) assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { @@ -929,7 +920,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events): "off", } - acc = cls.thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None) + acc = Thermostat(hass, hk_driver, "Climate", "climate.all_info_set", 2, None) assert acc.category == 9 assert acc.get_temperature_range() == (60.0, 70.0) assert set(acc.char_target_heat_cool.properties["ValidValues"].keys()) == { @@ -938,7 +929,7 @@ async def test_thermostat_restore(hass, hk_driver, cls, events): } -async def test_thermostat_hvac_modes(hass, hk_driver, cls): +async def test_thermostat_hvac_modes(hass, hk_driver): """Test if unsupported HVAC modes are deactivated in HomeKit.""" entity_id = "climate.test" @@ -947,7 +938,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver, cls): ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -971,7 +962,7 @@ async def test_thermostat_hvac_modes(hass, hk_driver, cls): assert acc.char_target_heat_cool.value == 1 -async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls): +async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver): """Test we get heat cool over auto.""" entity_id = "climate.test" @@ -990,7 +981,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls): call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1034,7 +1025,7 @@ async def test_thermostat_hvac_modes_with_auto_heat_cool(hass, hk_driver, cls): assert acc.char_target_heat_cool.value == 3 -async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls): +async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver): """Test we get auto when there is no heat cool.""" entity_id = "climate.test" @@ -1046,7 +1037,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1069,7 +1060,8 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls assert acc.char_target_heat_cool.value == 1 char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] - + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + await hass.async_block_till_done() hk_driver.set_characteristics( { HAP_REPR_CHARS: [ @@ -1090,7 +1082,7 @@ async def test_thermostat_hvac_modes_with_auto_no_heat_cool(hass, hk_driver, cls assert acc.char_target_heat_cool.value == 3 -async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls): +async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver): """Test if unsupported HVAC modes are deactivated in HomeKit.""" entity_id = "climate.test" @@ -1099,7 +1091,7 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls): ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1122,8 +1114,242 @@ async def test_thermostat_hvac_modes_with_auto_only(hass, hk_driver, cls): await hass.async_block_till_done() assert acc.char_target_heat_cool.value == 3 + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + await hass.async_block_till_done() + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_HEAT, + }, + ] + }, + "mock_addr", + ) -async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls): + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_AUTO + + +async def test_thermostat_hvac_modes_with_heat_only(hass, hk_driver): + """Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to heat.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_HEAT, {ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run_handler() + await hass.async_block_till_done() + hap = acc.char_target_heat_cool.to_HAP() + assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_HEAT] + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + await hass.async_block_till_done() + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + + +async def test_thermostat_hvac_modes_with_cool_only(hass, hk_driver): + """Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to cool.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, HVAC_MODE_COOL, {ATTR_HVAC_MODES: [HVAC_MODE_COOL, HVAC_MODE_OFF]} + ) + + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run_handler() + await hass.async_block_till_done() + hap = acc.char_target_heat_cool.to_HAP() + assert hap["valid-values"] == [HC_HEAT_COOL_OFF, HC_HEAT_COOL_COOL] + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + + +async def test_thermostat_hvac_modes_with_heat_cool_only(hass, hk_driver): + """Test if unsupported HVAC modes are deactivated in HomeKit and siri calls get converted to heat or cool.""" + entity_id = "climate.test" + + hass.states.async_set( + entity_id, + HVAC_MODE_COOL, + { + ATTR_CURRENT_TEMPERATURE: 30, + ATTR_HVAC_MODES: [HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_OFF], + }, + ) + + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run_handler() + await hass.async_block_till_done() + hap = acc.char_target_heat_cool.to_HAP() + assert hap["valid-values"] == [ + HC_HEAT_COOL_OFF, + HC_HEAT_COOL_HEAT, + HC_HEAT_COOL_COOL, + ] + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_COOL + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + with pytest.raises(ValueError): + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_AUTO + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + + await hass.async_add_executor_job( + acc.char_target_heat_cool.set_value, HC_HEAT_COOL_HEAT + ) + await hass.async_block_till_done() + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_HEAT + char_target_temp_iid = acc.char_target_temp.to_HAP()[HAP_REPR_IID] + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_temp_iid, + HAP_REPR_VALUE: 0, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_COOL + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_temp_iid, + HAP_REPR_VALUE: 200, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode + assert call_set_hvac_mode[1].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[1].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT + + +async def test_thermostat_hvac_modes_without_off(hass, hk_driver): """Test a thermostat that has no off.""" entity_id = "climate.test" @@ -1132,7 +1358,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls): ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1160,7 +1386,7 @@ async def test_thermostat_hvac_modes_without_off(hass, hk_driver, cls): assert acc.char_target_heat_cool.value == 1 -async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, events): +async def test_thermostat_without_target_temp_only_range(hass, hk_driver, events): """Test a thermostat that only supports a range.""" entity_id = "climate.test" @@ -1171,7 +1397,7 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e {ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE_RANGE}, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1342,13 +1568,13 @@ async def test_thermostat_without_target_temp_only_range(hass, hk_driver, cls, e assert events[-1].data[ATTR_VALUE] == "HeatingThresholdTemperature to 27.0°C" -async def test_water_heater(hass, hk_driver, cls, events): +async def test_water_heater(hass, hk_driver, events): """Test if accessory and HA are updated accordingly.""" entity_id = "water_heater.test" hass.states.async_set(entity_id, HVAC_MODE_HEAT) await hass.async_block_till_done() - acc = cls.water_heater(hass, hk_driver, "WaterHeater", entity_id, 2, None) + acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -1416,14 +1642,14 @@ async def test_water_heater(hass, hk_driver, cls, events): assert acc.char_target_heat_cool.value == 1 -async def test_water_heater_fahrenheit(hass, hk_driver, cls, events): +async def test_water_heater_fahrenheit(hass, hk_driver, events): """Test if accessory and HA are update accordingly.""" entity_id = "water_heater.test" hass.states.async_set(entity_id, HVAC_MODE_HEAT) await hass.async_block_till_done() with patch.object(hass.config.units, CONF_TEMPERATURE_UNIT, new=TEMP_FAHRENHEIT): - acc = cls.water_heater(hass, hk_driver, "WaterHeater", entity_id, 2, None) + acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() @@ -1448,13 +1674,13 @@ async def test_water_heater_fahrenheit(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "140.0°F" -async def test_water_heater_get_temperature_range(hass, hk_driver, cls): +async def test_water_heater_get_temperature_range(hass, hk_driver): """Test if temperature range is evaluated correctly.""" entity_id = "water_heater.test" hass.states.async_set(entity_id, HVAC_MODE_HEAT) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "WaterHeater", entity_id, 2, None) + acc = WaterHeater(hass, hk_driver, "WaterHeater", entity_id, 2, None) hass.states.async_set( entity_id, HVAC_MODE_HEAT, {ATTR_MIN_TEMP: 20, ATTR_MAX_TEMP: 25} @@ -1470,7 +1696,7 @@ async def test_water_heater_get_temperature_range(hass, hk_driver, cls): assert acc.get_temperature_range() == (15.5, 21.0) -async def test_water_heater_restore(hass, hk_driver, cls, events): +async def test_water_heater_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -1492,7 +1718,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "WaterHeater", "water_heater.simple", 2, None) + acc = Thermostat(hass, hk_driver, "WaterHeater", "water_heater.simple", 2, None) assert acc.category == 9 assert acc.get_temperature_range() == (7, 35) assert set(acc.char_current_heat_cool.properties["ValidValues"].keys()) == { @@ -1501,7 +1727,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events): "Off", } - acc = cls.thermostat( + acc = WaterHeater( hass, hk_driver, "WaterHeater", "water_heater.all_info_set", 2, None ) assert acc.category == 9 @@ -1513,7 +1739,7 @@ async def test_water_heater_restore(hass, hk_driver, cls, events): } -async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls, events): +async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, events): """Test if a thermostat that is not ready when we first see it.""" entity_id = "climate.test" @@ -1528,7 +1754,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls, }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1566,7 +1792,7 @@ async def test_thermostat_with_no_modes_when_we_first_see(hass, hk_driver, cls, assert acc.char_display_units.value == 0 -async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events): +async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, events): """Test if a thermostat that is not ready when we first see it that actually does not have off.""" entity_id = "climate.test" @@ -1581,7 +1807,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() @@ -1619,7 +1845,7 @@ async def test_thermostat_with_no_off_after_recheck(hass, hk_driver, cls, events assert acc.char_display_units.value == 0 -async def test_thermostat_with_temp_clamps(hass, hk_driver, cls, events): +async def test_thermostat_with_temp_clamps(hass, hk_driver, events): """Test that tempatures are clamped to valid values to prevent homekit crash.""" entity_id = "climate.test" @@ -1635,7 +1861,7 @@ async def test_thermostat_with_temp_clamps(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) hk_driver.add_accessory(acc) await acc.run_handler() diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index c1a956f3f4..d05e36ed0e 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -12,6 +12,7 @@ from aiohomekit.testing import FakePairing from homeassistant.components.climate.const import ( SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY @@ -40,7 +41,9 @@ async def test_ecobee3_setup(hass): climate_state = await climate_helper.poll_and_get_state() assert climate_state.attributes["friendly_name"] == "HomeW" assert climate_state.attributes["supported_features"] == ( - SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_HUMIDITY + SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_TARGET_HUMIDITY ) assert climate_state.attributes["hvac_modes"] == [ diff --git a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py index fe7b0c7783..a49effdb75 100644 --- a/tests/components/homekit_controller/specific_devices/test_lennox_e30.py +++ b/tests/components/homekit_controller/specific_devices/test_lennox_e30.py @@ -4,7 +4,10 @@ Regression tests for Aqara Gateway V3. https://github.com/home-assistant/core/issues/20885 """ -from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE +from homeassistant.components.climate.const import ( + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) from tests.components.homekit_controller.common import ( Helper, @@ -29,7 +32,7 @@ async def test_lennox_e30_setup(hass): climate_state = await climate_helper.poll_and_get_state() assert climate_state.attributes["friendly_name"] == "Lennox" assert climate_state.attributes["supported_features"] == ( - SUPPORT_TARGET_TEMPERATURE + SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE ) device_registry = await hass.helpers.device_registry.async_get_registry() diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 38156354cd..d3f852d7a4 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -24,6 +24,14 @@ from tests.components.homekit_controller.common import setup_test_component HEATING_COOLING_TARGET = ("thermostat", "heating-cooling.target") HEATING_COOLING_CURRENT = ("thermostat", "heating-cooling.current") +THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD = ( + "thermostat", + "temperature.cooling-threshold", +) +THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD = ( + "thermostat", + "temperature.heating-threshold", +) TEMPERATURE_TARGET = ("thermostat", "temperature.target") TEMPERATURE_CURRENT = ("thermostat", "temperature.current") HUMIDITY_TARGET = ("thermostat", "relative-humidity.target") @@ -42,6 +50,16 @@ def create_thermostat_service(accessory): char = service.add_char(CharacteristicsTypes.HEATING_COOLING_CURRENT) char.value = 0 + char = service.add_char(CharacteristicsTypes.TEMPERATURE_COOLING_THRESHOLD) + char.minValue = 15 + char.maxValue = 40 + char.value = 0 + + char = service.add_char(CharacteristicsTypes.TEMPERATURE_HEATING_THRESHOLD) + char.minValue = 4 + char.maxValue = 30 + char.value = 0 + char = service.add_char(CharacteristicsTypes.TEMPERATURE_TARGET) char.minValue = 7 char.maxValue = 35 @@ -126,6 +144,41 @@ async def test_climate_change_thermostat_state(hass, utcnow): assert helper.characteristics[HEATING_COOLING_TARGET].value == 0 +async def test_climate_check_min_max_values_per_mode(hass, utcnow): + """Test that we we get the appropriate min/max values for each mode.""" + helper = await setup_test_component(hass, create_thermostat_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 7 + assert climate_state.attributes["max_temp"] == 35 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_COOL}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 7 + assert climate_state.attributes["max_temp"] == 35 + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + climate_state = await helper.poll_and_get_state() + assert climate_state.attributes["min_temp"] == 4 + assert climate_state.attributes["max_temp"] == 40 + + async def test_climate_change_thermostat_temperature(hass, utcnow): """Test that we can turn a HomeKit thermostat on and off again.""" helper = await setup_test_component(hass, create_thermostat_service) @@ -147,6 +200,89 @@ async def test_climate_change_thermostat_temperature(hass, utcnow): assert helper.characteristics[TEMPERATURE_TARGET].value == 25 +async def test_climate_change_thermostat_temperature_range(hass, utcnow): + """Test that we can set separate heat and cool setpoints in heat_cool mode.""" + helper = await setup_test_component(hass, create_thermostat_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "hvac_mode": HVAC_MODE_HEAT_COOL, + "target_temp_high": 25, + "target_temp_low": 20, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 22.5 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 20 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 25 + + +async def test_climate_change_thermostat_temperature_range_iphone(hass, utcnow): + """Test that we can set all three set points at once (iPhone heat_cool mode support).""" + helper = await setup_test_component(hass, create_thermostat_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT_COOL}, + blocking=True, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "hvac_mode": HVAC_MODE_HEAT_COOL, + "temperature": 22, + "target_temp_low": 20, + "target_temp_high": 24, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 22 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 20 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 24 + + +async def test_climate_cannot_set_thermostat_temp_range_in_wrong_mode(hass, utcnow): + """Test that we cannot set range values when not in heat_cool mode.""" + helper = await setup_test_component(hass, create_thermostat_service) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_HVAC_MODE, + {"entity_id": "climate.testdevice", "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + + await hass.services.async_call( + DOMAIN, + SERVICE_SET_TEMPERATURE, + { + "entity_id": "climate.testdevice", + "hvac_mode": HVAC_MODE_HEAT_COOL, + "temperature": 22, + "target_temp_low": 20, + "target_temp_high": 24, + }, + blocking=True, + ) + assert helper.characteristics[TEMPERATURE_TARGET].value == 22 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_HEATING_THRESHOLD].value == 0 + assert helper.characteristics[THERMOSTAT_TEMPERATURE_COOLING_THRESHOLD].value == 0 + + async def test_climate_change_thermostat_humidity(hass, utcnow): """Test that we can turn a HomeKit thermostat on and off again.""" helper = await setup_test_component(hass, create_thermostat_service) diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 420977cd40..c392266666 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -540,13 +540,13 @@ async def test_hmip_security_sensor_group(hass, default_mock_hap_factory): assert ha_state.state == STATE_ON -async def test_hmip_wired_multi_contact_interface(hass, default_mock_hap_factory): +async def test_hmip_multi_contact_interface(hass, default_mock_hap_factory): """Test HomematicipMultiContactInterface.""" entity_id = "binary_sensor.wired_eingangsmodul_32_fach_channel5" entity_name = "Wired Eingangsmodul – 32-fach Channel5" device_model = "HmIPW-DRI32" mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=["Wired Eingangsmodul – 32-fach"] + test_devices=["Wired Eingangsmodul – 32-fach", "Licht Flur"] ) ha_state, hmip_device = get_and_check_entity_basics( @@ -563,3 +563,13 @@ async def test_hmip_wired_multi_contact_interface(hass, default_mock_hap_factory await async_manipulate_test_data(hass, hmip_device, "windowState", None, channel=5) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + + ha_state, hmip_device = get_and_check_entity_basics( + hass, + mock_hap, + "binary_sensor.licht_flur_5", + "Licht Flur 5", + "HmIP-FCI6", + ) + + assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 7ef0e3d670..a35576ed35 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -43,7 +43,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "set_shutter_level" - assert hmip_device.mock_calls[-1][1] == (0,) + assert hmip_device.mock_calls[-1][1] == (0, 1) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -57,7 +57,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 3 assert hmip_device.mock_calls[-1][0] == "set_shutter_level" - assert hmip_device.mock_calls[-1][1] == (0.5,) + assert hmip_device.mock_calls[-1][1] == (0.5, 1) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0.5) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -68,7 +68,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 5 assert hmip_device.mock_calls[-1][0] == "set_shutter_level" - assert hmip_device.mock_calls[-1][1] == (1,) + assert hmip_device.mock_calls[-1][1] == (1, 1) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 1) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_CLOSED @@ -79,7 +79,7 @@ async def test_hmip_cover_shutter(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 7 assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) ha_state = hass.states.get(entity_id) @@ -109,7 +109,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (0,) + assert hmip_device.mock_calls[-1][1] == (0, 1) await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0) ha_state = hass.states.get(entity_id) @@ -125,7 +125,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 4 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (0.5,) + assert hmip_device.mock_calls[-1][1] == (0.5, 1) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -137,7 +137,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 6 assert hmip_device.mock_calls[-1][0] == "set_slats_level" - assert hmip_device.mock_calls[-1][1] == (1,) + assert hmip_device.mock_calls[-1][1] == (1, 1) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OPEN @@ -149,7 +149,7 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 8 assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "slatsLevel", None) ha_state = hass.states.get(entity_id) @@ -160,6 +160,194 @@ async def test_hmip_cover_slats(hass, default_mock_hap_factory): assert ha_state.state == STATE_UNKNOWN +async def test_hmip_multi_cover_slats(hass, default_mock_hap_factory): + """Test HomematicipCoverSlats.""" + entity_id = "cover.wohnzimmer_fenster" + entity_name = "Wohnzimmer Fenster" + device_model = "HmIP-DRBLI4" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Jalousieaktor 1 für Hutschienenmontage – 4-fach"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 1, channel=4) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1, channel=4) + ha_state = hass.states.get(entity_id) + + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0, 4) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0, channel=4) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0, channel=4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + await hass.services.async_call( + "cover", + "set_cover_tilt_position", + {"entity_id": entity_id, "tilt_position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0.5, 4) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5, channel=4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 + + await hass.services.async_call( + "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 6 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (1, 4) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1, channel=4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 8 + assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" + assert hmip_device.mock_calls[-1][1] == (4,) + + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", None, channel=4) + ha_state = hass.states.get(entity_id) + assert not ha_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None, channel=4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNKNOWN + + +async def test_hmip_blind_module(hass, default_mock_hap_factory): + """Test HomematicipBlindModule.""" + entity_id = "cover.sonnenschutz_balkontur" + entity_name = "Sonnenschutz Balkontür" + device_model = "HmIP-HDM1" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=[entity_name] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 5 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_secondary_shading_level" + assert hmip_device.mock_calls[-1][2] == { + "primaryShadingLevel": 0.94956, + "secondaryShadingLevel": 0, + } + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 0) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 0) + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + + assert hmip_device.mock_calls[-1][0] == "set_primary_shading_level" + assert hmip_device.mock_calls[-1][2] == {"primaryShadingLevel": 0} + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 0.5) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 0.5) + await hass.services.async_call( + "cover", + "set_cover_tilt_position", + {"entity_id": entity_id, "tilt_position": "50"}, + blocking=True, + ) + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": entity_id, "position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 8 + + assert hmip_device.mock_calls[-1][0] == "set_primary_shading_level" + assert hmip_device.mock_calls[-1][2] == {"primaryShadingLevel": 0.5} + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 50 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", 1) + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", 1) + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + await hass.services.async_call( + "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 12 + + assert hmip_device.mock_calls[-1][0] == "set_secondary_shading_level" + assert hmip_device.mock_calls[-1][2] == { + "primaryShadingLevel": 1, + "secondaryShadingLevel": 1, + } + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 13 + assert hmip_device.mock_calls[-1][0] == "stop" + assert hmip_device.mock_calls[-1][1] == () + + await hass.services.async_call( + "cover", "stop_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 14 + assert hmip_device.mock_calls[-1][0] == "stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "secondaryShadingLevel", None) + ha_state = hass.states.get(entity_id) + assert not ha_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + + await async_manipulate_test_data(hass, hmip_device, "primaryShadingLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNKNOWN + + async def test_hmip_garage_door_tormatic(hass, default_mock_hap_factory): """Test HomematicipCoverShutte.""" entity_id = "cover.garage_door_module" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 31e62a1a71..0e69a67cdb 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 233 + assert len(mock_hap.hmip_device_by_entity_id) == 250 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 8ab62019c3..b4dbd0d140 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -175,7 +175,7 @@ async def test_hmip_dimmer(hass, default_mock_hap_factory): "light", "turn_on", {"entity_id": entity_id}, blocking=True ) assert hmip_device.mock_calls[-1][0] == "set_dim_level" - assert hmip_device.mock_calls[-1][1] == (1,) + assert hmip_device.mock_calls[-1][1] == (1, 1) await hass.services.async_call( "light", @@ -185,7 +185,7 @@ async def test_hmip_dimmer(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 2 assert hmip_device.mock_calls[-1][0] == "set_dim_level" - assert hmip_device.mock_calls[-1][1] == (1.0,) + assert hmip_device.mock_calls[-1][1] == (1.0, 1) await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON @@ -196,7 +196,7 @@ async def test_hmip_dimmer(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 4 assert hmip_device.mock_calls[-1][0] == "set_dim_level" - assert hmip_device.mock_calls[-1][1] == (0,) + assert hmip_device.mock_calls[-1][1] == (0, 1) await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF @@ -245,3 +245,55 @@ async def test_hmip_light_measuring(hass, default_mock_hap_factory): await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + + +async def test_hmip_wired_multi_dimmer(hass, default_mock_hap_factory): + """Test HomematicipMultiDimmer.""" + entity_id = "light.raumlich_kuche" + entity_name = "Raumlich (Küche)" + device_model = "HmIPW-DRD3" + mock_hap = await default_mock_hap_factory.async_get_mock_hap( + test_devices=["Wired Dimmaktor – 3-fach (Küche)"] + ) + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1, 1) + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness": "100"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 2 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0.39215686274509803, 1) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0, 1) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None, channel=1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index 034ca33aec..f2b3dfba32 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -43,7 +43,7 @@ async def test_hmip_switch(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "turn_off" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF @@ -53,7 +53,7 @@ async def test_hmip_switch(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 3 assert hmip_device.mock_calls[-1][0] == "turn_on" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", True) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON @@ -80,7 +80,7 @@ async def test_hmip_switch_input(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "turn_off" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF @@ -90,7 +90,7 @@ async def test_hmip_switch_input(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 3 assert hmip_device.mock_calls[-1][0] == "turn_on" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", True) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON @@ -117,7 +117,7 @@ async def test_hmip_switch_measuring(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 1 assert hmip_device.mock_calls[-1][0] == "turn_off" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", False) ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF @@ -127,7 +127,7 @@ async def test_hmip_switch_measuring(hass, default_mock_hap_factory): ) assert len(hmip_device.mock_calls) == service_call_counter + 3 assert hmip_device.mock_calls[-1][0] == "turn_on" - assert hmip_device.mock_calls[-1][1] == () + assert hmip_device.mock_calls[-1][1] == (1,) await async_manipulate_test_data(hass, hmip_device, "on", True) await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) ha_state = hass.states.get(entity_id) @@ -191,6 +191,7 @@ async def test_hmip_multi_switch(hass, default_mock_hap_factory): "Multi IO Box", "Heizungsaktor", "ioBroker", + "Schaltaktor Verteiler", ] ) @@ -221,6 +222,16 @@ async def test_hmip_multi_switch(hass, default_mock_hap_factory): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + ha_state, hmip_device = get_and_check_entity_basics( + hass, + mock_hap, + "switch.schaltaktor_verteiler_channel3", + "Schaltaktor Verteiler Channel3", + "HmIP-DRSI4", + ) + + assert ha_state.state == STATE_OFF + async def test_hmip_wired_multi_switch(hass, default_mock_hap_factory): """Test HomematicipMultiSwitch.""" diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index a2febcca2a..31a6c49eeb 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -50,6 +50,20 @@ TEST_INSTANCE_3: Dict[str, Any] = { "running": True, } +TEST_AUTH_REQUIRED_RESP: Dict[str, Any] = { + "command": "authorize-tokenRequired", + "info": { + "required": True, + }, + "success": True, + "tan": 1, +} + +TEST_AUTH_NOT_REQUIRED_RESP = { + **TEST_AUTH_REQUIRED_RESP, + "info": {"required": False}, +} + _LOGGER = logging.getLogger(__name__) @@ -78,12 +92,7 @@ def create_mock_client() -> Mock: mock_client.async_client_connect = AsyncMock(return_value=True) mock_client.async_client_disconnect = AsyncMock(return_value=True) mock_client.async_is_auth_required = AsyncMock( - return_value={ - "command": "authorize-tokenRequired", - "info": {"required": False}, - "success": True, - "tan": 1, - } + return_value=TEST_AUTH_NOT_REQUIRED_RESP ) mock_client.async_login = AsyncMock( return_value={"command": "authorize-login", "success": True, "tan": 0} @@ -91,6 +100,17 @@ def create_mock_client() -> Mock: mock_client.async_sysinfo_id = AsyncMock(return_value=TEST_SYSINFO_ID) mock_client.async_sysinfo_version = AsyncMock(return_value=TEST_SYSINFO_ID) + mock_client.async_client_switch_instance = AsyncMock(return_value=True) + mock_client.async_client_login = AsyncMock(return_value=True) + mock_client.async_get_serverinfo = AsyncMock( + return_value={ + "command": "serverinfo", + "success": True, + "tan": 0, + "info": {"fake": "data"}, + } + ) + mock_client.adjustment = None mock_client.effects = None mock_client.instances = [ @@ -100,12 +120,15 @@ def create_mock_client() -> Mock: return mock_client -def add_test_config_entry(hass: HomeAssistantType) -> ConfigEntry: +def add_test_config_entry( + hass: HomeAssistantType, data: Optional[Dict[str, Any]] = None +) -> ConfigEntry: """Add a test config entry.""" config_entry: MockConfigEntry = MockConfigEntry( # type: ignore[no-untyped-call] entry_id=TEST_CONFIG_ENTRY_ID, domain=DOMAIN, - data={ + data=data + or { CONF_HOST: TEST_HOST, CONF_PORT: TEST_PORT, }, @@ -118,10 +141,12 @@ def add_test_config_entry(hass: HomeAssistantType) -> ConfigEntry: async def setup_test_config_entry( - hass: HomeAssistantType, hyperion_client: Optional[Mock] = None + hass: HomeAssistantType, + config_entry: Optional[ConfigEntry] = None, + hyperion_client: Optional[Mock] = None, ) -> ConfigEntry: """Add a test Hyperion entity to hass.""" - config_entry = add_test_config_entry(hass) + config_entry = config_entry or add_test_config_entry(hass) hyperion_client = hyperion_client or create_mock_client() # pylint: disable=attribute-defined-outside-init diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 807a3829e7..481b795784 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -11,10 +11,14 @@ from homeassistant.components.hyperion.const import ( CONF_CREATE_TOKEN, CONF_PRIORITY, DOMAIN, - SOURCE_IMPORT, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_IMPORT, + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, +) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, @@ -25,6 +29,7 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from . import ( + TEST_AUTH_REQUIRED_RESP, TEST_CONFIG_ENTRY_ID, TEST_ENTITY_ID_1, TEST_HOST, @@ -49,15 +54,6 @@ TEST_HOST_PORT: Dict[str, Any] = { CONF_PORT: TEST_PORT, } -TEST_AUTH_REQUIRED_RESP = { - "command": "authorize-tokenRequired", - "info": { - "required": True, - }, - "success": True, - "tan": 1, -} - TEST_AUTH_ID = "ABCDE" TEST_REQUEST_TOKEN_SUCCESS = { "command": "authorize-requestToken", @@ -694,3 +690,62 @@ async def test_options(hass: HomeAssistantType) -> None: blocking=True, ) assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority + + +async def test_reauth_success(hass: HomeAssistantType) -> None: + """Check a reauth flow that succeeds.""" + + config_data = { + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + } + + config_entry = add_test_config_entry(hass, data=config_data) + client = create_mock_client() + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch("homeassistant.components.hyperion.async_setup", return_value=True), patch( + "homeassistant.components.hyperion.async_setup_entry", return_value=True + ): + result = await _init_flow( + hass, + source=SOURCE_REAUTH, + data=config_data, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await _configure_flow( + hass, result, user_input={CONF_CREATE_TOKEN: False, CONF_TOKEN: TEST_TOKEN} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert CONF_TOKEN in config_entry.data + + +async def test_reauth_cannot_connect(hass: HomeAssistantType) -> None: + """Check a reauth flow that fails to connect.""" + + config_data = { + CONF_HOST: TEST_HOST, + CONF_PORT: TEST_PORT, + } + + add_test_config_entry(hass, data=config_data) + client = create_mock_client() + client.async_client_connect = AsyncMock(return_value=False) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ): + result = await _init_flow( + hass, + source=SOURCE_REAUTH, + data=config_data, + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 5366f6e14d..4636a9ad59 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -17,12 +17,26 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, DOMAIN as LIGHT_DOMAIN, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.config_entries import ( + ENTRY_STATE_SETUP_ERROR, + SOURCE_REAUTH, + ConfigEntry, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_PORT, + CONF_SOURCE, + CONF_TOKEN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import HomeAssistantType from . import ( + TEST_AUTH_NOT_REQUIRED_RESP, + TEST_AUTH_REQUIRED_RESP, TEST_CONFIG_ENTRY_OPTIONS, TEST_ENTITY_ID_1, TEST_ENTITY_ID_2, @@ -206,7 +220,9 @@ async def test_setup_config_entry(hass: HomeAssistantType) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is not None -async def test_setup_config_entry_not_ready(hass: HomeAssistantType) -> None: +async def test_setup_config_entry_not_ready_connect_fail( + hass: HomeAssistantType, +) -> None: """Test the component not being ready.""" client = create_mock_client() client.async_client_connect = AsyncMock(return_value=False) @@ -214,6 +230,32 @@ async def test_setup_config_entry_not_ready(hass: HomeAssistantType) -> None: assert hass.states.get(TEST_ENTITY_ID_1) is None +async def test_setup_config_entry_not_ready_switch_instance_fail( + hass: HomeAssistantType, +) -> None: + """Test the component not being ready.""" + client = create_mock_client() + client.async_client_switch_instance = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_ENTITY_ID_1) is None + + +async def test_setup_config_entry_not_ready_load_state_fail( + hass: HomeAssistantType, +) -> None: + """Test the component not being ready.""" + client = create_mock_client() + client.async_get_serverinfo = AsyncMock( + return_value={ + "command": "serverinfo", + "success": False, + } + ) + + await setup_test_config_entry(hass, hyperion_client=client) + assert hass.states.get(TEST_ENTITY_ID_1) is None + + async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None: """Test dynamic changes in the omstamce configuration.""" config_entry = add_test_config_entry(hass) @@ -724,7 +766,7 @@ async def test_unload_entry(hass: HomeAssistantType) -> None: client = create_mock_client() await setup_test_config_entry(hass, hyperion_client=client) assert hass.states.get(TEST_ENTITY_ID_1) is not None - assert client.async_client_connect.called + assert client.async_client_connect.call_count == 2 assert not client.async_client_disconnect.called entry = _get_config_entry_from_unique_id(hass, TEST_SYSINFO_ID) assert entry @@ -749,3 +791,44 @@ async def test_version_no_log_warning(caplog, hass: HomeAssistantType) -> None: await setup_test_config_entry(hass, hyperion_client=client) assert hass.states.get(TEST_ENTITY_ID_1) is not None assert "Please consider upgrading" not in caplog.text + + +async def test_setup_entry_no_token_reauth(hass: HomeAssistantType) -> None: + """Verify a reauth flow when auth is required but no token provided.""" + client = create_mock_client() + config_entry = add_test_config_entry(hass) + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_REQUIRED_RESP) + + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + mock_flow_init.assert_called_once_with( + DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + data=config_entry.data, + ) + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + +async def test_setup_entry_bad_token_reauth(hass: HomeAssistantType) -> None: + """Verify a reauth flow when a bad token is provided.""" + client = create_mock_client() + config_entry = add_test_config_entry( + hass, + data={CONF_HOST: TEST_HOST, CONF_PORT: TEST_PORT, CONF_TOKEN: "expired_token"}, + ) + client.async_is_auth_required = AsyncMock(return_value=TEST_AUTH_NOT_REQUIRED_RESP) + + # Fail to log in. + client.async_client_login = AsyncMock(return_value=False) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", return_value=client + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + assert not await hass.config_entries.async_setup(config_entry.entry_id) + mock_flow_init.assert_called_once_with( + DOMAIN, + context={CONF_SOURCE: SOURCE_REAUTH}, + data=config_entry.data, + ) + assert config_entry.state == ENTRY_STATE_SETUP_ERROR diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index b57d8c9761..db8eb6b245 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -3,13 +3,13 @@ import os import shutil import tempfile -import unittest + +import pytest import homeassistant.components.kira as kira -from homeassistant.setup import setup_component +from homeassistant.setup import async_setup_component from tests.async_mock import MagicMock, patch -from tests.common import get_test_home_assistant TEST_CONFIG = { kira.DOMAIN: { @@ -31,57 +31,58 @@ KIRA_CODES = """ """ -class TestKiraSetup(unittest.TestCase): - """Test class for kira.""" +@pytest.fixture(autouse=True) +def setup_comp(): + """Set up things to be run when tests are started.""" + _base_mock = MagicMock() + pykira = _base_mock.pykira + pykira.__file__ = "test" + _module_patcher = patch.dict("sys.modules", {"pykira": pykira}) + _module_patcher.start() + yield + _module_patcher.stop() - # pylint: disable=invalid-name - def setUp(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - _base_mock = MagicMock() - pykira = _base_mock.pykira - pykira.__file__ = "test" - self._module_patcher = patch.dict("sys.modules", {"pykira": pykira}) - self._module_patcher.start() - self.work_dir = tempfile.mkdtemp() - self.addCleanup(self.tear_down_cleanup) +@pytest.fixture(scope="module") +def work_dir(): + """Set up temporary workdir.""" + work_dir = tempfile.mkdtemp() + yield work_dir + shutil.rmtree(work_dir, ignore_errors=True) - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - self._module_patcher.stop() - shutil.rmtree(self.work_dir, ignore_errors=True) - def test_kira_empty_config(self): - """Kira component should load a default sensor.""" - setup_component(self.hass, kira.DOMAIN, {}) - assert len(self.hass.data[kira.DOMAIN]["sensor"]) == 1 +async def test_kira_empty_config(hass): + """Kira component should load a default sensor.""" + await async_setup_component(hass, kira.DOMAIN, {kira.DOMAIN: {}}) + assert len(hass.data[kira.DOMAIN]["sensor"]) == 1 - def test_kira_setup(self): - """Ensure platforms are loaded correctly.""" - setup_component(self.hass, kira.DOMAIN, TEST_CONFIG) - assert len(self.hass.data[kira.DOMAIN]["sensor"]) == 2 - assert sorted(self.hass.data[kira.DOMAIN]["sensor"].keys()) == [ - "kira", - "kira_1", - ] - assert len(self.hass.data[kira.DOMAIN]["remote"]) == 2 - assert sorted(self.hass.data[kira.DOMAIN]["remote"].keys()) == [ - "kira", - "kira_1", - ] - def test_kira_creates_codes(self): - """Kira module should create codes file if missing.""" - code_path = os.path.join(self.work_dir, "codes.yaml") - kira.load_codes(code_path) - assert os.path.exists(code_path), "Kira component didn't create codes file" +async def test_kira_setup(hass): + """Ensure platforms are loaded correctly.""" + await async_setup_component(hass, kira.DOMAIN, TEST_CONFIG) + assert len(hass.data[kira.DOMAIN]["sensor"]) == 2 + assert sorted(hass.data[kira.DOMAIN]["sensor"].keys()) == [ + "kira", + "kira_1", + ] + assert len(hass.data[kira.DOMAIN]["remote"]) == 2 + assert sorted(hass.data[kira.DOMAIN]["remote"].keys()) == [ + "kira", + "kira_1", + ] - def test_load_codes(self): - """Kira should ignore invalid codes.""" - code_path = os.path.join(self.work_dir, "codes.yaml") - with open(code_path, "w") as code_file: - code_file.write(KIRA_CODES) - res = kira.load_codes(code_path) - assert len(res) == 1, "Expected exactly 1 valid Kira code" + +async def test_kira_creates_codes(work_dir): + """Kira module should create codes file if missing.""" + code_path = os.path.join(work_dir, "codes.yaml") + kira.load_codes(code_path) + assert os.path.exists(code_path), "Kira component didn't create codes file" + + +async def test_load_codes(work_dir): + """Kira should ignore invalid codes.""" + code_path = os.path.join(work_dir, "codes.yaml") + with open(code_path, "w") as code_file: + code_file.write(KIRA_CODES) + res = kira.load_codes(code_path) + assert len(res) == 1, "Expected exactly 1 valid Kira code" diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 3aa70aa48b..6c1d9312f9 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the Meteo-France config flow.""" -from meteofrance.model import Place +from meteofrance_api.model import Place import pytest from homeassistant import data_entry_flow diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index faa3e7115b..4a25026959 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -8,10 +8,54 @@ from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_N from homeassistant.components.motion_blinds.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST -from tests.async_mock import patch +from tests.async_mock import Mock, patch TEST_HOST = "1.2.3.4" +TEST_HOST2 = "5.6.7.8" TEST_API_KEY = "12ab345c-d67e-8f" +TEST_MAC = "ab:cd:ef:gh" +TEST_MAC2 = "ij:kl:mn:op" +TEST_DEVICE_LIST = {TEST_MAC: Mock()} + +TEST_DISCOVERY_1 = { + TEST_HOST: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + {"mac": "abcdefghujkl0002", "deviceType": "10000000"}, + ], + } +} + +TEST_DISCOVERY_2 = { + TEST_HOST: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + ], + }, + TEST_HOST2: { + "msgType": "GetDeviceListAck", + "mac": TEST_MAC2, + "deviceType": "02000002", + "ProtocolVersion": "0.9", + "token": "12345A678B9CDEFG", + "data": [ + {"mac": "abcdefghujkl", "deviceType": "02000002"}, + {"mac": "abcdefghujkl0001", "deviceType": "10000000"}, + ], + }, +} @pytest.fixture(name="motion_blinds_connect", autouse=True) @@ -23,6 +67,12 @@ def motion_blinds_connect_fixture(): ), patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.Update", return_value=True, + ), patch( + "homeassistant.components.motion_blinds.gateway.MotionGateway.device_list", + TEST_DEVICE_LIST, + ), patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value=TEST_DISCOVERY_1, ), patch( "homeassistant.components.motion_blinds.async_setup_entry", return_value=True ): @@ -41,7 +91,16 @@ async def test_config_flow_manual_host_success(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY}, + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, ) assert result["type"] == "create_entry" @@ -52,6 +111,87 @@ async def test_config_flow_manual_host_success(hass): } +async def test_config_flow_discovery_1_success(hass): + """Successful flow with 1 gateway discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST, + CONF_API_KEY: TEST_API_KEY, + } + + +async def test_config_flow_discovery_2_success(hass): + """Successful flow with 2 gateway discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value=TEST_DISCOVERY_2, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "select" + assert result["data_schema"].schema["select_ip"].container == [ + TEST_HOST, + TEST_HOST2, + ] + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"select_ip": TEST_HOST2}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: TEST_API_KEY}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == DEFAULT_GATEWAY_NAME + assert result["data"] == { + CONF_HOST: TEST_HOST2, + CONF_API_KEY: TEST_API_KEY, + } + + async def test_config_flow_connection_error(hass): """Failed flow manually initialized by the user with connection timeout.""" result = await hass.config_entries.flow.async_init( @@ -62,14 +202,47 @@ async def test_config_flow_connection_error(hass): assert result["step_id"] == "user" assert result["errors"] == {} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: TEST_HOST}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "connect" + assert result["errors"] is None + with patch( "homeassistant.components.motion_blinds.gateway.MotionGateway.GetDeviceList", side_effect=socket.timeout, ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: TEST_HOST, CONF_API_KEY: TEST_API_KEY}, + {CONF_API_KEY: TEST_API_KEY}, ) assert result["type"] == "abort" assert result["reason"] == "connection_error" + + +async def test_config_flow_discovery_fail(hass): + """Failed flow with no gateways discovered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.motion_blinds.config_flow.MotionDiscovery.discover", + return_value={}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {"base": "discovery_error"} diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 4d049753f4..0a9c1dc610 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -133,9 +133,9 @@ async def test_set_operation_bad_attr_and_state(hass, mqtt_mock, caplog): assert state.state == "off" with pytest.raises(vol.Invalid) as excinfo: await common.async_set_hvac_mode(hass, None, ENTITY_CLIMATE) - assert ("value is not allowed for dictionary value @ data['hvac_mode']") in str( - excinfo.value - ) + assert ( + "value must be one of ['auto', 'cool', 'dry', 'fan_only', 'heat', 'heat_cool', 'off'] for dictionary value @ data['hvac_mode']" + ) in str(excinfo.value) state = hass.states.get(ENTITY_CLIMATE) assert state.state == "off" diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py new file mode 100644 index 0000000000..4ee6986e59 --- /dev/null +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -0,0 +1,361 @@ +"""The tests for the MQTT device_tracker discovery platform.""" + +import pytest + +from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNKNOWN + +from tests.common import async_fire_mqtt_message, mock_device_registry, mock_registry + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_discover_device_tracker(hass, mqtt_mock, caplog): + """Test discovering an MQTT device tracker component.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test_topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state is not None + assert state.name == "test" + assert ("device_tracker", "bla") in hass.data[ALREADY_DISCOVERED] + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is None + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "required-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Beer" + + +async def test_non_duplicate_device_tracker_discovery(hass, mqtt_mock, caplog): + """Test for a non duplicate component.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + state_duplicate = hass.states.get("device_tracker.beer1") + + assert state is not None + assert state.name == "Beer" + assert state_duplicate is None + assert "Component has already been discovered: device_tracker bla" in caplog.text + + +async def test_device_tracker_removal(hass, mqtt_mock, caplog): + """Test removal of component through empty discovery message.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is None + + +async def test_device_tracker_rediscover(hass, mqtt_mock, caplog): + """Test rediscover of removed component.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is None + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("device_tracker.beer") + assert state is not None + + +async def test_duplicate_device_tracker_removal(hass, mqtt_mock, caplog): + """Test for a non duplicate component.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + assert "Component has already been discovered: device_tracker bla" in caplog.text + caplog.clear() + async_fire_mqtt_message(hass, "homeassistant/device_tracker/bla/config", "") + await hass.async_block_till_done() + + assert ( + "Component has already been discovered: device_tracker bla" not in caplog.text + ) + + +async def test_device_tracker_discovery_update(hass, mqtt_mock, caplog): + """Test for a discovery update event.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Beer" + + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "Cider", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.beer") + assert state is not None + assert state.name == "Cider" + + +async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): + """Test discvered device is cleaned up when removed from registry.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "device":{"identifiers":["0AFFD2"]},' + ' "state_topic": "foobar/tracker",' + ' "unique_id": "unique" }', + ) + await hass.async_block_till_done() + + # Verify device and registry entries are created + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is not None + entity_entry = entity_reg.async_get("device_tracker.mqtt_unique") + assert entity_entry is not None + + state = hass.states.get("device_tracker.mqtt_unique") + assert state is not None + + device_reg.async_remove_device(device_entry.id) + await hass.async_block_till_done() + + # Verify device and registry entries are cleared + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is None + entity_entry = entity_reg.async_get("device_tracker.mqtt_unique") + assert entity_entry is None + + # Verify state is removed + state = hass.states.get("device_tracker.mqtt_unique") + assert state is None + await hass.async_block_till_done() + + # Verify retained discovery topic has been cleared + mqtt_mock.async_publish.assert_called_once_with( + "homeassistant/device_tracker/bla/config", "", 0, True + ) + + +async def test_setting_device_tracker_value_via_mqtt_message(hass, mqtt_mock, caplog): + """Test the setting of the value via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test-topic" }', + ) + + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "not_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_value_via_mqtt_message_and_template( + hass, mqtt_mock, caplog +): + """Test the setting of the value via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{" + '"name": "test", ' + '"state_topic": "test-topic", ' + '"value_template": "{% if value is equalto \\"proxy_for_home\\" %}home{% else %}not_home{% endif %}" ' + "}", + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test-topic", "proxy_for_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "anything_for_not_home") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_value_via_mqtt_message_and_template2( + hass, mqtt_mock, caplog +): + """Test the setting of the value via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{" + '"name": "test", ' + '"state_topic": "test-topic", ' + '"value_template": "{{ value | lower }}" ' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "HOME") + state = hass.states.get("device_Tracker.test") + assert state.state == STATE_HOME + + async_fire_mqtt_message(hass, "test-topic", "NOT_HOME") + state = hass.states.get("device_tracker.test") + assert state.state == STATE_NOT_HOME + + +async def test_setting_device_tracker_location_via_mqtt_message( + hass, mqtt_mock, caplog +): + """Test the setting of the location via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + '{ "name": "test", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "test-topic", "test-location") + state = hass.states.get("device_tracker.test") + assert state.state == "test-location" + + +async def test_setting_device_tracker_location_via_lat_lon_message( + hass, mqtt_mock, caplog +): + """Test the setting of the latitude and longitude via MQTT.""" + async_fire_mqtt_message( + hass, + "homeassistant/device_tracker/bla/config", + "{ " + '"name": "test", ' + '"state_topic": "test-topic", ' + '"json_attributes_topic": "attributes-topic" ' + "}", + ) + await hass.async_block_till_done() + + state = hass.states.get("device_tracker.test") + + assert state.state == STATE_UNKNOWN + + hass.config.latitude = 32.87336 + hass.config.longitude = -117.22743 + + async_fire_mqtt_message( + hass, + "attributes-topic", + '{"latitude":32.87336,"longitude": -117.22743, "gps_accuracy":1.5}', + ) + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 32.87336 + assert state.attributes["longitude"] == -117.22743 + assert state.attributes["gps_accuracy"] == 1.5 + assert state.state == STATE_HOME + + async_fire_mqtt_message( + hass, + "attributes-topic", + '{"latitude":50.1,"longitude": -2.1, "gps_accuracy":1.5}', + ) + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 50.1 + assert state.attributes["longitude"] == -2.1 + assert state.attributes["gps_accuracy"] == 1.5 + assert state.state == STATE_NOT_HOME + + async_fire_mqtt_message(hass, "attributes-topic", '{"longitude": -117.22743}') + state = hass.states.get("device_tracker.test") + assert state.attributes["longitude"] == -117.22743 + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "attributes-topic", '{"latitude":32.87336}') + state = hass.states.get("device_tracker.test") + assert state.attributes["latitude"] == 32.87336 + assert state.state == STATE_NOT_HOME diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 86b905f2b0..82a88de918 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -781,6 +781,18 @@ async def test_custom_birth_message(hass, mqtt_client_mock, mqtt_mock): mqtt_client_mock.publish.assert_called_with("birth", "birth", 0, False) +@pytest.mark.parametrize( + "mqtt_config", + [ + { + mqtt.CONF_BROKER: "mock-broker", + mqtt.CONF_BIRTH_MESSAGE: { + mqtt.ATTR_TOPIC: "homeassistant/status", + mqtt.ATTR_PAYLOAD: "online", + }, + } + ], +) async def test_default_birth_message(hass, mqtt_client_mock, mqtt_mock): """Test sending birth message.""" birth = asyncio.Event() diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 31c2cddd09..6954eb1b7a 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -1,160 +1,156 @@ -"""Tests for the Neato config flow.""" -from pybotvac.exceptions import NeatoLoginException, NeatoRobotException -import pytest +"""Test the Neato Botvac config flow.""" +from pybotvac.neato import Neato -from homeassistant import data_entry_flow -from homeassistant.components.neato import config_flow -from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.neato.const import NEATO_DOMAIN +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.typing import HomeAssistantType from tests.async_mock import patch from tests.common import MockConfigEntry -USERNAME = "myUsername" -PASSWORD = "myPassword" -VENDOR_NEATO = "neato" -VENDOR_VORWERK = "vorwerk" -VENDOR_INVALID = "invalid" +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + +VENDOR = Neato() +OAUTH2_AUTHORIZE = VENDOR.auth_endpoint +OAUTH2_TOKEN = VENDOR.token_endpoint -@pytest.fixture(name="account") -def mock_controller_login(): - """Mock a successful login.""" - with patch("homeassistant.components.neato.config_flow.Account", return_value=True): - yield - - -def init_config_flow(hass): - """Init a configuration flow.""" - flow = config_flow.NeatoConfigFlow() - flow.hass = hass - return flow - - -async def test_user(hass, account): - """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_VENDOR] == VENDOR_NEATO - - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_VORWERK} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_VENDOR] == VENDOR_VORWERK - - -async def test_import(hass, account): - """Test import step.""" - flow = init_config_flow(hass) - - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == f"{USERNAME} (from configuration)" - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_VENDOR] == VENDOR_NEATO - - -async def test_abort_if_already_setup(hass, account): - """Test we abort if Neato is already setup.""" - flow = init_config_flow(hass) - MockConfigEntry( - domain=NEATO_DOMAIN, - data={ - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "neato", + { + "neato": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, }, + ) + + result = await hass.config_entries.flow.async_init( + "neato", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + f"&client_secret={CLIENT_SECRET}" + "&scope=public_profile+control_robots+maps" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.neato.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 + + +async def test_abort_if_already_setup(hass: HomeAssistantType): + """Test we abort if Neato is already setup.""" + entry = MockConfigEntry( + domain=NEATO_DOMAIN, + data={"auth_implementation": "neato", "token": {"some": "data"}}, + ) + entry.add_to_hass(hass) + + # Should fail + result = await hass.config_entries.flow.async_init( + "neato", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_reauth( + hass: HomeAssistantType, aiohttp_client, aioclient_mock, current_request_with_host +): + """Test initialization of the reauth flow.""" + assert await setup.async_setup_component( + hass, + "neato", + { + "neato": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, + }, + ) + + MockConfigEntry( + entry_id="my_entry", + domain=NEATO_DOMAIN, + data={"username": "abcdef", "password": "123456", "vendor": "neato"}, ).add_to_hass(hass) - # Should fail, same USERNAME (import) - result = await flow.async_step_import( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + # Should show form + result = await hass.config_entries.flow.async_init( + "neato", context={"source": config_entries.SOURCE_REAUTH} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" - # Should fail, same USERNAME (flow) - result = await flow.async_step_user( - {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + # Confirm reauth flow + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 -async def test_abort_on_invalid_credentials(hass): - """Test when we have invalid credentials.""" - flow = init_config_flow(hass) + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + # Update entry with patch( - "homeassistant.components.neato.config_flow.Account", - side_effect=NeatoLoginException(), - ): - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} + "homeassistant.components.neato.async_setup_entry", return_value=True + ) as mock_setup: + result3 = await hass.config_entries.flow.async_configure(result2["flow_id"]) + await hass.async_block_till_done() - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "invalid_auth" + new_entry = hass.config_entries.async_get_entry("my_entry") - -async def test_abort_on_unexpected_error(hass): - """Test when we have an unexpected error.""" - flow = init_config_flow(hass) - - with patch( - "homeassistant.components.neato.config_flow.Account", - side_effect=NeatoRobotException(), - ): - result = await flow.async_step_user( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} - - result = await flow.async_step_import( - { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, - } - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "unknown" + assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result3["reason"] == "reauth_successful" + assert new_entry.state == "loaded" + assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py deleted file mode 100644 index 182ef98e52..0000000000 --- a/tests/components/neato/test_init.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Tests for the Neato init file.""" -from pybotvac.exceptions import NeatoLoginException -import pytest - -from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.setup import async_setup_component - -from tests.async_mock import patch -from tests.common import MockConfigEntry - -USERNAME = "myUsername" -PASSWORD = "myPassword" -VENDOR_NEATO = "neato" -VENDOR_VORWERK = "vorwerk" -VENDOR_INVALID = "invalid" - -VALID_CONFIG = { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_NEATO, -} - -DIFFERENT_CONFIG = { - CONF_USERNAME: "anotherUsername", - CONF_PASSWORD: "anotherPassword", - CONF_VENDOR: VENDOR_VORWERK, -} - -INVALID_CONFIG = { - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_VENDOR: VENDOR_INVALID, -} - - -@pytest.fixture(name="config_flow") -def mock_config_flow_login(): - """Mock a successful login.""" - with patch("homeassistant.components.neato.config_flow.Account", return_value=True): - yield - - -@pytest.fixture(name="hub") -def mock_controller_login(): - """Mock a successful login.""" - with patch("homeassistant.components.neato.Account", return_value=True): - yield - - -async def test_no_config_entry(hass): - """There is nothing in configuration.yaml.""" - res = await async_setup_component(hass, NEATO_DOMAIN, {}) - assert res is True - - -async def test_create_valid_config_entry(hass, config_flow, hub): - """There is something in configuration.yaml.""" - assert hass.config_entries.async_entries(NEATO_DOMAIN) == [] - assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(NEATO_DOMAIN) - assert entries - assert entries[0].data[CONF_USERNAME] == USERNAME - assert entries[0].data[CONF_PASSWORD] == PASSWORD - assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO - - -async def test_config_entries_in_sync(hass, hub): - """The config entry and configuration.yaml are in sync.""" - MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) - - assert hass.config_entries.async_entries(NEATO_DOMAIN) - assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(NEATO_DOMAIN) - assert entries - assert entries[0].data[CONF_USERNAME] == USERNAME - assert entries[0].data[CONF_PASSWORD] == PASSWORD - assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO - - -async def test_config_entries_not_in_sync(hass, config_flow, hub): - """The config entry and configuration.yaml are not in sync.""" - MockConfigEntry(domain=NEATO_DOMAIN, data=DIFFERENT_CONFIG).add_to_hass(hass) - - assert hass.config_entries.async_entries(NEATO_DOMAIN) - assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(NEATO_DOMAIN) - assert entries - assert entries[0].data[CONF_USERNAME] == USERNAME - assert entries[0].data[CONF_PASSWORD] == PASSWORD - assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO - - -async def test_config_entries_not_in_sync_error(hass): - """The config entry and configuration.yaml are not in sync, the new configuration is wrong.""" - MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) - - assert hass.config_entries.async_entries(NEATO_DOMAIN) - with patch( - "homeassistant.components.neato.config_flow.Account", - side_effect=NeatoLoginException(), - ): - assert not await async_setup_component( - hass, NEATO_DOMAIN, {NEATO_DOMAIN: DIFFERENT_CONFIG} - ) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(NEATO_DOMAIN) - assert entries - assert entries[0].data[CONF_USERNAME] == USERNAME - assert entries[0].data[CONF_PASSWORD] == PASSWORD - assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 4a018305bc..69b413ba51 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -9,9 +9,11 @@ import datetime import aiohttp from google_nest_sdm.device import Device +import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_IDLE +from homeassistant.exceptions import HomeAssistantError from homeassistant.util.dt import utcnow from .common import async_setup_sdm_platform @@ -140,6 +142,36 @@ async def test_camera_stream(hass, auth): assert image.content == b"image bytes" +async def test_camera_stream_missing_trait(hass, auth): + """Test fetching a video stream when not supported by the API.""" + traits = { + "sdm.devices.traits.Info": { + "customName": "My Camera", + }, + "sdm.devices.traits.CameraImage": { + "maxImageResolution": { + "width": 800, + "height": 600, + } + }, + } + + await async_setup_camera(hass, traits, auth=auth) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source is None + + # Currently on support getting the image from a live stream + with pytest.raises(HomeAssistantError): + image = await camera.async_get_image(hass, "camera.my_camera") + assert image is None + + async def test_refresh_expired_stream_token(hass, auth): """Test a camera stream expiration and refresh.""" now = utcnow() @@ -220,6 +252,59 @@ async def test_refresh_expired_stream_token(hass, auth): assert stream_source == "rtsp://some/url?auth=g.3.streamingToken" +async def test_stream_response_already_expired(hass, auth): + """Test a API response returning an expired stream url.""" + now = utcnow() + stream_1_expiration = now + datetime.timedelta(seconds=-90) + stream_2_expiration = now + datetime.timedelta(seconds=+90) + auth.responses = [ + aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" + }, + "streamExtensionToken": "g.1.extensionToken", + "streamToken": "g.1.streamingToken", + "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), + }, + } + ), + aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": "rtsp://some/url?auth=g.2.streamingToken" + }, + "streamExtensionToken": "g.2.extensionToken", + "streamToken": "g.2.streamingToken", + "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), + }, + } + ), + ] + await async_setup_camera( + hass, + DEVICE_TRAITS, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + cam = hass.states.get("camera.my_camera") + assert cam is not None + assert cam.state == STATE_IDLE + + # The stream is expired, but we return it anyway + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.1.streamingToken" + + await fire_alarm(hass, now) + + # Second attempt sees that the stream is expired and refreshes + stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") + assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" + + async def test_camera_removed(hass, auth): """Test case where entities are removed and stream tokens expired.""" now = utcnow() diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index bf6716ec96..886b67f8e2 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -7,6 +7,7 @@ pubsub subscriber. from google_nest_sdm.device import Device from google_nest_sdm.event import EventMessage +import pytest from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, @@ -21,14 +22,17 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_OFF, + FAN_LOW, FAN_OFF, FAN_ON, HVAC_MODE_COOL, + HVAC_MODE_DRY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_ECO, PRESET_NONE, + PRESET_SLEEP, ) from homeassistant.const import ATTR_TEMPERATURE @@ -450,6 +454,34 @@ async def test_thermostat_set_hvac_mode(hass, auth): assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT +async def test_thermostat_invalid_hvac_mode(hass, auth): + """Test setting an hvac_mode that is not supported.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + }, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + + with pytest.raises(ValueError): + await common.async_set_hvac_mode(hass, HVAC_MODE_DRY) + await hass.async_block_till_done() + + assert thermostat.state == HVAC_MODE_OFF + assert auth.method is None # No communication with API + + async def test_thermostat_set_eco_preset(hass, auth): """Test a thermostat put into eco mode.""" subscriber = await setup_climate( @@ -782,6 +814,53 @@ async def test_thermostat_fan_empty(hass): assert ATTR_FAN_MODE not in thermostat.attributes assert ATTR_FAN_MODES not in thermostat.attributes + # Ignores set_fan_mode since it is lacking SUPPORT_FAN_MODE + await common.async_set_fan_mode(hass, FAN_ON) + await hass.async_block_till_done() + + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_invalid_fan_mode(hass): + """Test setting a fan mode that is not supported.""" + await setup_climate( + hass, + { + "sdm.devices.traits.Fan": { + "timerMode": "ON", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + "sdm.devices.traits.Temperature": { + "ambientTemperatureCelsius": 16.2, + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + with pytest.raises(ValueError): + await common.async_set_fan_mode(hass, FAN_LOW) + await hass.async_block_till_done() + async def test_thermostat_target_temp(hass, auth): """Test a thermostat changing hvac modes and affected on target temps.""" @@ -843,3 +922,208 @@ async def test_thermostat_target_temp(hass, auth): assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] == 22.0 assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] == 28.0 assert thermostat.attributes[ATTR_TEMPERATURE] is None + + +async def test_thermostat_missing_mode_traits(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == set() + assert ATTR_TEMPERATURE not in thermostat.attributes + assert ATTR_TARGET_TEMP_LOW not in thermostat.attributes + assert ATTR_TARGET_TEMP_HIGH not in thermostat.attributes + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + await common.async_set_temperature(hass, temperature=24.0) + await hass.async_block_till_done() + assert ATTR_TEMPERATURE not in thermostat.attributes + + await common.async_set_preset_mode(hass, PRESET_ECO) + await hass.async_block_till_done() + assert ATTR_PRESET_MODE not in thermostat.attributes + + +async def test_thermostat_missing_temperature_trait(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEAT", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + await common.async_set_temperature(hass, temperature=24.0) + await hass.async_block_till_done() + assert thermostat.attributes[ATTR_TEMPERATURE] is None + + +async def test_thermostat_unexpected_hvac_status(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "UNEXPECTED"}, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert ATTR_HVAC_ACTION not in thermostat.attributes + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == set() + assert ATTR_TEMPERATURE not in thermostat.attributes + assert ATTR_TARGET_TEMP_LOW not in thermostat.attributes + assert ATTR_TARGET_TEMP_HIGH not in thermostat.attributes + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + with pytest.raises(ValueError): + await common.async_set_hvac_mode(hass, HVAC_MODE_DRY) + await hass.async_block_till_done() + assert thermostat.state == HVAC_MODE_OFF + + +async def test_thermostat_missing_set_point(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "HEATCOOL", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_HEAT_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_unexepected_hvac_mode(hass): + """Test a thermostat missing many thermostat traits in api response.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF", "UNEXPECTED"], + "mode": "UNEXPECTED", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] is None + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_TEMPERATURE] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_LOW] is None + assert thermostat.attributes[ATTR_TARGET_TEMP_HIGH] is None + assert ATTR_PRESET_MODE not in thermostat.attributes + assert ATTR_PRESET_MODES not in thermostat.attributes + assert ATTR_FAN_MODE not in thermostat.attributes + assert ATTR_FAN_MODES not in thermostat.attributes + + +async def test_thermostat_invalid_set_preset_mode(hass, auth): + """Test a thermostat set with an invalid preset mode.""" + await setup_climate( + hass, + { + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatEco": { + "availableModes": ["MANUAL_ECO", "OFF"], + "mode": "OFF", + "heatCelsius": 15.0, + "coolCelsius": 28.0, + }, + }, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] + + # Set preset mode that is invalid + with pytest.raises(ValueError): + await common.async_set_preset_mode(hass, PRESET_SLEEP) + await hass.async_block_till_done() + + # No RPC sent + assert auth.method is None + + # Preset is unchanged + assert thermostat.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert thermostat.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_NONE] diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index cd3a06a5af..65a3756391 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -1,9 +1,10 @@ """Common libraries for test setup.""" import time +from typing import Awaitable, Callable from google_nest_sdm.device_manager import DeviceManager -from google_nest_sdm.event import AsyncEventCallback, EventMessage +from google_nest_sdm.event import EventMessage from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN @@ -18,7 +19,7 @@ CONFIG = { "client_secret": "some-client-secret", # Required fields for using SDM API "project_id": "some-project-id", - "subscriber_id": "some-subscriber-id", + "subscriber_id": "projects/example/subscriptions/subscriber-id-9876", }, } @@ -59,13 +60,12 @@ class FakeSubscriber(GoogleNestSubscriber): def __init__(self, device_manager: FakeDeviceManager): """Initialize Fake Subscriber.""" self._device_manager = device_manager - self._callback = None - def set_update_callback(self, callback: AsyncEventCallback): + def set_update_callback(self, callback: Callable[[EventMessage], Awaitable[None]]): """Capture the callback set by Home Assistant.""" self._callback = callback - async def start_async(self) -> DeviceManager: + async def start_async(self): """Return the fake device manager.""" return self._device_manager @@ -81,7 +81,7 @@ class FakeSubscriber(GoogleNestSubscriber): """Simulate a received pubsub message, invoked by tests.""" # Update device state, then invoke HomeAssistant to refresh await self._device_manager.async_handle_event(event_message) - await self._callback.async_handle_event(event_message) + await self._callback(event_message) async def async_setup_sdm_platform(hass, platform, devices={}, structures={}): diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index 6573b17980..aad5621935 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -1,74 +1,225 @@ """Test the Google Nest Device Access config flow.""" + +import pytest + from homeassistant import config_entries, setup from homeassistant.components.nest.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow +from .common import MockConfigEntry + from tests.async_mock import patch CLIENT_ID = "1234" CLIENT_SECRET = "5678" PROJECT_ID = "project-id-4321" -SUBSCRIBER_ID = "subscriber-id-9876" +SUBSCRIBER_ID = "projects/example/subscriptions/subscriber-id-9876" + +CONFIG = { + DOMAIN: { + "project_id": PROJECT_ID, + "subscriber_id": SUBSCRIBER_ID, + CONF_CLIENT_ID: CLIENT_ID, + CONF_CLIENT_SECRET: CLIENT_SECRET, + }, + "http": {"base_url": "https://example.com"}, +} -async def test_full_flow( - hass, aiohttp_client, aioclient_mock, current_request_with_host -): - """Check full flow.""" - assert await setup.async_setup_component( - hass, - DOMAIN, - { - DOMAIN: { - "project_id": PROJECT_ID, - "subscriber_id": SUBSCRIBER_ID, - CONF_CLIENT_ID: CLIENT_ID, - CONF_CLIENT_SECRET: CLIENT_SECRET, +def get_config_entry(hass): + """Return a single config entry.""" + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + return entries[0] + + +class OAuthFixture: + """Simulate the oauth flow used by the config flow.""" + + def __init__(self, hass, aiohttp_client, aioclient_mock): + """Initialize OAuthFixture.""" + self.hass = hass + self.aiohttp_client = aiohttp_client + self.aioclient_mock = aioclient_mock + + async def async_oauth_flow(self, result): + """Invoke the oauth flow with fake responses.""" + state = config_entry_oauth2_flow._encode_jwt( + self.hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", }, - "http": {"base_url": "https://example.com"}, - }, - ) + ) + + oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID) + assert result["type"] == "external" + assert result["url"] == ( + f"{oauth_authorize}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=https://www.googleapis.com/auth/sdm.service" + "+https://www.googleapis.com/auth/pubsub" + "&access_type=offline&prompt=consent" + ) + + client = await self.aiohttp_client(self.hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + self.aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.nest.async_setup_entry", return_value=True + ) as mock_setup: + await self.hass.config_entries.flow.async_configure(result["flow_id"]) + assert len(mock_setup.mock_calls) == 1 + + +@pytest.fixture +async def oauth(hass, aiohttp_client, aioclient_mock, current_request_with_host): + """Create the simulated oauth flow.""" + return OAuthFixture(hass, aiohttp_client, aioclient_mock) + + +async def test_full_flow(hass, oauth): + """Check full flow.""" + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - state = config_entry_oauth2_flow._encode_jwt( - hass, - { - "flow_id": result["flow_id"], - "redirect_uri": "https://example.com/auth/external/callback", + await oauth.async_oauth_flow(result) + + entry = get_config_entry(hass) + assert entry.title == "Configuration.yaml" + assert "token" in entry.data + entry.data["token"].pop("expires_at") + assert entry.unique_id == DOMAIN + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + +async def test_reauth(hass, oauth): + """Test Nest reauthentication.""" + + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + + old_entry = MockConfigEntry( + domain=DOMAIN, + data={ + "auth_implementation": DOMAIN, + "token": { + # Verify this is replaced at end of the test + "access_token": "some-revoked-token", + }, + "sdm": {}, }, + unique_id=DOMAIN, + ) + old_entry.add_to_hass(hass) + + entry = get_config_entry(hass) + assert entry.data["token"] == { + "access_token": "some-revoked-token", + } + + await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data ) - oauth_authorize = OAUTH2_AUTHORIZE.format(project_id=PROJECT_ID) - assert result["url"] == ( - f"{oauth_authorize}?response_type=code&client_id={CLIENT_ID}" - "&redirect_uri=https://example.com/auth/external/callback" - f"&state={state}&scope=https://www.googleapis.com/auth/sdm.service" - "+https://www.googleapis.com/auth/pubsub" - "&access_type=offline&prompt=consent" + # Advance through the reauth flow + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + # Run the oauth flow + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + await oauth.async_oauth_flow(result) + + # Verify existing tokens are replaced + entry = get_config_entry(hass) + entry.data["token"].pop("expires_at") + assert entry.unique_id == DOMAIN + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } + + +async def test_single_config_entry(hass): + """Test that only a single config entry is allowed.""" + old_entry = MockConfigEntry( + domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} ) + old_entry.add_to_hass(hass) - client = await aiohttp_client(hass.http.app) - resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") - assert resp.status == 200 - assert resp.headers["content-type"] == "text/html; charset=utf-8" + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) - aioclient_mock.post( - OAUTH2_TOKEN, - json={ - "refresh_token": "mock-refresh-token", - "access_token": "mock-access-token", - "type": "Bearer", - "expires_in": 60, - }, + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) + assert result["type"] == "abort" + assert result["reason"] == "single_instance_allowed" - with patch( - "homeassistant.components.nest.async_setup_entry", return_value=True - ) as mock_setup: - await hass.config_entries.flow.async_configure(result["flow_id"]) - assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert len(mock_setup.mock_calls) == 1 +async def test_unexpected_existing_config_entries(hass, oauth): + """Test Nest reauthentication with multiple existing config entries.""" + # Note that this case will not happen in the future since only a single + # instance is now allowed, but this may have been allowed in the past. + # On reauth, only one entry is kept and the others are deleted. + + assert await setup.async_setup_component(hass, DOMAIN, CONFIG) + + old_entry = MockConfigEntry( + domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} + ) + old_entry.add_to_hass(hass) + + old_entry = MockConfigEntry( + domain=DOMAIN, data={"auth_implementation": DOMAIN, "sdm": {}} + ) + old_entry.add_to_hass(hass) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 2 + + # Invoke the reauth flow + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data=old_entry.data + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + flows = hass.config_entries.flow.async_progress() + + result = await hass.config_entries.flow.async_configure(flows[0]["flow_id"], {}) + await oauth.async_oauth_flow(result) + + # Only a single entry now exists, and the other was cleaned up + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + assert entry.unique_id == DOMAIN + entry.data["token"].pop("expires_at") + assert entry.data["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + } diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index 89dccf6c31..b7c7586215 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -7,8 +7,10 @@ import homeassistant.components.automation as automation from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.components.nest import DOMAIN, NEST_EVENT +from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.events import NEST_EVENT from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow from .common import async_setup_sdm_platform @@ -148,21 +150,21 @@ async def test_multiple_devices(hass): triggers = await async_get_device_automations(hass, "trigger", entry1.device_id) assert len(triggers) == 1 - assert { + assert triggers[0] == { "platform": "device", "domain": DOMAIN, "type": "camera_sound", "device_id": entry1.device_id, - } == triggers[0] + } triggers = await async_get_device_automations(hass, "trigger", entry2.device_id) assert len(triggers) == 1 - assert { + assert triggers[0] == { "platform": "device", "domain": DOMAIN, "type": "doorbell_chime", "device_id": entry2.device_id, - } == triggers[0] + } async def test_triggers_for_invalid_device_id(hass): @@ -205,14 +207,14 @@ async def test_no_triggers(hass): assert entry.unique_id == "some-device-id-camera" triggers = await async_get_device_automations(hass, "trigger", entry.device_id) - assert [] == triggers + assert triggers == [] async def test_fires_on_camera_motion(hass, calls): """Test camera_motion triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_motion") - message = {"device_id": DEVICE_ID, "type": "camera_motion"} + message = {"device_id": DEVICE_ID, "type": "camera_motion", "timestamp": utcnow()} hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 1 @@ -223,7 +225,7 @@ async def test_fires_on_camera_person(hass, calls): """Test camera_person triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_person") - message = {"device_id": DEVICE_ID, "type": "camera_person"} + message = {"device_id": DEVICE_ID, "type": "camera_person", "timestamp": utcnow()} hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 1 @@ -234,7 +236,7 @@ async def test_fires_on_camera_sound(hass, calls): """Test camera_person triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_sound") - message = {"device_id": DEVICE_ID, "type": "camera_sound"} + message = {"device_id": DEVICE_ID, "type": "camera_sound", "timestamp": utcnow()} hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 1 @@ -245,7 +247,7 @@ async def test_fires_on_doorbell_chime(hass, calls): """Test doorbell_chime triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "doorbell_chime") - message = {"device_id": DEVICE_ID, "type": "doorbell_chime"} + message = {"device_id": DEVICE_ID, "type": "doorbell_chime", "timestamp": utcnow()} hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 1 @@ -256,7 +258,11 @@ async def test_trigger_for_wrong_device_id(hass, calls): """Test for turn_on and turn_off triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_motion") - message = {"device_id": "wrong-device-id", "type": "camera_motion"} + message = { + "device_id": "wrong-device-id", + "type": "camera_motion", + "timestamp": utcnow(), + } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 0 @@ -266,7 +272,11 @@ async def test_trigger_for_wrong_event_type(hass, calls): """Test for turn_on and turn_off triggers firing.""" assert await setup_automation(hass, DEVICE_ID, "camera_motion") - message = {"device_id": DEVICE_ID, "type": "wrong-event-type"} + message = { + "device_id": DEVICE_ID, + "type": "wrong-event-type", + "timestamp": utcnow(), + } hass.bus.async_fire(NEST_EVENT, message) await hass.async_block_till_done() assert len(calls) == 0 diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 12314f6056..7295d13408 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -54,7 +54,7 @@ def create_device_traits(event_trait): } -def create_event(event_type, device_id=DEVICE_ID): +def create_event(event_type, device_id=DEVICE_ID, timestamp=None): """Create an EventMessage for a single event type.""" events = { event_type: { @@ -65,12 +65,14 @@ def create_event(event_type, device_id=DEVICE_ID): return create_events(events=events, device_id=device_id) -def create_events(events, device_id=DEVICE_ID): +def create_events(events, device_id=DEVICE_ID, timestamp=None): """Create an EventMessage for events.""" + if not timestamp: + timestamp = utcnow() return EventMessage( { "eventId": "some-event-id", - "timestamp": utcnow().isoformat(timespec="seconds"), + "timestamp": timestamp.isoformat(timespec="seconds"), "resourceUpdate": { "name": device_id, "events": events, @@ -102,15 +104,18 @@ async def test_doorbell_chime_event(hass): assert device.model == "Doorbell" assert device.identifiers == {("nest", DEVICE_ID)} + timestamp = utcnow() await subscriber.async_receive_event( - create_event("sdm.devices.events.DoorbellChime.Chime") + create_event("sdm.devices.events.DoorbellChime.Chime", timestamp=timestamp) ) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, "type": "doorbell_chime", + "timestamp": event_time, } @@ -126,15 +131,18 @@ async def test_camera_motion_event(hass): entry = registry.async_get("camera.front") assert entry is not None + timestamp = utcnow() await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraMotion.Motion") + create_event("sdm.devices.events.CameraMotion.Motion", timestamp=timestamp) ) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, "type": "camera_motion", + "timestamp": event_time, } @@ -150,15 +158,18 @@ async def test_camera_sound_event(hass): entry = registry.async_get("camera.front") assert entry is not None + timestamp = utcnow() await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraSound.Sound") + create_event("sdm.devices.events.CameraSound.Sound", timestamp=timestamp) ) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, "type": "camera_sound", + "timestamp": event_time, } @@ -174,15 +185,18 @@ async def test_camera_person_event(hass): entry = registry.async_get("camera.front") assert entry is not None + timestamp = utcnow() await subscriber.async_receive_event( - create_event("sdm.devices.events.CameraPerson.Person") + create_event("sdm.devices.events.CameraPerson.Person", timestamp=timestamp) ) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 1 assert events[0].data == { "device_id": entry.device_id, "type": "camera_person", + "timestamp": event_time, } @@ -209,17 +223,21 @@ async def test_camera_multiple_event(hass): }, } - await subscriber.async_receive_event(create_events(event_map)) + timestamp = utcnow() + await subscriber.async_receive_event(create_events(event_map, timestamp=timestamp)) await hass.async_block_till_done() + event_time = timestamp.replace(microsecond=0) assert len(events) == 2 assert events[0].data == { "device_id": entry.device_id, "type": "camera_motion", + "timestamp": event_time, } assert events[1].data == { "device_id": entry.device_id, "type": "camera_person", + "timestamp": event_time, } diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py new file mode 100644 index 0000000000..f85fcdaa74 --- /dev/null +++ b/tests/components/nest/test_init_legacy.py @@ -0,0 +1,87 @@ +"""Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" + +import time + +from homeassistant.setup import async_setup_component + +from tests.async_mock import MagicMock, PropertyMock, patch +from tests.common import MockConfigEntry + +DOMAIN = "nest" + +CONFIG = { + "nest": { + "client_id": "some-client-id", + "client_secret": "some-client-secret", + }, +} + +CONFIG_ENTRY_DATA = { + "auth_implementation": "local", + "tokens": { + "expires_at": time.time() + 86400, + "access_token": { + "token": "some-token", + }, + }, +} + + +def make_thermostat(): + """Make a mock thermostat with dummy values.""" + device = MagicMock() + type(device).device_id = PropertyMock(return_value="a.b.c.d.e.f.g") + type(device).name = PropertyMock(return_value="My Thermostat") + type(device).name_long = PropertyMock(return_value="My Thermostat") + type(device).serial = PropertyMock(return_value="serial-number") + type(device).mode = "off" + type(device).hvac_state = "off" + type(device).target = PropertyMock(return_value=31.0) + type(device).temperature = PropertyMock(return_value=30.1) + type(device).min_temperature = PropertyMock(return_value=10.0) + type(device).max_temperature = PropertyMock(return_value=50.0) + type(device).humidity = PropertyMock(return_value=40.4) + type(device).software_version = PropertyMock(return_value="a.b.c") + return device + + +async def test_thermostat(hass): + """Test simple initialization for thermostat entities.""" + + thermostat = make_thermostat() + + structure = MagicMock() + type(structure).name = PropertyMock(return_value="My Room") + type(structure).thermostats = PropertyMock(return_value=[thermostat]) + type(structure).eta = PropertyMock(return_value="away") + + nest = MagicMock() + type(nest).structures = PropertyMock(return_value=[structure]) + + config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) + config_entry.add_to_hass(hass) + with patch("homeassistant.components.nest.legacy.Nest", return_value=nest), patch( + "homeassistant.components.nest.legacy.sensor._VALID_SENSOR_TYPES", + ["humidity", "temperature"], + ), patch( + "homeassistant.components.nest.legacy.binary_sensor._VALID_BINARY_SENSOR_TYPES", + {"fan": None}, + ): + assert await async_setup_component(hass, DOMAIN, CONFIG) + await hass.async_block_till_done() + + climate = hass.states.get("climate.my_thermostat") + assert climate is not None + assert climate.state == "off" + + temperature = hass.states.get("sensor.my_thermostat_temperature") + assert temperature is not None + assert temperature.state == "-1.1" + + humidity = hass.states.get("sensor.my_thermostat_humidity") + assert humidity is not None + assert humidity.state == "40.4" + + fan = hass.states.get("binary_sensor.my_thermostat_fan") + assert fan is not None + assert fan.state == "on" diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py new file mode 100644 index 0000000000..cb17f81d18 --- /dev/null +++ b/tests/components/nest/test_init_sdm.py @@ -0,0 +1,90 @@ +""" +Test for setup methods for the SDM API. + +The tests fake out the subscriber/devicemanager and simulate setup behavior +and failure modes. +""" + +import logging + +from google_nest_sdm.exceptions import GoogleNestException + +from homeassistant.components.nest import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_SETUP_ERROR, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.setup import async_setup_component + +from .common import CONFIG, CONFIG_ENTRY_DATA, async_setup_sdm_platform + +from tests.async_mock import patch +from tests.common import MockConfigEntry + +PLATFORM = "sensor" + + +async def test_setup_success(hass, caplog): + """Test successful setup.""" + with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + await async_setup_sdm_platform(hass, PLATFORM) + assert not caplog.records + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_LOADED + + +async def async_setup_sdm(hass, config=CONFIG): + """Prepare test setup.""" + MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA).add_to_hass(hass) + with patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" + ): + await async_setup_component(hass, DOMAIN, config) + + +async def test_setup_configuration_failure(hass, caplog): + """Test configuration error.""" + config = CONFIG.copy() + config[DOMAIN]["subscriber_id"] = "invalid-subscriber-format" + + await async_setup_sdm(hass, config) + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_ERROR + + # This error comes from the python google-nest-sdm library, as a check added + # to prevent common misconfigurations (e.g. confusing topic and subscriber) + assert "Subscription misconfigured. Expected subscriber_id" in caplog.text + + +async def test_setup_susbcriber_failure(hass, caplog): + """Test configuration error.""" + with patch( + "homeassistant.components.nest.GoogleNestSubscriber.start_async", + side_effect=GoogleNestException(), + ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + await async_setup_sdm(hass) + assert "Subscriber error:" in caplog.text + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_RETRY + + +async def test_setup_device_manager_failure(hass, caplog): + """Test configuration error.""" + with patch("homeassistant.components.nest.GoogleNestSubscriber.start_async"), patch( + "homeassistant.components.nest.GoogleNestSubscriber.async_get_device_manager", + side_effect=GoogleNestException(), + ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + await async_setup_sdm(hass) + assert len(caplog.messages) == 1 + assert "Device manager error:" in caplog.text + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/nest/test_local_auth.py b/tests/components/nest/test_local_auth.py index 491b9bd9e0..ecc37bbe24 100644 --- a/tests/components/nest/test_local_auth.py +++ b/tests/components/nest/test_local_auth.py @@ -4,7 +4,8 @@ from urllib.parse import parse_qsl import pytest import requests_mock as rmock -from homeassistant.components.nest import config_flow, const, local_auth +from homeassistant.components.nest import config_flow, const +from homeassistant.components.nest.legacy import local_auth @pytest.fixture diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 6037bde5af..58e090db20 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -1,8 +1,17 @@ """The tests for the Number component.""" -from unittest.mock import MagicMock - from homeassistant.components.number import NumberEntity +from tests.async_mock import MagicMock + + +class MockDefaultNumberEntity(NumberEntity): + """Mock NumberEntity device to use in tests.""" + + @property + def value(self): + """Return the current value.""" + return 0.5 + class MockNumberEntity(NumberEntity): """Mock NumberEntity device to use in tests.""" @@ -13,14 +22,14 @@ class MockNumberEntity(NumberEntity): return 1.0 @property - def state(self): + def value(self): """Return the current value.""" - return "0.5" + return 0.5 async def test_step(hass): """Test the step calculation.""" - number = NumberEntity() + number = MockDefaultNumberEntity() assert number.step == 1.0 number_2 = MockNumberEntity() @@ -29,7 +38,7 @@ async def test_step(hass): async def test_sync_set_value(hass): """Test if async set_value calls sync set_value.""" - number = NumberEntity() + number = MockDefaultNumberEntity() number.hass = hass number.set_value = MagicMock() diff --git a/tests/components/number/test_reproduce_state.py b/tests/components/number/test_reproduce_state.py new file mode 100644 index 0000000000..654f87cbce --- /dev/null +++ b/tests/components/number/test_reproduce_state.py @@ -0,0 +1,53 @@ +"""Test reproduce state for Number entities.""" +from homeassistant.components.number.const import ( + ATTR_MAX, + ATTR_MIN, + DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.core import State + +from tests.common import async_mock_service + +VALID_NUMBER1 = "19.0" +VALID_NUMBER2 = "99.9" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Number states.""" + + hass.states.async_set( + "number.test_number", VALID_NUMBER1, {ATTR_MIN: 5, ATTR_MAX: 100} + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("number.test_number", VALID_NUMBER1), + # Should not raise + State("number.non_existing", "234"), + ], + ) + + assert hass.states.get("number.test_number").state == VALID_NUMBER1 + + # Test reproducing with different state + calls = async_mock_service(hass, DOMAIN, SERVICE_SET_VALUE) + await hass.helpers.state.async_reproduce_state( + [ + State("number.test_number", VALID_NUMBER2), + # Should not raise + State("number.non_existing", "234"), + ], + ) + + assert len(calls) == 1 + assert calls[0].domain == DOMAIN + assert calls[0].data == {"entity_id": "number.test_number", "value": VALID_NUMBER2} + + # Test invalid state + await hass.helpers.state.async_reproduce_state( + [State("number.test_number", "invalid_state")] + ) + + assert len(calls) == 1 diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index bc8cf1defc..be740b13fb 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -84,3 +84,6 @@ async def test_owserver_binary_sensor(owproxy, hass, device_id): assert registry_entry is not None state = hass.states.get(entity_id) assert state.state == expected_sensor["result"] + assert state.attributes["device_file"] == expected_sensor.get( + "device_file", registry_entry.unique_id + ) diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/test_entity_owserver.py index a09808316c..aee84f9fe2 100644 --- a/tests/components/onewire/test_entity_owserver.py +++ b/tests/components/onewire/test_entity_owserver.py @@ -179,6 +179,18 @@ MOCK_DEVICE_SENSORS = { }, ], }, + "1F.111111111111": { + "inject_reads": [ + b"DS2409", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "1F.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2409", + "name": "1F.111111111111", + }, + SENSOR_DOMAIN: [], + }, "22.111111111111": { "inject_reads": [ b"DS1822", # read device type @@ -752,3 +764,6 @@ async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): assert state is None else: assert state.state == expected_sensor["result"] + assert state.attributes["device_file"] == expected_sensor.get( + "device_file", registry_entry.unique_id + ) diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index 751ef10614..ad9580f34e 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,16 +1,66 @@ """Tests for 1-Wire sensor platform.""" -from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR -import homeassistant.components.sensor as sensor +from pyownet.protocol import Error as ProtocolError +import pytest + +from homeassistant.components.onewire.const import DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.setup import async_setup_component -from tests.common import assert_setup_component +from . import setup_onewire_patched_owserver_integration + +from tests.async_mock import patch +from tests.common import assert_setup_component, mock_registry + +MOCK_COUPLERS = { + "1F.111111111111": { + "inject_reads": [ + b"DS2409", # read device type + ], + "branches": { + "aux": {}, + "main": { + "1D.111111111111": { + "inject_reads": [ + b"DS2423", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "1D.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2423", + "name": "1D.111111111111", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.1d_111111111111_counter_a", + "device_file": "/1F.111111111111/main/1D.111111111111/counter.A", + "unique_id": "/1D.111111111111/counter.A", + "injected_value": b" 251123", + "result": "251123", + "unit": "count", + "class": None, + }, + { + "entity_id": "sensor.1d_111111111111_counter_b", + "device_file": "/1F.111111111111/main/1D.111111111111/counter.B", + "unique_id": "/1D.111111111111/counter.B", + "injected_value": b" 248125", + "result": "248125", + "unit": "count", + "class": None, + }, + ], + }, + }, + }, + } +} async def test_setup_minimum(hass): """Test old platform setup with minimum configuration.""" config = {"sensor": {"platform": "onewire"}} with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, sensor.DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() @@ -23,7 +73,7 @@ async def test_setup_sysbus(hass): } } with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, sensor.DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() @@ -31,7 +81,7 @@ async def test_setup_owserver(hass): """Test old platform setup with OWServer configuration.""" config = {"sensor": {"platform": "onewire", "host": "localhost"}} with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, sensor.DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() @@ -39,5 +89,67 @@ async def test_setup_owserver_with_port(hass): """Test old platform setup with OWServer configuration.""" config = {"sensor": {"platform": "onewire", "host": "localhost", "port": "1234"}} with assert_setup_component(1, "sensor"): - assert await async_setup_component(hass, sensor.DOMAIN, config) + assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() + + +@pytest.mark.parametrize("device_id", ["1F.111111111111"]) +@patch("homeassistant.components.onewire.onewirehub.protocol.proxy") +async def test_sensors_on_owserver_coupler(owproxy, hass, device_id): + """Test for 1-Wire sensors connected to DS2409 coupler.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + + mock_coupler = MOCK_COUPLERS[device_id] + + dir_side_effect = [] # List of lists of string + read_side_effect = [] # List of byte arrays + + dir_side_effect.append([f"/{device_id}/"]) # dir on root + read_side_effect.append(device_id[0:2].encode()) # read family on root + if "inject_reads" in mock_coupler: + read_side_effect += mock_coupler["inject_reads"] + + expected_sensors = [] + for branch, branch_details in mock_coupler["branches"].items(): + dir_side_effect.append( + [ # dir on branch + f"/{device_id}/{branch}/{sub_device_id}/" + for sub_device_id in branch_details + ] + ) + + for sub_device_id, sub_device in branch_details.items(): + read_side_effect.append(sub_device_id[0:2].encode()) + if "inject_reads" in sub_device: + read_side_effect.extend(sub_device["inject_reads"]) + + expected_sensors += sub_device[SENSOR_DOMAIN] + for expected_sensor in sub_device[SENSOR_DOMAIN]: + read_side_effect.append(expected_sensor["injected_value"]) + + # Ensure enough read side effect + read_side_effect.extend([ProtocolError("Missing injected value")] * 10) + owproxy.return_value.dir.side_effect = dir_side_effect + owproxy.return_value.read.side_effect = read_side_effect + + with patch("homeassistant.components.onewire.SUPPORTED_PLATFORMS", [SENSOR_DOMAIN]): + await setup_onewire_patched_owserver_integration(hass) + await hass.async_block_till_done() + + assert len(entity_registry.entities) == len(expected_sensors) + + for expected_sensor in expected_sensors: + entity_id = expected_sensor["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_sensor["unique_id"] + assert registry_entry.unit_of_measurement == expected_sensor["unit"] + assert registry_entry.device_class == expected_sensor["class"] + assert registry_entry.disabled == expected_sensor.get("disabled", False) + state = hass.states.get(entity_id) + if registry_entry.disabled: + assert state is None + else: + assert state.state == expected_sensor["result"] + assert state.attributes["device_file"] == expected_sensor["device_file"] diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 3a1f2eb9f7..0c70ad3c9f 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -127,3 +127,6 @@ async def test_owserver_switch(owproxy, hass, device_id): state = hass.states.get(entity_id) assert state.state == expected_sensor["result"] + assert state.attributes["device_file"] == expected_sensor.get( + "device_file", registry_entry.unique_id + ) diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index a7be6ddcf6..e0f4c6eda9 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Opentherm Gateway config flow.""" import asyncio -from pyotgw.vars import OTGW_ABOUT +from pyotgw.vars import OTGW, OTGW_ABOUT from serial import SerialException from homeassistant import config_entries, data_entry_flow, setup @@ -15,6 +15,8 @@ from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVE from tests.async_mock import patch from tests.common import MockConfigEntry +MINIMAL_STATUS = {OTGW: {OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}} + async def test_form_user(hass): """Test we get the form.""" @@ -32,8 +34,7 @@ async def test_form_user(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: @@ -65,8 +66,7 @@ async def test_form_import(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: @@ -108,8 +108,7 @@ async def test_form_duplicate_entries(hass): "homeassistant.components.opentherm_gw.async_setup_entry", return_value=True, ) as mock_setup_entry, patch( - "pyotgw.pyotgw.connect", - return_value={OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}, + "pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS ) as mock_pyotgw_connect, patch( "pyotgw.pyotgw.disconnect", return_value=None ) as mock_pyotgw_disconnect: diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 0febf142d0..8ef1133519 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -4,6 +4,7 @@ import pytest from homeassistant import data_entry_flow from homeassistant.components import ps4 +from homeassistant.components.ps4.config_flow import LOCAL_UDP_PORT from homeassistant.components.ps4.const import ( DEFAULT_ALIAS, DEFAULT_NAME, @@ -360,7 +361,7 @@ async def test_0_pin(hass): result["flow_id"], mock_config ) mock_call.assert_called_once_with( - MOCK_HOST, MOCK_CREDS, MOCK_CODE_LEAD_0_STR, DEFAULT_ALIAS + MOCK_HOST, MOCK_CREDS, MOCK_CODE_LEAD_0_STR, DEFAULT_ALIAS, LOCAL_UDP_PORT ) diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 4e22591d3b..8a03f13bed 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,5 +1,7 @@ """Tests for the PS4 media player platform.""" from pyps4_2ndscreen.credential import get_ddp_message +from pyps4_2ndscreen.ddp import DEFAULT_UDP_PORT +from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -8,6 +10,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_TITLE, + MEDIA_TYPE_APP, MEDIA_TYPE_GAME, ) from homeassistant.components.ps4.const import ( @@ -149,7 +152,7 @@ async def setup_mock_component(hass, entry=None): async def mock_ddp_response(hass, mock_status_data): """Mock raw UDP response from device.""" mock_protocol = hass.data[PS4_DATA].protocol - + assert mock_protocol.local_port == DEFAULT_UDP_PORT mock_code = mock_status_data.get("status_code") mock_status = mock_status_data.get("status") mock_status_header = f"{mock_code} {mock_status}" @@ -224,7 +227,7 @@ async def test_media_attributes_are_fetched(hass): mock_result = MagicMock() mock_result.name = MOCK_TITLE_NAME mock_result.cover_art = MOCK_TITLE_ART_URL - mock_result.game_type = "game" + mock_result.game_type = "not_an_app" with patch(mock_func, return_value=mock_result) as mock_fetch: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) @@ -241,6 +244,21 @@ async def test_media_attributes_are_fetched(hass): assert mock_attrs.get(ATTR_MEDIA_TITLE) == MOCK_TITLE_NAME assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MOCK_TITLE_TYPE + # Change state so that the next fetch is called. + await mock_ddp_response(hass, MOCK_STATUS_STANDBY) + + # Test that content type of app is set. + mock_result.game_type = PS_TYPE_APP + + with patch(mock_func, return_value=mock_result) as mock_fetch_app: + await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + + mock_state = hass.states.get(mock_entity_id) + mock_attrs = dict(mock_state.attributes) + + assert len(mock_fetch_app.mock_calls) == 1 + assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_APP + async def test_media_attributes_are_loaded(hass, patch_load_json): """Test that media attributes are loaded.""" diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index bec87b72ee..ca3fbe8be5 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -1,4 +1,4 @@ -"""Define tests for the Recollect Waste config flow.""" +"""Define tests for the ReCollect Waste config flow.""" from aiorecollect.errors import RecollectError from homeassistant import data_entry_flow @@ -8,6 +8,7 @@ from homeassistant.components.recollect_waste import ( DOMAIN, ) from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_FRIENDLY_NAME from tests.async_mock import patch from tests.common import MockConfigEntry @@ -45,6 +46,30 @@ async def test_invalid_place_or_service_id(hass): assert result["errors"] == {"base": "invalid_place_or_service_id"} +async def test_options_flow(hass): + """Test config flow options.""" + conf = {CONF_PLACE_ID: "12345", CONF_SERVICE_ID: "12345"} + + config_entry = MockConfigEntry(domain=DOMAIN, unique_id="12345, 12345", data=conf) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.recollect_waste.async_setup_entry", return_value=True + ): + await hass.config_entries.async_setup(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_FRIENDLY_NAME: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {CONF_FRIENDLY_NAME: True} + + async def test_show_form(hass): """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index fcda7b0bb6..41c1f52b99 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -23,7 +23,7 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done from tests.async_mock import patch -from tests.common import async_fire_time_changed, get_test_home_assistant +from tests.common import fire_time_changed, get_test_home_assistant def test_saving_state(hass, hass_recorder): @@ -351,8 +351,15 @@ async def test_defaults_set(hass): assert recorder_config["purge_keep_days"] == 10 +def run_tasks_at_time(hass, test_time): + """Advance the clock and wait for any callbacks to finish.""" + fire_time_changed(hass, test_time) + hass.block_till_done() + hass.data[DATA_INSTANCE].block_till_done() + + def test_auto_purge(hass_recorder): - """Test saving and restoring a state.""" + """Test periodic purge alarm scheduling.""" hass = hass_recorder() original_tz = dt_util.DEFAULT_TIME_ZONE @@ -360,18 +367,40 @@ def test_auto_purge(hass_recorder): tz = dt_util.get_time_zone("Europe/Copenhagen") dt_util.set_default_time_zone(tz) + # Purging is schedule to happen at 4:12am every day. Exercise this behavior + # by firing alarms and advancing the clock around this time. Pick an arbitrary + # year in the future to avoid boundary conditions relative to the current date. + # + # The clock is started at 4:15am then advanced forward below now = dt_util.utcnow() - test_time = tz.localize(datetime(now.year + 1, 1, 1, 4, 12, 0)) - async_fire_time_changed(hass, test_time) + test_time = tz.localize(datetime(now.year + 2, 1, 1, 4, 15, 0)) + run_tasks_at_time(hass, test_time) with patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True ) as purge_old_data: - for delta in (-1, 0, 1): - async_fire_time_changed(hass, test_time + timedelta(seconds=delta)) - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() + # Advance one day, and the purge task should run + test_time = test_time + timedelta(days=1) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 1 + purge_old_data.reset_mock() + + # Advance one day, and the purge task should run again + test_time = test_time + timedelta(days=1) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 1 + + purge_old_data.reset_mock() + + # Advance less than one full day. The alarm should not yet fire. + test_time = test_time + timedelta(hours=23) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 0 + + # Advance to the next day and fire the alarm again + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 dt_util.set_default_time_zone(original_tz) diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 48d13a716a..2638477ef7 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch +from tests.async_mock import patch async def test_setup_missing_basic_config(hass): @@ -50,9 +50,7 @@ async def test_setup_missing_config(hass): @respx.mock async def test_setup_failed_connect(hass): """Test setup when connection error occurs.""" - respx.get( - "http://localhost", content=httpx.RequestError(message="any", request=Mock()) - ) + respx.get("http://localhost").mock(side_effect=httpx.RequestError) assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -71,7 +69,7 @@ async def test_setup_failed_connect(hass): @respx.mock async def test_setup_timeout(hass): """Test setup when connection timeout occurs.""" - respx.get("http://localhost", content=asyncio.TimeoutError()) + respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -90,7 +88,7 @@ async def test_setup_timeout(hass): @respx.mock async def test_setup_minimum(hass): """Test setup with minimum configuration.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -109,7 +107,7 @@ async def test_setup_minimum(hass): @respx.mock async def test_setup_minimum_resource_template(hass): """Test setup with minimum configuration (resource_template).""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -127,7 +125,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock async def test_setup_duplicate_resource_template(hass): """Test setup with duplicate resources.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, binary_sensor.DOMAIN, @@ -146,7 +144,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock async def test_setup_get(hass): """Test setup with valid configuration.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -174,7 +172,7 @@ async def test_setup_get(hass): @respx.mock async def test_setup_get_digest_auth(hass): """Test setup with valid configuration.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -202,7 +200,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock async def test_setup_post(hass): """Test setup with valid configuration.""" - respx.post("http://localhost", status_code=200, content="{}") + respx.post("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -230,11 +228,10 @@ async def test_setup_post(hass): @respx.mock async def test_setup_get_off(hass): """Test setup with valid off configuration.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/json"}, - content='{"dog": false}', + json={"dog": False}, ) assert await async_setup_component( hass, @@ -261,11 +258,10 @@ async def test_setup_get_off(hass): @respx.mock async def test_setup_get_on(hass): """Test setup with valid on configuration.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/json"}, - content='{"dog": true}', + json={"dog": True}, ) assert await async_setup_component( hass, @@ -292,7 +288,7 @@ async def test_setup_get_on(hass): @respx.mock async def test_setup_with_exception(hass): """Test setup with exception.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "binary_sensor", @@ -318,9 +314,7 @@ async def test_setup_with_exception(hass): await hass.async_block_till_done() respx.clear() - respx.get( - "http://localhost", content=httpx.RequestError(message="any", request=Mock()) - ) + respx.get("http://localhost").mock(side_effect=httpx.RequestError) await hass.services.async_call( "homeassistant", "update_entity", @@ -337,7 +331,7 @@ async def test_setup_with_exception(hass): async def test_reload(hass): """Verify we can reload reset sensors.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 await async_setup_component( hass, @@ -380,10 +374,7 @@ async def test_reload(hass): @respx.mock async def test_setup_query_params(hass): """Test setup with query params.""" - respx.get( - "http://localhost?search=something", - status_code=200, - ) + respx.get("http://localhost", params={"search": "something"}) % 200 assert await async_setup_component( hass, binary_sensor.DOMAIN, diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 71bcbedda8..16d3f8ba0a 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -6,8 +6,10 @@ import httpx import respx from homeassistant import config as hass_config +from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY import homeassistant.components.sensor as sensor from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, CONTENT_TYPE_JSON, DATA_MEGABYTES, @@ -16,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch +from tests.async_mock import patch async def test_setup_missing_config(hass): @@ -42,9 +44,7 @@ async def test_setup_missing_schema(hass): @respx.mock async def test_setup_failed_connect(hass): """Test setup when connection error occurs.""" - respx.get( - "http://localhost", content=httpx.RequestError(message="any", request=Mock()) - ) + respx.get("http://localhost").mock(side_effect=httpx.RequestError) assert await async_setup_component( hass, sensor.DOMAIN, @@ -63,7 +63,7 @@ async def test_setup_failed_connect(hass): @respx.mock async def test_setup_timeout(hass): """Test setup when connection timeout occurs.""" - respx.get("http://localhost", content=asyncio.TimeoutError()) + respx.get("http://localhost").mock(side_effect=asyncio.TimeoutError()) assert await async_setup_component( hass, sensor.DOMAIN, @@ -76,7 +76,7 @@ async def test_setup_timeout(hass): @respx.mock async def test_setup_minimum(hass): """Test setup with minimum configuration.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, sensor.DOMAIN, @@ -95,7 +95,7 @@ async def test_setup_minimum(hass): @respx.mock async def test_setup_minimum_resource_template(hass): """Test setup with minimum configuration (resource_template).""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, sensor.DOMAIN, @@ -113,7 +113,7 @@ async def test_setup_minimum_resource_template(hass): @respx.mock async def test_setup_duplicate_resource_template(hass): """Test setup with duplicate resources.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 assert await async_setup_component( hass, sensor.DOMAIN, @@ -132,7 +132,7 @@ async def test_setup_duplicate_resource_template(hass): @respx.mock async def test_setup_get(hass): """Test setup with valid configuration.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "sensor", @@ -153,15 +153,26 @@ async def test_setup_get(hass): } }, ) + await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() assert len(hass.states.async_all()) == 1 + assert hass.states.get("sensor.foo").state == "" + await hass.services.async_call( + "homeassistant", + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: "sensor.foo"}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get("sensor.foo").state == "" + @respx.mock async def test_setup_get_digest_auth(hass): """Test setup with valid configuration.""" - respx.get("http://localhost", status_code=200, content="{}") + respx.get("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "sensor", @@ -190,7 +201,7 @@ async def test_setup_get_digest_auth(hass): @respx.mock async def test_setup_post(hass): """Test setup with valid configuration.""" - respx.post("http://localhost", status_code=200, content="{}") + respx.post("http://localhost").respond(status_code=200, json={}) assert await async_setup_component( hass, "sensor", @@ -219,8 +230,7 @@ async def test_setup_post(hass): @respx.mock async def test_setup_get_xml(hass): """Test setup with valid xml configuration.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content="abc", @@ -252,10 +262,7 @@ async def test_setup_get_xml(hass): @respx.mock async def test_setup_query_params(hass): """Test setup with query params.""" - respx.get( - "http://localhost?search=something", - status_code=200, - ) + respx.get("http://localhost", params={"search": "something"}) % 200 assert await async_setup_component( hass, sensor.DOMAIN, @@ -276,11 +283,9 @@ async def test_setup_query_params(hass): async def test_update_with_json_attrs(hass): """Test attributes get extracted from a JSON result.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, - headers={"content-type": CONTENT_TYPE_JSON}, - content='{ "key": "some_json_value" }', + json={"key": "some_json_value"}, ) assert await async_setup_component( hass, @@ -311,11 +316,9 @@ async def test_update_with_json_attrs(hass): async def test_update_with_no_template(hass): """Test update when there is no value template.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, - headers={"content-type": CONTENT_TYPE_JSON}, - content='{ "key": "some_json_value" }', + json={"key": "some_json_value"}, ) assert await async_setup_component( hass, @@ -338,15 +341,14 @@ async def test_update_with_no_template(hass): assert len(hass.states.async_all()) == 1 state = hass.states.get("sensor.foo") - assert state.state == '{ "key": "some_json_value" }' + assert state.state == '{"key": "some_json_value"}' @respx.mock async def test_update_with_json_attrs_no_data(hass, caplog): """Test attributes when no JSON result fetched.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": CONTENT_TYPE_JSON}, content="", @@ -382,11 +384,9 @@ async def test_update_with_json_attrs_no_data(hass, caplog): async def test_update_with_json_attrs_not_dict(hass, caplog): """Test attributes get extracted from a JSON result.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, - headers={"content-type": CONTENT_TYPE_JSON}, - content='["list", "of", "things"]', + json=["list", "of", "things"], ) assert await async_setup_component( hass, @@ -419,8 +419,7 @@ async def test_update_with_json_attrs_not_dict(hass, caplog): async def test_update_with_json_attrs_bad_JSON(hass, caplog): """Test attributes get extracted from a JSON result.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": CONTENT_TYPE_JSON}, content="This is text rather than JSON data.", @@ -456,11 +455,17 @@ async def test_update_with_json_attrs_bad_JSON(hass, caplog): async def test_update_with_json_attrs_with_json_attrs_path(hass): """Test attributes get extracted from a JSON result with a template for the attributes.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, - headers={"content-type": CONTENT_TYPE_JSON}, - content='{ "toplevel": {"master_value": "master", "second_level": {"some_json_key": "some_json_value", "some_json_key2": "some_json_value2" } } }', + json={ + "toplevel": { + "master_value": "master", + "second_level": { + "some_json_key": "some_json_value", + "some_json_key2": "some_json_value2", + }, + }, + }, ) assert await async_setup_component( hass, @@ -494,8 +499,7 @@ async def test_update_with_json_attrs_with_json_attrs_path(hass): async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): """Test attributes get extracted from a JSON result that was converted from XML with a template for the attributes.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content="mastersome_json_valuesome_json_value2", @@ -531,8 +535,7 @@ async def test_update_with_xml_convert_json_attrs_with_json_attrs_path(hass): async def test_update_with_xml_convert_json_attrs_with_jsonattr_template(hass): """Test attributes get extracted from a JSON result that was converted from XML.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content='01255648alexander000bogus000000000upupupup000x0XF0x0XF 0', @@ -573,8 +576,7 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp ): """Test attributes get extracted from a JSON result that was converted from XML with application/xml mime type.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "application/xml"}, content="
13
", @@ -610,8 +612,7 @@ async def test_update_with_application_xml_convert_json_attrs_with_jsonattr_temp async def test_update_with_xml_convert_bad_xml(hass, caplog): """Test attributes get extracted from a XML result with bad xml.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content="", @@ -646,8 +647,7 @@ async def test_update_with_xml_convert_bad_xml(hass, caplog): async def test_update_with_failed_get(hass, caplog): """Test attributes get extracted from a XML result with bad xml.""" - respx.get( - "http://localhost", + respx.get("http://localhost").respond( status_code=200, headers={"content-type": "text/xml"}, content="", @@ -682,7 +682,7 @@ async def test_update_with_failed_get(hass, caplog): async def test_reload(hass): """Verify we can reload reset sensors.""" - respx.get("http://localhost", status_code=200) + respx.get("http://localhost") % 200 await async_setup_component( hass, diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 6ba045d60a..04545a1a42 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -29,7 +29,7 @@ def serial_connect_fail(self): def com_port(): """Mock of a serial port.""" - port = serial.tools.list_ports_common.ListPortInfo() + port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") port.serial_number = "1234" port.manufacturer = "Virtual serial port" port.device = "/dev/ttyUSB1234" diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py index f2da007b5e..4ab2991bd4 100644 --- a/tests/components/roku/__init__.py +++ b/tests/components/roku/__init__.py @@ -8,14 +8,16 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_FRIENDLY_NAME, ATTR_UPNP_SERIAL, ) -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_ID, CONF_NAME from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -HOST = "192.168.1.160" NAME = "Roku 3" +NAME_ROKUTV = '58" Onn Roku TV' + +HOST = "192.168.1.160" SSDP_LOCATION = "http://192.168.1.160/" UPNP_FRIENDLY_NAME = "My Roku 3" UPNP_SERIAL = "1GU48T017973" @@ -26,6 +28,16 @@ MOCK_SSDP_DISCOVERY_INFO = { ATTR_UPNP_SERIAL: UPNP_SERIAL, } +HOMEKIT_HOST = "192.168.1.161" + +MOCK_HOMEKIT_DISCOVERY_INFO = { + CONF_NAME: "onn._hap._tcp.local.", + CONF_HOST: HOMEKIT_HOST, + "properties": { + CONF_ID: "2d:97:da:ee:dc:99", + }, +} + def mock_connection( aioclient_mock: AiohttpClientMocker, diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index a3cda6afa6..16e4a434dc 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,6 +1,6 @@ """Test the Roku config flow.""" from homeassistant.components.roku.const import DOMAIN -from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -12,8 +12,11 @@ from homeassistant.setup import async_setup_component from tests.async_mock import patch from tests.components.roku import ( + HOMEKIT_HOST, HOST, + MOCK_HOMEKIT_DISCOVERY_INFO, MOCK_SSDP_DISCOVERY_INFO, + NAME_ROKUTV, UPNP_FRIENDLY_NAME, mock_connection, setup_integration, @@ -128,6 +131,92 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None: assert len(mock_validate_input.mock_calls) == 1 +async def test_homekit_cannot_connect( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort homekit flow on connection error.""" + mock_connection( + aioclient_mock, + host=HOMEKIT_HOST, + error=True, + ) + + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_HOMEKIT}, + data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_homekit_unknown_error( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort homekit flow on unknown error.""" + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + with patch( + "homeassistant.components.roku.config_flow.Roku.update", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_HOMEKIT}, + data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_homekit_discovery( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the homekit discovery flow.""" + mock_connection(aioclient_mock, device="rokutv", host=HOMEKIT_HOST) + + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + assert result["description_placeholders"] == {CONF_NAME: NAME_ROKUTV} + + with patch( + "homeassistant.components.roku.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roku.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME_ROKUTV + + assert result["data"] + assert result["data"][CONF_HOST] == HOMEKIT_HOST + assert result["data"][CONF_NAME] == NAME_ROKUTV + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + # test abort on existing host + discovery_info = MOCK_HOMEKIT_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_HOMEKIT}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + async def test_ssdp_cannot_connect( hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: @@ -176,7 +265,7 @@ async def test_ssdp_discovery( ) assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "ssdp_confirm" + assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} with patch( diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index fe6a567e32..2850be1145 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -5,7 +5,7 @@ import aiohttp import aioshelly import pytest -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.shelly.const import DOMAIN from tests.async_mock import AsyncMock, Mock, patch @@ -226,6 +226,36 @@ async def test_form_already_configured(hass): assert entry.data["host"] == "1.1.1.1" +async def test_user_setup_ignored_device(hass): + """Test user can successfully setup an ignored device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="shelly", + unique_id="test-mac", + data={"host": "0.0.0.0"}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "aioshelly.get_info", + return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + async def test_form_firmware_unsupported(hass): """Test we abort if device firmware is unsupported.""" result = await hass.config_entries.flow.async_init( @@ -458,14 +488,3 @@ async def test_zeroconf_require_auth(hass): } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_zeroconf_not_shelly(hass): - """Test we filter out non-shelly devices.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - data={"host": "1.1.1.1", "name": "notshelly"}, - context={"source": config_entries.SOURCE_ZEROCONF}, - ) - assert result["type"] == "abort" - assert result["reason"] == "not_shelly" diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index d4ba26bd48..ec7ad592f1 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -72,6 +72,7 @@ async def test_options_flow(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ): + await hass.config_entries.async_setup(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/tado/test_binary_sensor.py b/tests/components/tado/test_binary_sensor.py new file mode 100644 index 0000000000..39dd068f5a --- /dev/null +++ b/tests/components/tado/test_binary_sensor.py @@ -0,0 +1,14 @@ +"""The sensor tests for the tado platform.""" + +from homeassistant.const import STATE_ON + +from .util import async_init_integration + + +async def test_home_create_binary_sensors(hass): + """Test creation of home binary sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.wr1_connection_state") + assert state.state == STATE_ON diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py index 2ea2c0508e..646e774153 100644 --- a/tests/components/tado/test_sensor.py +++ b/tests/components/tado/test_sensor.py @@ -85,12 +85,3 @@ async def test_water_heater_create_sensors(hass): state = hass.states.get("sensor.water_heater_power") assert state.state == "ON" - - -async def test_home_create_sensors(hass): - """Test creation of home sensors.""" - - await async_init_integration(hass) - - state = hass.states.get("sensor.home_name_tado_bridge_status") - assert state.state == "True" diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 5cadc20218..1aca8c84e0 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -7,6 +7,7 @@ from hatasmota.utils import ( get_topic_tele_state, get_topic_tele_will, ) +import pytest from homeassistant.components import fan from homeassistant.components.tasmota.const import DEFAULT_PREFIX @@ -152,6 +153,33 @@ async def test_sending_mqtt_commands(hass, mqtt_mock, setup_tasmota): ) +async def test_invalid_fan_speed(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["if"] = 1 + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("fan.tasmota") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Set an unsupported speed and verify MQTT message is not sent + with pytest.raises(ValueError) as excinfo: + await common.async_set_speed(hass, "fan.tasmota", "no_such_speed") + assert "Unsupported speed no_such_speed" in str(excinfo.value) + mqtt_mock.async_publish.assert_not_called() + + async def test_availability_when_connection_lost( hass, mqtt_client_mock, mqtt_mock, setup_tasmota ): diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index e966188afd..5f33aa2be4 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -858,6 +858,7 @@ async def test_zeroconf_ignore( async def test_zeroconf_no_unique_id( hass: HomeAssistantType, + vizio_guess_device_type: pytest.fixture, vizio_no_unique_id: pytest.fixture, ) -> None: """Test zeroconf discovery aborts when unique_id is None.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 996d46e08a..0d11ec2289 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -40,6 +40,7 @@ from homeassistant.components.vizio.const import ( CONF_ADDITIONAL_CONFIGS, CONF_APPS, CONF_VOLUME_STEP, + DEFAULT_VOLUME_STEP, DOMAIN, SERVICE_UPDATE_SETTING, VIZIO_SCHEMA, @@ -259,6 +260,7 @@ async def _test_service( **kwargs, ) -> None: """Test generic Vizio media player entity service.""" + kwargs["log_api_exception"] = False service_data = {ATTR_ENTITY_ID: ENTITY_ID} if additional_service_data: service_data.update(additional_service_data) @@ -378,13 +380,27 @@ async def test_services( {ATTR_INPUT_SOURCE: "USB"}, "USB", ) - await _test_service(hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None) - await _test_service(hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_DOWN, None) await _test_service( - hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 1} + hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None, num=DEFAULT_VOLUME_STEP ) await _test_service( - hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0} + hass, MP_DOMAIN, "vol_down", SERVICE_VOLUME_DOWN, None, num=DEFAULT_VOLUME_STEP + ) + await _test_service( + hass, + MP_DOMAIN, + "vol_up", + SERVICE_VOLUME_SET, + {ATTR_MEDIA_VOLUME_LEVEL: 1}, + num=(100 - 15), + ) + await _test_service( + hass, + MP_DOMAIN, + "vol_down", + SERVICE_VOLUME_SET, + {ATTR_MEDIA_VOLUME_LEVEL: 0}, + num=(15 - 0), ) await _test_service(hass, MP_DOMAIN, "ch_up", SERVICE_MEDIA_NEXT_TRACK, None) await _test_service(hass, MP_DOMAIN, "ch_down", SERVICE_MEDIA_PREVIOUS_TRACK, None) @@ -394,6 +410,9 @@ async def test_services( "set_setting", SERVICE_SELECT_SOUND_MODE, {ATTR_SOUND_MODE: "Music"}, + "audio", + "eq", + "Music", ) # Test that the update_setting service does config validation/transformation correctly await _test_service( diff --git a/tests/components/wemo/__init__.py b/tests/components/wemo/__init__.py new file mode 100644 index 0000000000..33bdcacd37 --- /dev/null +++ b/tests/components/wemo/__init__.py @@ -0,0 +1 @@ +"""Tests for the wemo component.""" diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py new file mode 100644 index 0000000000..573de21c69 --- /dev/null +++ b/tests/components/wemo/conftest.py @@ -0,0 +1,80 @@ +"""Fixtures for pywemo.""" +import asyncio + +import pytest +import pywemo + +from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.async_mock import create_autospec, patch + +MOCK_HOST = "127.0.0.1" +MOCK_PORT = 50000 +MOCK_NAME = "WemoDeviceName" +MOCK_SERIAL_NUMBER = "WemoSerialNumber" + + +@pytest.fixture(name="pywemo_model") +def pywemo_model_fixture(): + """Fixture containing a pywemo class name used by pywemo_device_fixture.""" + return "Insight" + + +@pytest.fixture(name="pywemo_registry") +def pywemo_registry_fixture(): + """Fixture for SubscriptionRegistry instances.""" + registry = create_autospec(pywemo.SubscriptionRegistry, instance=True) + + registry.callbacks = {} + registry.semaphore = asyncio.Semaphore(value=0) + + def on_func(device, type_filter, callback): + registry.callbacks[device.name] = callback + registry.semaphore.release() + + registry.on.side_effect = on_func + + with patch("pywemo.SubscriptionRegistry", return_value=registry): + yield registry + + +@pytest.fixture(name="pywemo_device") +def pywemo_device_fixture(pywemo_registry, pywemo_model): + """Fixture for WeMoDevice instances.""" + device = create_autospec(getattr(pywemo, pywemo_model), instance=True) + device.host = MOCK_HOST + device.port = MOCK_PORT + device.name = MOCK_NAME + device.serialnumber = MOCK_SERIAL_NUMBER + device.model_name = pywemo_model + device.get_state.return_value = 0 # Default to Off + + url = f"http://{MOCK_HOST}:{MOCK_PORT}/setup.xml" + with patch("pywemo.setup_url_for_address", return_value=url), patch( + "pywemo.discovery.device_from_description", return_value=device + ): + yield device + + +@pytest.fixture(name="wemo_entity") +async def async_wemo_entity_fixture(hass, pywemo_device): + """Fixture for a Wemo entity in hass.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [f"{MOCK_HOST}:{MOCK_PORT}"], + }, + }, + ) + await hass.async_block_till_done() + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_registry.entities.values()) + assert len(entity_entries) == 1 + + yield entity_entries[0] diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py new file mode 100644 index 0000000000..16a2f8b3f0 --- /dev/null +++ b/tests/components/wemo/entity_test_helpers.py @@ -0,0 +1,167 @@ +"""Test cases that are in common among wemo platform modules. + +This is not a test module. These test methods are used by the platform test modules. +""" +import asyncio +import threading + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + +from tests.async_mock import patch + + +def _perform_registry_callback(hass, pywemo_registry, pywemo_device): + """Return a callable method to trigger a state callback from the device.""" + + @callback + def async_callback(): + # Cause a state update callback to be triggered by the device. + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + return hass.async_block_till_done() + + return async_callback + + +def _perform_async_update(hass, wemo_entity): + """Return a callable method to cause hass to update the state of the entity.""" + + @callback + def async_callback(): + return hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + return async_callback + + +async def _async_multiple_call_helper( + hass, + pywemo_registry, + wemo_entity, + pywemo_device, + call1, + call2, + update_polling_method=None, +): + """Create two calls (call1 & call2) in parallel; verify only one polls the device. + + The platform entity should only perform one update poll on the device at a time. + Any parallel updates that happen at the same time should be ignored. This is + verified by blocking in the update polling method. The polling method should + only be called once as a result of calling call1 & call2 simultaneously. + """ + # get_state is called outside the event loop. Use non-async Python Event. + event = threading.Event() + + def get_update(force_update=True): + event.wait() + + update_polling_method = update_polling_method or pywemo_device.get_state + update_polling_method.side_effect = get_update + + # One of these two calls will block on `event`. The other will return right + # away because the `_update_lock` is held. + _, pending = await asyncio.wait( + [call1(), call2()], return_when=asyncio.FIRST_COMPLETED + ) + + # Allow the blocked call to return. + event.set() + if pending: + await asyncio.wait(pending) + + # Make sure the state update only happened once. + update_polling_method.assert_called_once() + + +async def test_async_update_locked_callback_and_update( + hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs +): + """Test that a callback and a state update request can't both happen at the same time. + + When a state update is received via a callback from the device at the same time + as hass is calling `async_update`, verify that only one of the updates proceeds. + """ + await async_setup_component(hass, HA_DOMAIN, {}) + callback = _perform_registry_callback(hass, pywemo_registry, pywemo_device) + update = _perform_async_update(hass, wemo_entity) + await _async_multiple_call_helper( + hass, pywemo_registry, wemo_entity, pywemo_device, callback, update, **kwargs + ) + + +async def test_async_update_locked_multiple_updates( + hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs +): + """Test that two hass async_update state updates do not proceed at the same time.""" + await async_setup_component(hass, HA_DOMAIN, {}) + update = _perform_async_update(hass, wemo_entity) + await _async_multiple_call_helper( + hass, pywemo_registry, wemo_entity, pywemo_device, update, update, **kwargs + ) + + +async def test_async_update_locked_multiple_callbacks( + hass, pywemo_registry, wemo_entity, pywemo_device, **kwargs +): + """Test that two device callback state updates do not proceed at the same time.""" + await async_setup_component(hass, HA_DOMAIN, {}) + callback = _perform_registry_callback(hass, pywemo_registry, pywemo_device) + await _async_multiple_call_helper( + hass, pywemo_registry, wemo_entity, pywemo_device, callback, callback, **kwargs + ) + + +async def test_async_locked_update_with_exception( + hass, wemo_entity, pywemo_device, update_polling_method=None +): + """Test that the entity becomes unavailable when communication is lost.""" + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + await async_setup_component(hass, HA_DOMAIN, {}) + update_polling_method = update_polling_method or pywemo_device.get_state + update_polling_method.side_effect = AttributeError + + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE + pywemo_device.reconnect_with_device.assert_called_with() + + +async def test_async_update_with_timeout_and_recovery(hass, wemo_entity, pywemo_device): + """Test that the entity becomes unavailable after a timeout, and that it recovers.""" + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + await async_setup_component(hass, HA_DOMAIN, {}) + + with patch("async_timeout.timeout", side_effect=asyncio.TimeoutError): + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE + + # Check that the entity recovers and is available after the update succeeds. + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_binary_sensor.py b/tests/components/wemo/test_binary_sensor.py new file mode 100644 index 0000000000..1bf6f0f3be --- /dev/null +++ b/tests/components/wemo/test_binary_sensor.py @@ -0,0 +1,82 @@ +"""Tests for the Wemo binary_sensor entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from . import entity_test_helpers + + +@pytest.fixture +def pywemo_model(): + """Pywemo Motion models use the binary_sensor platform.""" + return "Motion" + + +# Tests that are in common among wemo platforms. These test methods will be run +# in the scope of this test module. They will run using the pywemo_model from +# this test module (Motion). +test_async_update_locked_multiple_updates = ( + entity_test_helpers.test_async_update_locked_multiple_updates +) +test_async_update_locked_multiple_callbacks = ( + entity_test_helpers.test_async_update_locked_multiple_callbacks +) +test_async_update_locked_callback_and_update = ( + entity_test_helpers.test_async_update_locked_callback_and_update +) +test_async_locked_update_with_exception = ( + entity_test_helpers.test_async_locked_update_with_exception +) +test_async_update_with_timeout_and_recovery = ( + entity_test_helpers.test_async_update_with_timeout_and_recovery +) + + +async def test_binary_sensor_registry_state_callback( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the binary_sensor receives state updates from the registry.""" + # On state. + pywemo_device.get_state.return_value = 1 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_binary_sensor_update_entity( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the binary_sensor performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_device.get_state.return_value = 1 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_fan.py b/tests/components/wemo/test_fan.py new file mode 100644 index 0000000000..38055ba972 --- /dev/null +++ b/tests/components/wemo/test_fan.py @@ -0,0 +1,120 @@ +"""Tests for the Wemo fan entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.wemo import fan +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from . import entity_test_helpers + + +@pytest.fixture +def pywemo_model(): + """Pywemo Humidifier models use the fan platform.""" + return "Humidifier" + + +# Tests that are in common among wemo platforms. These test methods will be run +# in the scope of this test module. They will run using the pywemo_model from +# this test module (Humidifier). +test_async_update_locked_multiple_updates = ( + entity_test_helpers.test_async_update_locked_multiple_updates +) +test_async_update_locked_multiple_callbacks = ( + entity_test_helpers.test_async_update_locked_multiple_callbacks +) +test_async_update_locked_callback_and_update = ( + entity_test_helpers.test_async_update_locked_callback_and_update +) +test_async_locked_update_with_exception = ( + entity_test_helpers.test_async_locked_update_with_exception +) +test_async_update_with_timeout_and_recovery = ( + entity_test_helpers.test_async_update_with_timeout_and_recovery +) + + +async def test_fan_registry_state_callback( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the fan receives state updates from the registry.""" + # On state. + pywemo_device.get_state.return_value = 1 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_fan_update_entity(hass, pywemo_registry, pywemo_device, wemo_entity): + """Verify that the fan performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_device.get_state.return_value = 1 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_fan_reset_filter_service(hass, pywemo_device, wemo_entity): + """Verify that SERVICE_RESET_FILTER_LIFE is registered and works.""" + assert await hass.services.async_call( + DOMAIN, + fan.SERVICE_RESET_FILTER_LIFE, + {ATTR_ENTITY_ID: wemo_entity.entity_id}, + blocking=True, + ) + pywemo_device.reset_filter_life.assert_called_with() + + +@pytest.mark.parametrize( + "test_input,expected", + [ + (0, fan.WEMO_HUMIDITY_45), + (45, fan.WEMO_HUMIDITY_45), + (50, fan.WEMO_HUMIDITY_50), + (55, fan.WEMO_HUMIDITY_55), + (60, fan.WEMO_HUMIDITY_60), + (100, fan.WEMO_HUMIDITY_100), + ], +) +async def test_fan_set_humidity_service( + hass, pywemo_device, wemo_entity, test_input, expected +): + """Verify that SERVICE_SET_HUMIDITY is registered and works.""" + assert await hass.services.async_call( + DOMAIN, + fan.SERVICE_SET_HUMIDITY, + { + ATTR_ENTITY_ID: wemo_entity.entity_id, + fan.ATTR_TARGET_HUMIDITY: test_input, + }, + blocking=True, + ) + pywemo_device.set_humidity.assert_called_with(expected) diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py new file mode 100644 index 0000000000..2af91c0fe3 --- /dev/null +++ b/tests/components/wemo/test_init.py @@ -0,0 +1,141 @@ +"""Tests for the wemo component.""" +from datetime import timedelta + +import pywemo + +from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC, WemoDiscovery +from homeassistant.components.wemo.const import DOMAIN +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_SERIAL_NUMBER + +from tests.async_mock import create_autospec, patch +from tests.common import async_fire_time_changed + + +async def test_config_no_config(hass): + """Component setup succeeds when there are no config entry for the domain.""" + assert await async_setup_component(hass, DOMAIN, {}) + + +async def test_config_no_static(hass): + """Component setup succeeds when there are no static config entries.""" + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: False}}) + + +async def test_static_duplicate_static_entry(hass, pywemo_device): + """Duplicate static entries are merged into a single entity.""" + static_config_entry = f"{MOCK_HOST}:{MOCK_PORT}" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [ + static_config_entry, + static_config_entry, + ], + }, + }, + ) + await hass.async_block_till_done() + entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_reg.entities.values()) + assert len(entity_entries) == 1 + + +async def test_static_config_with_port(hass, pywemo_device): + """Static device with host and port is added and removed.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [f"{MOCK_HOST}:{MOCK_PORT}"], + }, + }, + ) + await hass.async_block_till_done() + entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_reg.entities.values()) + assert len(entity_entries) == 1 + + +async def test_static_config_without_port(hass, pywemo_device): + """Static device with host and no port is added and removed.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [MOCK_HOST], + }, + }, + ) + await hass.async_block_till_done() + entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_reg.entities.values()) + assert len(entity_entries) == 1 + + +async def test_static_config_with_invalid_host(hass): + """Component setup fails if a static host is invalid.""" + setup_success = await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DISCOVERY: False, + CONF_STATIC: [""], + }, + }, + ) + assert not setup_success + + +async def test_discovery(hass, pywemo_registry): + """Verify that discovery dispatches devices to the platform for setup.""" + + def create_device(counter): + """Create a unique mock Motion detector device for each counter value.""" + device = create_autospec(pywemo.Motion, instance=True) + device.host = f"{MOCK_HOST}_{counter}" + device.port = MOCK_PORT + counter + device.name = f"{MOCK_NAME}_{counter}" + device.serialnumber = f"{MOCK_SERIAL_NUMBER}_{counter}" + device.model_name = "Motion" + device.get_state.return_value = 0 # Default to Off + return device + + pywemo_devices = [create_device(0), create_device(1)] + # Setup the component and start discovery. + with patch( + "pywemo.discover_devices", return_value=pywemo_devices + ) as mock_discovery: + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}} + ) + await pywemo_registry.semaphore.acquire() # Returns after platform setup. + mock_discovery.assert_called() + pywemo_devices.append(create_device(2)) + + # Test that discovery runs periodically and the async_dispatcher_send code works. + async_fire_time_changed( + hass, + dt.utcnow() + + timedelta(seconds=WemoDiscovery.ADDITIONAL_SECONDS_BETWEEN_SCANS + 1), + ) + await hass.async_block_till_done() + + # Verify that the expected number of devices were setup. + entity_reg = await hass.helpers.entity_registry.async_get_registry() + entity_entries = list(entity_reg.entities.values()) + assert len(entity_entries) == 3 + + # Verify that hass stops cleanly. + await hass.async_stop() + await hass.async_block_till_done() diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py new file mode 100644 index 0000000000..1a36e5421e --- /dev/null +++ b/tests/components/wemo/test_light_bridge.py @@ -0,0 +1,114 @@ +"""Tests for the Wemo light entity via the bridge.""" +import pytest +import pywemo + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.components.wemo.light import MIN_TIME_BETWEEN_SCANS +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from . import entity_test_helpers + +from tests.async_mock import create_autospec, patch + + +@pytest.fixture +def pywemo_model(): + """Pywemo Bridge models use the light platform (WemoLight class).""" + return "Bridge" + + +# Note: The ordering of where the pywemo_bridge_light comes in test arguments matters. +# In test methods, the pywemo_bridge_light fixture argument must come before the +# wemo_entity fixture argument. +@pytest.fixture(name="pywemo_bridge_light") +def pywemo_bridge_light_fixture(pywemo_device): + """Fixture for Bridge.Light WeMoDevice instances.""" + light = create_autospec(pywemo.ouimeaux_device.bridge.Light, instance=True) + light.uniqueID = pywemo_device.serialnumber + light.name = pywemo_device.name + light.bridge = pywemo_device + light.state = {"onoff": 0} + pywemo_device.Lights = {pywemo_device.serialnumber: light} + return light + + +def _bypass_throttling(): + """Bypass the util.Throttle on the update_lights method.""" + utcnow = dt_util.utcnow() + + def increment_and_return_time(): + nonlocal utcnow + utcnow += MIN_TIME_BETWEEN_SCANS + return utcnow + + return patch("homeassistant.util.utcnow", side_effect=increment_and_return_time) + + +async def test_async_update_locked_multiple_updates( + hass, pywemo_registry, pywemo_bridge_light, wemo_entity, pywemo_device +): + """Test that two state updates do not proceed at the same time.""" + pywemo_device.bridge_update.reset_mock() + + with _bypass_throttling(): + await entity_test_helpers.test_async_update_locked_multiple_updates( + hass, + pywemo_registry, + wemo_entity, + pywemo_device, + update_polling_method=pywemo_device.bridge_update, + ) + + +async def test_async_update_with_timeout_and_recovery( + hass, pywemo_bridge_light, wemo_entity, pywemo_device +): + """Test that the entity becomes unavailable after a timeout, and that it recovers.""" + await entity_test_helpers.test_async_update_with_timeout_and_recovery( + hass, wemo_entity, pywemo_device + ) + + +async def test_async_locked_update_with_exception( + hass, pywemo_bridge_light, wemo_entity, pywemo_device +): + """Test that the entity becomes unavailable when communication is lost.""" + with _bypass_throttling(): + await entity_test_helpers.test_async_locked_update_with_exception( + hass, + wemo_entity, + pywemo_device, + update_polling_method=pywemo_device.bridge_update, + ) + + +async def test_light_update_entity( + hass, pywemo_registry, pywemo_bridge_light, wemo_entity +): + """Verify that the light performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_bridge_light.state = {"onoff": 1} + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_bridge_light.state = {"onoff": 0} + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_light_dimmer.py b/tests/components/wemo/test_light_dimmer.py new file mode 100644 index 0000000000..45fdd01a64 --- /dev/null +++ b/tests/components/wemo/test_light_dimmer.py @@ -0,0 +1,80 @@ +"""Tests for the Wemo standalone/non-bridge light entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from . import entity_test_helpers + + +@pytest.fixture +def pywemo_model(): + """Pywemo Dimmer models use the light platform (WemoDimmer class).""" + return "Dimmer" + + +# Tests that are in common among wemo platforms. These test methods will be run +# in the scope of this test module. They will run using the pywemo_model from +# this test module (Dimmer). +test_async_update_locked_multiple_updates = ( + entity_test_helpers.test_async_update_locked_multiple_updates +) +test_async_update_locked_multiple_callbacks = ( + entity_test_helpers.test_async_update_locked_multiple_callbacks +) +test_async_update_locked_callback_and_update = ( + entity_test_helpers.test_async_update_locked_callback_and_update +) +test_async_locked_update_with_exception = ( + entity_test_helpers.test_async_locked_update_with_exception +) +test_async_update_with_timeout_and_recovery = ( + entity_test_helpers.test_async_update_with_timeout_and_recovery +) + + +async def test_light_registry_state_callback( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the light receives state updates from the registry.""" + # On state. + pywemo_device.get_state.return_value = 1 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_light_update_entity(hass, pywemo_registry, pywemo_device, wemo_entity): + """Verify that the light performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_device.get_state.return_value = 1 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/wemo/test_switch.py b/tests/components/wemo/test_switch.py new file mode 100644 index 0000000000..05151d38be --- /dev/null +++ b/tests/components/wemo/test_switch.py @@ -0,0 +1,80 @@ +"""Tests for the Wemo switch entity.""" + +import pytest + +from homeassistant.components.homeassistant import ( + DOMAIN as HA_DOMAIN, + SERVICE_UPDATE_ENTITY, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component + +from . import entity_test_helpers + + +@pytest.fixture +def pywemo_model(): + """Pywemo LightSwitch models use the switch platform.""" + return "LightSwitch" + + +# Tests that are in common among wemo platforms. These test methods will be run +# in the scope of this test module. They will run using the pywemo_model from +# this test module (LightSwitch). +test_async_update_locked_multiple_updates = ( + entity_test_helpers.test_async_update_locked_multiple_updates +) +test_async_update_locked_multiple_callbacks = ( + entity_test_helpers.test_async_update_locked_multiple_callbacks +) +test_async_update_locked_callback_and_update = ( + entity_test_helpers.test_async_update_locked_callback_and_update +) +test_async_locked_update_with_exception = ( + entity_test_helpers.test_async_locked_update_with_exception +) +test_async_update_with_timeout_and_recovery = ( + entity_test_helpers.test_async_update_with_timeout_and_recovery +) + + +async def test_switch_registry_state_callback( + hass, pywemo_registry, pywemo_device, wemo_entity +): + """Verify that the switch receives state updates from the registry.""" + # On state. + pywemo_device.get_state.return_value = 1 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") + await hass.async_block_till_done() + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF + + +async def test_switch_update_entity(hass, pywemo_registry, pywemo_device, wemo_entity): + """Verify that the switch performs state updates.""" + await async_setup_component(hass, HA_DOMAIN, {}) + + # On state. + pywemo_device.get_state.return_value = 1 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_ON + + # Off state. + pywemo_device.get_state.return_value = 0 + await hass.services.async_call( + HA_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, + blocking=True, + ) + assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 8767953b36..6b79c55291 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -242,13 +242,17 @@ async def test_zeroconf_match(hass, mock_zeroconf): handlers[0]( zeroconf, "_http._tcp.local.", - "shelly108._http._tcp.local.", + "Shelly108._http._tcp.local.", ServiceStateChange.Added, ) with patch.dict( zc_gen.ZEROCONF, - {"_http._tcp.local.": [{"domain": "shelly", "name": "shelly*"}]}, + { + "_http._tcp.local.": [ + {"domain": "shelly", "name": "shelly*", "macaddress": "FFAADD*"} + ] + }, clear=True, ), patch.object( hass.config_entries.flow, "async_init" diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 871f91d447..77fdfb7d48 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -24,19 +24,27 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import patch +from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, async_fire_time_changed @pytest.fixture -async def mock_light(hass): +async def mock_entry(hass): + """Create a mock light entity.""" + return MockConfigEntry(domain=DOMAIN) + + +@pytest.fixture +async def mock_light(hass, mock_entry): """Create a mock light entity.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_entry = MockConfigEntry(domain=DOMAIN) mock_entry.add_to_hass(hass) - light = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") + light = MagicMock(spec=pyzerproc.Light) + light.address = "AA:BB:CC:DD:EE:FF" + light.name = "LEDBlue-CCDDEEFF" + light.is_connected.return_value = False mock_state = pyzerproc.LightState(False, (0, 0, 0)) @@ -49,31 +57,36 @@ async def mock_light(hass): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() + light.is_connected.return_value = True + return light -async def test_init(hass): +async def test_init(hass, mock_entry): """Test platform setup.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_entry = MockConfigEntry(domain=DOMAIN) mock_entry.add_to_hass(hass) - mock_light_1 = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") - mock_light_2 = pyzerproc.Light("11:22:33:44:55:66", "LEDBlue-33445566") + mock_light_1 = MagicMock(spec=pyzerproc.Light) + mock_light_1.address = "AA:BB:CC:DD:EE:FF" + mock_light_1.name = "LEDBlue-CCDDEEFF" + mock_light_1.is_connected.return_value = True + + mock_light_2 = MagicMock(spec=pyzerproc.Light) + mock_light_2.address = "11:22:33:44:55:66" + mock_light_2.name = "LEDBlue-33445566" + mock_light_2.is_connected.return_value = True mock_state_1 = pyzerproc.LightState(False, (0, 0, 0)) mock_state_2 = pyzerproc.LightState(True, (0, 80, 255)) + mock_light_1.get_state.return_value = mock_state_1 + mock_light_2.get_state.return_value = mock_state_2 + with patch( "homeassistant.components.zerproc.light.pyzerproc.discover", return_value=[mock_light_1, mock_light_2], - ), patch.object(mock_light_1, "connect"), patch.object( - mock_light_2, "connect" - ), patch.object( - mock_light_1, "get_state", return_value=mock_state_1 - ), patch.object( - mock_light_2, "get_state", return_value=mock_state_2 ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() @@ -98,22 +111,17 @@ async def test_init(hass): ATTR_XY_COLOR: (0.138, 0.08), } - with patch.object(hass.loop, "stop"), patch.object( - mock_light_1, "disconnect" - ) as mock_disconnect_1, patch.object( - mock_light_2, "disconnect" - ) as mock_disconnect_2: + with patch.object(hass.loop, "stop"): await hass.async_stop() - assert mock_disconnect_1.called - assert mock_disconnect_2.called + assert mock_light_1.disconnect.called + assert mock_light_2.disconnect.called -async def test_discovery_exception(hass): +async def test_discovery_exception(hass, mock_entry): """Test platform setup.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_entry = MockConfigEntry(domain=DOMAIN) mock_entry.add_to_hass(hass) with patch( @@ -127,26 +135,52 @@ async def test_discovery_exception(hass): assert len(hass.data[DOMAIN]["addresses"]) == 0 -async def test_connect_exception(hass): +async def test_connect_exception(hass, mock_entry): """Test platform setup.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mock_entry = MockConfigEntry(domain=DOMAIN) mock_entry.add_to_hass(hass) - mock_light = pyzerproc.Light("AA:BB:CC:DD:EE:FF", "LEDBlue-CCDDEEFF") + mock_light_1 = MagicMock(spec=pyzerproc.Light) + mock_light_1.address = "AA:BB:CC:DD:EE:FF" + mock_light_1.name = "LEDBlue-CCDDEEFF" + mock_light_1.is_connected.return_value = False + + mock_light_2 = MagicMock(spec=pyzerproc.Light) + mock_light_2.address = "11:22:33:44:55:66" + mock_light_2.name = "LEDBlue-33445566" + mock_light_2.is_connected.return_value = False with patch( "homeassistant.components.zerproc.light.pyzerproc.discover", - return_value=[mock_light], + return_value=[mock_light_1, mock_light_2], ), patch.object( - mock_light, "connect", side_effect=pyzerproc.ZerprocException("TEST") + mock_light_1, "connect", side_effect=pyzerproc.ZerprocException("TEST") ): await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - # The exception should be captured and no entities should be added - assert len(hass.data[DOMAIN]["addresses"]) == 0 + # The exception connecting to light 1 should be captured, but light 2 + # should still be added + assert len(hass.data[DOMAIN]["addresses"]) == 1 + + +async def test_remove_entry(hass, mock_light, mock_entry): + """Test platform setup.""" + with patch.object(mock_light, "disconnect") as mock_disconnect: + await hass.config_entries.async_remove(mock_entry.entry_id) + + assert mock_disconnect.called + + +async def test_remove_entry_exceptions_caught(hass, mock_light, mock_entry): + """Assert that disconnect exceptions are caught.""" + with patch.object( + mock_light, "disconnect", side_effect=pyzerproc.ZerprocException("Mock error") + ) as mock_disconnect: + await hass.config_entries.async_remove(mock_entry.entry_id) + + assert mock_disconnect.called async def test_light_turn_on(hass, mock_light): diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index afae1b661a..e0c31d38bb 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -14,7 +14,7 @@ import homeassistant.components.zha.core.registries as registries from .common import get_zha_gateway, make_zcl_header -import tests.async_mock +from tests.async_mock import AsyncMock, patch from tests.common import async_capture_events @@ -38,9 +38,26 @@ async def zha_gateway(hass, setup_zha): @pytest.fixture -def channel_pool(): +def zigpy_coordinator_device(zigpy_device_mock): + """Coordinator device fixture.""" + + coordinator = zigpy_device_mock( + {1: {"in_clusters": [0x1000], "out_clusters": [], "device_type": 0x1234}}, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + with patch.object(coordinator, "add_to_group", AsyncMock(return_value=[0])): + yield coordinator + + +@pytest.fixture +def channel_pool(zigpy_coordinator_device): """Endpoint Channels fixture.""" ch_pool_mock = mock.MagicMock(spec_set=zha_channels.ChannelPool) + ch_pool_mock.endpoint.device.application.get_device.return_value = ( + zigpy_coordinator_device + ) type(ch_pool_mock).skip_configuration = mock.PropertyMock(return_value=False) ch_pool_mock.id = 1 return ch_pool_mock @@ -117,7 +134,6 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock): (0x0406, 1, {"occupancy"}), (0x0702, 1, {"instantaneous_demand"}), (0x0B04, 1, {"active_power"}), - (0x1000, 1, {}), ], ) async def test_in_channel_config( @@ -174,7 +190,6 @@ async def test_in_channel_config( (0x0406, 1), (0x0702, 1), (0x0B04, 1), - (0x1000, 1), ], ) async def test_out_channel_config( @@ -386,12 +401,12 @@ async def test_ep_channels_configure(channel): ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_2 = channel(zha_const.CHANNEL_LEVEL, 8) ch_3 = channel(zha_const.CHANNEL_COLOR, 768) - ch_3.async_configure = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) - ch_3.async_initialize = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) + ch_3.async_configure = AsyncMock(side_effect=asyncio.TimeoutError) + ch_3.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError) ch_4 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_5 = channel(zha_const.CHANNEL_LEVEL, 8) - ch_5.async_configure = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) - ch_5.async_initialize = tests.async_mock.AsyncMock(side_effect=asyncio.TimeoutError) + ch_5.async_configure = AsyncMock(side_effect=asyncio.TimeoutError) + ch_5.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError) channels = mock.MagicMock(spec_set=zha_channels.Channels) type(channels).semaphore = mock.PropertyMock(return_value=asyncio.Semaphore(3)) @@ -427,8 +442,8 @@ async def test_poll_control_configure(poll_control_ch): async def test_poll_control_checkin_response(poll_control_ch): """Test poll control channel checkin response.""" - rsp_mock = tests.async_mock.AsyncMock() - set_interval_mock = tests.async_mock.AsyncMock() + rsp_mock = AsyncMock() + set_interval_mock = AsyncMock() cluster = poll_control_ch.cluster patch_1 = mock.patch.object(cluster, "checkin_response", rsp_mock) patch_2 = mock.patch.object(cluster, "set_long_poll_interval", set_interval_mock) @@ -449,7 +464,7 @@ async def test_poll_control_checkin_response(poll_control_ch): async def test_poll_control_cluster_command(hass, poll_control_device): """Test poll control channel response to cluster command.""" - checkin_mock = tests.async_mock.AsyncMock() + checkin_mock = AsyncMock() poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"] cluster = poll_control_ch.cluster events = async_capture_events(hass, "zha_event") @@ -474,3 +489,60 @@ async def test_poll_control_cluster_command(hass, poll_control_device): assert data["args"][2] is mock.sentinel.args3 assert data["unique_id"] == "00:11:22:33:44:55:66:77:1:0x0020" assert data["device_id"] == poll_control_device.device_id + + +@pytest.fixture +def zigpy_zll_device(zigpy_device_mock): + """ZLL device fixture.""" + + return zigpy_device_mock( + {1: {"in_clusters": [0x1000], "out_clusters": [], "device_type": 0x1234}}, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + + +async def test_zll_device_groups( + zigpy_zll_device, channel_pool, zigpy_coordinator_device +): + """Test adding coordinator to ZLL groups.""" + + cluster = zigpy_zll_device.endpoints[1].lightlink + channel = zha_channels.lightlink.LightLink(cluster, channel_pool) + + with patch.object( + cluster, "command", AsyncMock(return_value=[1, 0, []]) + ) as cmd_mock: + await channel.async_configure() + assert cmd_mock.await_count == 1 + assert ( + cluster.server_commands[cmd_mock.await_args[0][0]][0] + == "get_group_identifiers" + ) + assert cluster.bind.call_count == 0 + assert zigpy_coordinator_device.add_to_group.await_count == 1 + assert zigpy_coordinator_device.add_to_group.await_args[0][0] == 0x0000 + + zigpy_coordinator_device.add_to_group.reset_mock() + group_1 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xABCD, 0x00) + group_2 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xAABB, 0x00) + with patch.object( + cluster, "command", AsyncMock(return_value=[1, 0, [group_1, group_2]]) + ) as cmd_mock: + await channel.async_configure() + assert cmd_mock.await_count == 1 + assert ( + cluster.server_commands[cmd_mock.await_args[0][0]][0] + == "get_group_identifiers" + ) + assert cluster.bind.call_count == 0 + assert zigpy_coordinator_device.add_to_group.await_count == 2 + assert ( + zigpy_coordinator_device.add_to_group.await_args_list[0][0][0] + == group_1.group_id + ) + assert ( + zigpy_coordinator_device.add_to_group.await_args_list[1][0][0] + == group_2.group_id + ) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 709b9a0ff2..6fcc369182 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -19,7 +19,7 @@ from tests.common import MockConfigEntry def com_port(): """Mock of a serial port.""" - port = serial.tools.list_ports_common.ListPortInfo() + port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") port.serial_number = "1234" port.manufacturer = "Virtual serial port" port.device = "/dev/ttyUSB1234" diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index b12b924937..65be13fd96 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -3,6 +3,7 @@ import pytest import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.hvac as hvac +import zigpy.zcl.foundation as zcl_f from homeassistant.components import fan from homeassistant.components.fan import ( @@ -10,11 +11,13 @@ from homeassistant.components.fan import ( DOMAIN, SERVICE_SET_SPEED, SPEED_HIGH, + SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, ) from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.zha.core.discovery import GROUP_PROBE +from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -23,6 +26,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.setup import async_setup_component from .common import ( async_enable_traffic, @@ -33,7 +37,7 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import call +from tests.async_mock import AsyncMock, call, patch IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @@ -49,7 +53,9 @@ def zigpy_device(zigpy_device_mock): "device_type": zha.DeviceType.ON_OFF_SWITCH, } } - return zigpy_device_mock(endpoints) + return zigpy_device_mock( + endpoints, node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00" + ) @pytest.fixture @@ -59,7 +65,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [], + "in_clusters": [general.Groups.cluster_id], "out_clusters": [], "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, } @@ -80,14 +86,20 @@ async def device_fan_1(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [general.OnOff.cluster_id, hvac.Fan.cluster_id], + "in_clusters": [ + general.Groups.cluster_id, + general.OnOff.cluster_id, + hvac.Fan.cluster_id, + ], "out_clusters": [], - } + "device_type": zha.DeviceType.ON_OFF_LIGHT, + }, }, ieee=IEEE_GROUPABLE_DEVICE, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True + await hass.async_block_till_done() return zha_device @@ -99,17 +111,20 @@ async def device_fan_2(hass, zigpy_device_mock, zha_device_joined): { 1: { "in_clusters": [ + general.Groups.cluster_id, general.OnOff.cluster_id, hvac.Fan.cluster_id, general.LevelControl.cluster_id, ], "out_clusters": [], - } + "device_type": zha.DeviceType.ON_OFF_LIGHT, + }, }, ieee=IEEE_GROUPABLE_DEVICE2, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True + await hass.async_block_till_done() return zha_device @@ -191,9 +206,11 @@ async def async_set_speed(hass, entity_id, speed=None): await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True) -async def async_test_zha_group_fan_entity( - hass, device_fan_1, device_fan_2, coordinator -): +@patch( + "zigpy.zcl.clusters.hvac.Fan.write_attributes", + new=AsyncMock(return_value=zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]), +) +async def test_zha_group_fan_entity(hass, device_fan_1, device_fan_2, coordinator): """Test the fan entity for a ZHA group.""" zha_gateway = get_zha_gateway(hass) assert zha_gateway is not None @@ -202,19 +219,20 @@ async def async_test_zha_group_fan_entity( device_fan_1._zha_gateway = zha_gateway device_fan_2._zha_gateway = zha_gateway member_ieee_addresses = [device_fan_1.ieee, device_fan_2.ieee] + members = [GroupMember(device_fan_1.ieee, 1), GroupMember(device_fan_2.ieee, 1)] # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", member_ieee_addresses - ) + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) await hass.async_block_till_done() assert zha_group is not None assert len(zha_group.members) == 2 for member in zha_group.members: - assert member.ieee in member_ieee_addresses + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None - entity_domains = GROUP_PROBE.determine_entity_domains(zha_group) + entity_domains = GROUP_PROBE.determine_entity_domains(hass, zha_group) assert len(entity_domains) == 2 assert LIGHT_DOMAIN in entity_domains @@ -224,14 +242,17 @@ async def async_test_zha_group_fan_entity( assert hass.states.get(entity_id) is not None group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id] - dev1_fan_cluster = device_fan_1.endpoints[1].fan - dev2_fan_cluster = device_fan_2.endpoints[1].fan - # test that the lights were created and that they are unavailable + dev1_fan_cluster = device_fan_1.device.endpoints[1].fan + dev2_fan_cluster = device_fan_2.device.endpoints[1].fan + + await async_enable_traffic(hass, [device_fan_1, device_fan_2], enabled=False) + await hass.async_block_till_done() + # test that the fans were created and that they are unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_group.members) + await async_enable_traffic(hass, [device_fan_1, device_fan_2]) # test that the fan group entity was created and is off assert hass.states.get(entity_id).state == STATE_OFF @@ -239,37 +260,103 @@ async def async_test_zha_group_fan_entity( # turn on from HA group_fan_cluster.write_attributes.reset_mock() await async_turn_on(hass, entity_id) + await hass.async_block_till_done() assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 2}) - assert hass.states.get(entity_id).state == SPEED_MEDIUM + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 2} # turn off from HA group_fan_cluster.write_attributes.reset_mock() await async_turn_off(hass, entity_id) assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 0}) - assert hass.states.get(entity_id).state == STATE_OFF + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 0} # change speed from HA group_fan_cluster.write_attributes.reset_mock() await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 3}) - assert hass.states.get(entity_id).state == SPEED_HIGH + assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 3} # test some of the group logic to make sure we key off states correctly - await dev1_fan_cluster.async_set_speed(SPEED_OFF) - await dev2_fan_cluster.async_set_speed(SPEED_OFF) + await send_attributes_report(hass, dev1_fan_cluster, {0: 0}) + await send_attributes_report(hass, dev2_fan_cluster, {0: 0}) # test that group fan is off assert hass.states.get(entity_id).state == STATE_OFF - await dev1_fan_cluster.async_set_speed(SPEED_MEDIUM) + await send_attributes_report(hass, dev2_fan_cluster, {0: 2}) + await hass.async_block_till_done() # test that group fan is speed medium - assert hass.states.get(entity_id).state == SPEED_MEDIUM + assert hass.states.get(entity_id).state == STATE_ON - await dev1_fan_cluster.async_set_speed(SPEED_OFF) + await send_attributes_report(hass, dev2_fan_cluster, {0: 0}) + await hass.async_block_till_done() # test that group fan is now off assert hass.states.get(entity_id).state == STATE_OFF + + +@pytest.mark.parametrize( + "plug_read, expected_state, expected_speed", + ( + (None, STATE_OFF, None), + ({"fan_mode": 0}, STATE_OFF, SPEED_OFF), + ({"fan_mode": 1}, STATE_ON, SPEED_LOW), + ({"fan_mode": 2}, STATE_ON, SPEED_MEDIUM), + ({"fan_mode": 3}, STATE_ON, SPEED_HIGH), + ), +) +async def test_fan_init( + hass, + zha_device_joined_restored, + zigpy_device, + plug_read, + expected_state, + expected_speed, +): + """Test zha fan platform.""" + + cluster = zigpy_device.endpoints.get(1).fan + cluster.PLUGGED_ATTR_READS = plug_read + + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + assert hass.states.get(entity_id).state == expected_state + assert hass.states.get(entity_id).attributes[ATTR_SPEED] == expected_speed + + +async def test_fan_update_entity( + hass, + zha_device_joined_restored, + zigpy_device, +): + """Test zha fan platform.""" + + cluster = zigpy_device.endpoints.get(1).fan + cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} + + zha_device = await zha_device_joined_restored(zigpy_device) + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF + assert cluster.read_attributes.await_count == 1 + + await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_OFF + assert cluster.read_attributes.await_count == 2 + + cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(entity_id).attributes[ATTR_SPEED] == SPEED_LOW + assert cluster.read_attributes.await_count == 3 diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py new file mode 100644 index 0000000000..947bad37e0 --- /dev/null +++ b/tests/components/zha/test_number.py @@ -0,0 +1,130 @@ +"""Test zha analog output.""" +import pytest +import zigpy.profiles.zha +import zigpy.types +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f + +from homeassistant.components.number import DOMAIN +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import async_setup_component + +from .common import ( + async_enable_traffic, + async_test_rejoin, + find_entity_id, + send_attributes_report, +) + +from tests.async_mock import call, patch +from tests.common import mock_coro + + +@pytest.fixture +def zigpy_analog_output_device(zigpy_device_mock): + """Zigpy analog_output device.""" + + endpoints = { + 1: { + "device_type": zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, + "in_clusters": [general.AnalogOutput.cluster_id, general.Basic.cluster_id], + "out_clusters": [], + } + } + return zigpy_device_mock(endpoints) + + +async def test_number(hass, zha_device_joined_restored, zigpy_analog_output_device): + """Test zha number platform.""" + + cluster = zigpy_analog_output_device.endpoints.get(1).analog_output + cluster.PLUGGED_ATTR_READS = { + "present_value": 15.0, + "max_present_value": 100.0, + "min_present_value": 0.0, + "relinquish_default": 50.0, + "resolution": 1.0, + "description": "PWM1", + "engineering_units": 98, + "application_type": 4 * 0x10000, + } + zha_device = await zha_device_joined_restored(zigpy_analog_output_device) + # one for present_value and one for the rest configuration attributes + assert cluster.read_attributes.call_count == 2 + assert "max_present_value" in cluster.read_attributes.call_args[0][0] + assert "min_present_value" in cluster.read_attributes.call_args[0][0] + assert "relinquish_default" in cluster.read_attributes.call_args[0][0] + assert "resolution" in cluster.read_attributes.call_args[0][0] + assert "description" in cluster.read_attributes.call_args[0][0] + assert "engineering_units" in cluster.read_attributes.call_args[0][0] + assert "application_type" in cluster.read_attributes.call_args[0][0] + + entity_id = await find_entity_id(DOMAIN, zha_device, hass) + assert entity_id is not None + + await async_enable_traffic(hass, [zha_device], enabled=False) + # test that the number was created and that it is unavailable + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + # allow traffic to flow through the gateway and device + assert cluster.read_attributes.call_count == 2 + await async_enable_traffic(hass, [zha_device]) + await hass.async_block_till_done() + assert cluster.read_attributes.call_count == 4 + + # test that the state has changed from unavailable to 15.0 + assert hass.states.get(entity_id).state == "15.0" + + # test attributes + assert hass.states.get(entity_id).attributes.get("min") == 0.0 + assert hass.states.get(entity_id).attributes.get("max") == 100.0 + assert hass.states.get(entity_id).attributes.get("step") == 1.0 + assert hass.states.get(entity_id).attributes.get("icon") == "mdi:percent" + assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%" + assert ( + hass.states.get(entity_id).attributes.get("friendly_name") + == "FakeManufacturer FakeModel e769900a analog_output PWM1" + ) + + # change value from device + assert cluster.read_attributes.call_count == 4 + await send_attributes_report(hass, cluster, {0x0055: 15}) + assert hass.states.get(entity_id).state == "15.0" + + # update value from device + await send_attributes_report(hass, cluster, {0x0055: 20}) + assert hass.states.get(entity_id).state == "20.0" + + # change value from HA + with patch( + "zigpy.zcl.Cluster.write_attributes", + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), + ): + # set value via UI + await hass.services.async_call( + DOMAIN, "set_value", {"entity_id": entity_id, "value": 30.0}, blocking=True + ) + assert len(cluster.write_attributes.mock_calls) == 1 + assert cluster.write_attributes.call_args == call({"present_value": 30.0}) + cluster.PLUGGED_ATTR_READS["present_value"] = 30.0 + + # test rejoin + assert cluster.read_attributes.call_count == 4 + await async_test_rejoin(hass, zigpy_analog_output_device, [cluster], (1,)) + assert hass.states.get(entity_id).state == "30.0" + assert cluster.read_attributes.call_count == 6 + + # update device value with failed attribute report + cluster.PLUGGED_ATTR_READS["present_value"] = 40.0 + # validate the entity still contains old value + assert hass.states.get(entity_id).state == "30.0" + + await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() + + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == "40.0" + assert cluster.read_attributes.call_count == 7 + assert "present_value" in cluster.read_attributes.call_args[0][0] diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 80412d95fb..da3037f720 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -7,6 +7,7 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f from homeassistant.components.switch import DOMAIN +from homeassistant.components.zha.core.group import GroupMember from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( @@ -67,15 +68,16 @@ async def device_switch_1(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [general.OnOff.cluster_id], + "in_clusters": [general.OnOff.cluster_id, general.Groups.cluster_id], "out_clusters": [], - "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + "device_type": zha.DeviceType.ON_OFF_SWITCH, } }, ieee=IEEE_GROUPABLE_DEVICE, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True + await hass.async_block_till_done() return zha_device @@ -86,15 +88,16 @@ async def device_switch_2(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [general.OnOff.cluster_id], + "in_clusters": [general.OnOff.cluster_id, general.Groups.cluster_id], "out_clusters": [], - "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + "device_type": zha.DeviceType.ON_OFF_SWITCH, } }, ieee=IEEE_GROUPABLE_DEVICE2, ) zha_device = await zha_device_joined(zigpy_device) zha_device.available = True + await hass.async_block_till_done() return zha_device @@ -157,7 +160,7 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) -async def async_test_zha_group_switch_entity( +async def test_zha_group_switch_entity( hass, device_switch_1, device_switch_2, coordinator ): """Test the switch entity for a ZHA group.""" @@ -168,30 +171,38 @@ async def async_test_zha_group_switch_entity( device_switch_1._zha_gateway = zha_gateway device_switch_2._zha_gateway = zha_gateway member_ieee_addresses = [device_switch_1.ieee, device_switch_2.ieee] + members = [ + GroupMember(device_switch_1.ieee, 1), + GroupMember(device_switch_2.ieee, 1), + ] # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", member_ieee_addresses - ) + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) await hass.async_block_till_done() assert zha_group is not None assert len(zha_group.members) == 2 for member in zha_group.members: - assert member.ieee in member_ieee_addresses + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) assert hass.states.get(entity_id) is not None group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] - dev1_cluster_on_off = device_switch_1.endpoints[1].on_off - dev2_cluster_on_off = device_switch_2.endpoints[1].on_off + dev1_cluster_on_off = device_switch_1.device.endpoints[1].on_off + dev2_cluster_on_off = device_switch_2.device.endpoints[1].on_off - # test that the lights were created and that they are unavailable + await async_enable_traffic(hass, [device_switch_1, device_switch_2], enabled=False) + await hass.async_block_till_done() + + # test that the lights were created and that they are off assert hass.states.get(entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_group.members) + await async_enable_traffic(hass, [device_switch_1, device_switch_2]) + await hass.async_block_till_done() # test that the lights were created and are off assert hass.states.get(entity_id).state == STATE_OFF @@ -207,7 +218,7 @@ async def async_test_zha_group_switch_entity( ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( - False, ON, (), expect_reply=True, manufacturer=None, tsn=None + False, ON, (), expect_reply=True, manufacturer=None, tries=1, tsn=None ) assert hass.states.get(entity_id).state == STATE_ON @@ -222,28 +233,32 @@ async def async_test_zha_group_switch_entity( ) assert len(group_cluster_on_off.request.mock_calls) == 1 assert group_cluster_on_off.request.call_args == call( - False, OFF, (), expect_reply=True, manufacturer=None, tsn=None + False, OFF, (), expect_reply=True, manufacturer=None, tries=1, tsn=None ) assert hass.states.get(entity_id).state == STATE_OFF # test some of the group logic to make sure we key off states correctly - await dev1_cluster_on_off.on() - await dev2_cluster_on_off.on() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) + await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) + await hass.async_block_till_done() # test that group light is on assert hass.states.get(entity_id).state == STATE_ON - await dev1_cluster_on_off.off() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) + await hass.async_block_till_done() # test that group light is still on assert hass.states.get(entity_id).state == STATE_ON - await dev2_cluster_on_off.off() + await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) + await hass.async_block_till_done() # test that group light is now off assert hass.states.get(entity_id).state == STATE_OFF - await dev1_cluster_on_off.on() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) + await hass.async_block_till_done() # test that group light is now back on assert hass.states.get(entity_id).state == STATE_ON diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index 136af1f4be..1ea52d4e60 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -3612,6 +3612,8 @@ DEVICES = [ "sensor.digi_xbee3_77665544_analog_input_2", "sensor.digi_xbee3_77665544_analog_input_3", "sensor.digi_xbee3_77665544_analog_input_4", + "number.digi_xbee3_77665544_analog_output", + "number.digi_xbee3_77665544_analog_output_2", ], "entity_map": { ("switch", "00:11:22:33:44:55:66:77-208-6"): { @@ -3714,6 +3716,16 @@ DEVICES = [ "entity_class": "AnalogInput", "entity_id": "sensor.digi_xbee3_77665544_analog_input_5", }, + ("number", "00:11:22:33:44:55:66:77-218-13"): { + "channels": ["analog_output"], + "entity_class": "ZhaNumber", + "entity_id": "number.digi_xbee3_77665544_analog_output", + }, + ("number", "00:11:22:33:44:55:66:77-219-13"): { + "channels": ["analog_output"], + "entity_class": "ZhaNumber", + "entity_id": "number.digi_xbee3_77665544_analog_output_2", + }, }, "event_channels": ["232:0x0008"], "manufacturer": "Digi", diff --git a/tests/conftest.py b/tests/conftest.py index fa390f9bf3..d8fb9f2914 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -400,7 +400,7 @@ def mqtt_client_mock(hass): async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): """Fixture to mock MQTT component.""" if mqtt_config is None: - mqtt_config = {mqtt.CONF_BROKER: "mock-broker"} + mqtt_config = {mqtt.CONF_BROKER: "mock-broker", mqtt.CONF_BIRTH_MESSAGE: {}} result = await async_setup_component(hass, mqtt.DOMAIN, {mqtt.DOMAIN: mqtt_config}) assert result diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 9c2a1b1e37..beb7e42400 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -233,7 +233,7 @@ "profileMode": "AUTOMATIC", "secondaryCloseAdjustable": false, "secondaryOpenAdjustable": false, - "secondaryShadingLevel": null, + "secondaryShadingLevel": 0, "secondaryShadingStateType": "NOT_EXISTENT", "shadingDriveVersion": null, "shadingPackagePosition": "TOP", @@ -6116,6 +6116,508 @@ "serializedGlobalTradeItemNumber": "3014F0000000000000FAF9B4", "type": "TORMATIC_MODULE", "updateState": "UP_TO_DATE" + }, + "3014F7110000000000005521": { + "availableFirmwareVersion": "1.4.2", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.4.2", + "firmwareVersionInteger": 66562, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000005521", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000034" + ], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -82, + "rssiPeerValue": -78, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": true, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000005521", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "index": 1, + "label": "Poolpumpe", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000005521", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "index": 2, + "label": "Poollicht", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000005521", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "4": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000005521", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 4, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000005521", + "label": "Schaltaktor Verteiler", + "lastStatusUpdate": 1605271783993, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 405, + "modelType": "HmIP-DRSI4", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000005521", + "type": "DIN_RAIL_SWITCH_4", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000022311": { + "availableFirmwareVersion": "1.6.0", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.6.0", + "firmwareVersionInteger": 67072, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000022311", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000026" + ], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -70, + "rssiPeerValue": -63, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": true, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "blindModeActive": false, + "bottomToTopReferenceTime": 18.19999999999999, + "changeOverDelay": 0.5, + "delayCompensationValue": 0.0, + "deviceId": "3014F7110000000000022311", + "endpositionAutoDetectionEnabled": false, + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "MULTI_MODE_INPUT_BLIND_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000027" + ], + "index": 1, + "label": "Badezimmer ", + "multiModeInputMode": "KEY_BEHAVIOR", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.0, + "slatsLevel": null, + "slatsReferenceTime": 0.0, + "supportedOptionalFeatures": { + "IOptionalFeatureSlatsState": false + }, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 17.49999999999998, + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "blindModeActive": false, + "bottomToTopReferenceTime": 17.899999999999984, + "changeOverDelay": 0.5, + "delayCompensationValue": 0.0, + "deviceId": "3014F7110000000000022311", + "endpositionAutoDetectionEnabled": false, + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "MULTI_MODE_INPUT_BLIND_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000028" + ], + "index": 2, + "label": "Schlafzimmer ", + "multiModeInputMode": "KEY_BEHAVIOR", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.0, + "slatsLevel": null, + "slatsReferenceTime": 0.0, + "supportedOptionalFeatures": { + "IOptionalFeatureSlatsState": false + }, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 17.399999999999977, + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "blindModeActive": false, + "bottomToTopReferenceTime": 27.300000000000118, + "changeOverDelay": 0.5, + "delayCompensationValue": 0.0, + "deviceId": "3014F7110000000000022311", + "endpositionAutoDetectionEnabled": false, + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "MULTI_MODE_INPUT_BLIND_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000029" + ], + "index": 3, + "label": "Wohnzimmer T\u00fcr", + "multiModeInputMode": "KEY_BEHAVIOR", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.0, + "slatsLevel": null, + "slatsReferenceTime": 0.0, + "supportedOptionalFeatures": { + "IOptionalFeatureSlatsState": false + }, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 24.400000000000077, + "userDesiredProfileMode": "AUTOMATIC" + }, + "4": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "blindModeActive": false, + "bottomToTopReferenceTime": 25.900000000000098, + "changeOverDelay": 0.5, + "delayCompensationValue": 0.0, + "deviceId": "3014F7110000000000022311", + "endpositionAutoDetectionEnabled": false, + "favoritePrimaryShadingPosition": 0.5, + "favoriteSecondaryShadingPosition": 0.5, + "functionalChannelType": "MULTI_MODE_INPUT_BLIND_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000029" + ], + "index": 4, + "label": "Wohnzimmer Fenster", + "multiModeInputMode": "KEY_BEHAVIOR", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.0, + "slatsLevel": null, + "slatsReferenceTime": 0.0, + "supportedOptionalFeatures": { + "IOptionalFeatureSlatsState": false + }, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 25.000000000000085, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000022311", + "label": "Jalousieaktor 1 f\u00fcr Hutschienenmontage \u2013 4-fach", + "lastStatusUpdate": 1604414124509, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 406, + "modelType": "HmIP-DRBLI4", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000022311", + "type": "DIN_RAIL_BLIND_4", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000056775": { + "availableFirmwareVersion": "1.0.16", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.16", + "firmwareVersionInteger": 65552, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000056775", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000043" + ], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true + }, + "temperatureOutOfRange": false, + "unreach": null + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000044", + "00000000-0000-0000-0000-000000000045" + ], + "index": 1, + "label": "Licht Flur 1", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + }, + "2": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000044", + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000047" + ], + "index": 2, + "label": "Licht Flur 2", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + }, + "3": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000044" + ], + "index": 3, + "label": "Tür", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": true + }, + "windowState": "OPEN" + }, + "4": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000044" + ], + "index": 4, + "label": "Licht Flur 4", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + }, + "5": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 5, + "groups": [ + "00000000-0000-0000-0000-000000000044" + ], + "index": 5, + "label": "Licht Flur 5", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + }, + "6": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000056775", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 6, + "groups": [ + "00000000-0000-0000-0000-000000000044" + ], + "index": 6, + "label": "Licht Flur 6", + "multiModeInputMode": "KEY_BEHAVIOR", + "supportedOptionalFeatures": { + "IOptionalFeatureWindowState": false + }, + "windowState": null + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000056775", + "label": "Licht Flur", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 379, + "modelType": "HmIP-FCI6", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000056775", + "type": "FULL_FLUSH_CONTACT_INTERFACE_6", + "updateState": "UP_TO_DATE" } }, "groups": { diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index 157bbf3bc2..f2f2db37d7 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -3,6 +3,7 @@ import asyncio import logging import time +import aiohttp import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -546,3 +547,32 @@ async def test_implementation_provider(hass, local_impl): assert await config_entry_oauth2_flow.async_get_implementations( hass, mock_domain_with_impl ) == {TEST_DOMAIN: local_impl, "cloud": provider_source[mock_domain_with_impl]} + + +async def test_oauth_session_refresh_failure( + hass, flow_handler, local_impl, aioclient_mock +): + """Test the OAuth2 session helper when no refresh is needed.""" + flow_handler.async_register_implementation(hass, local_impl) + + aioclient_mock.post(TOKEN_URL, status=400) + + config_entry = MockConfigEntry( + domain=TEST_DOMAIN, + data={ + "auth_implementation": TEST_DOMAIN, + "token": { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + # Already expired, requires a refresh + "expires_in": -500, + "expires_at": time.time() - 500, + "token_type": "bearer", + "random_other_data": "should_stay", + }, + }, + ) + + session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl) + with pytest.raises(aiohttp.client_exceptions.ClientResponseError): + await session.async_request("post", "https://example.com") diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 1b9ddc5219..9c7ddb09f8 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -908,6 +908,33 @@ async def test_track_template_error_can_recover(hass, caplog): assert "UndefinedError" not in caplog.text +async def test_track_template_time_change(hass, caplog): + """Test tracking template with time change.""" + template_error = Template("{{ utcnow().minute % 2 == 0 }}", hass) + calls = [] + + @ha.callback + def error_callback(entity_id, old_state, new_state): + calls.append((entity_id, old_state, new_state)) + + start_time = dt_util.utcnow() + timedelta(hours=24) + time_that_will_not_match_right_away = start_time.replace(minute=1, second=0) + with patch( + "homeassistant.util.dt.utcnow", return_value=time_that_will_not_match_right_away + ): + async_track_template(hass, template_error, error_callback) + await hass.async_block_till_done() + assert not calls + + first_time = start_time.replace(minute=2, second=0) + with patch("homeassistant.util.dt.utcnow", return_value=first_time): + async_fire_time_changed(hass, first_time) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0] == (None, None, None) + + async def test_track_template_result(hass): """Test tracking template.""" specific_runs = [] diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py new file mode 100644 index 0000000000..5444cd4643 --- /dev/null +++ b/tests/helpers/test_httpx_client.py @@ -0,0 +1,143 @@ +"""Test the httpx client helper.""" + +import httpx +import pytest + +from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE +import homeassistant.helpers.httpx_client as client + +from tests.async_mock import Mock, patch + + +async def test_get_async_client_with_ssl(hass): + """Test init async client with ssl.""" + client.get_async_client(hass) + + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT], httpx.AsyncClient) + + +async def test_get_async_client_without_ssl(hass): + """Test init async client without ssl.""" + client.get_async_client(hass, verify_ssl=False) + + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT_NOVERIFY], httpx.AsyncClient) + + +async def test_create_async_httpx_client_with_ssl_and_cookies(hass): + """Test init async client with ssl and cookies.""" + client.get_async_client(hass) + + httpx_client = client.create_async_httpx_client(hass, cookies={"bla": True}) + assert isinstance(httpx_client, httpx.AsyncClient) + assert hass.data[client.DATA_ASYNC_CLIENT] != httpx_client + + +async def test_create_async_httpx_client_without_ssl_and_cookies(hass): + """Test init async client without ssl and cookies.""" + client.get_async_client(hass, verify_ssl=False) + + httpx_client = client.create_async_httpx_client( + hass, verify_ssl=False, cookies={"bla": True} + ) + assert isinstance(httpx_client, httpx.AsyncClient) + assert hass.data[client.DATA_ASYNC_CLIENT_NOVERIFY] != httpx_client + + +async def test_get_async_client_cleanup(hass): + """Test init async client with ssl.""" + client.get_async_client(hass) + + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT], httpx.AsyncClient) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + await hass.async_block_till_done() + + assert hass.data[client.DATA_ASYNC_CLIENT].is_closed + + +async def test_get_async_client_cleanup_without_ssl(hass): + """Test init async client without ssl.""" + client.get_async_client(hass, verify_ssl=False) + + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT_NOVERIFY], httpx.AsyncClient) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + await hass.async_block_till_done() + + assert hass.data[client.DATA_ASYNC_CLIENT_NOVERIFY].is_closed + + +async def test_get_async_client_patched_close(hass): + """Test closing the async client does not work.""" + + with patch("httpx.AsyncClient.aclose") as mock_aclose: + httpx_session = client.get_async_client(hass) + assert isinstance(hass.data[client.DATA_ASYNC_CLIENT], httpx.AsyncClient) + + with pytest.raises(RuntimeError): + await httpx_session.aclose() + + assert mock_aclose.call_count == 0 + + +async def test_warning_close_session_integration(hass, caplog): + """Test log warning message when closing the session from integration context.""" + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/homeassistant/components/hue/light.py", + lineno="23", + line="await session.aclose()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ): + httpx_session = client.get_async_client(hass) + await httpx_session.aclose() + + assert ( + "Detected integration that closes the Home Assistant httpx client. " + "Please report issue for hue using this method at " + "homeassistant/components/hue/light.py, line 23: await session.aclose()" + ) in caplog.text + + +async def test_warning_close_session_custom(hass, caplog): + """Test log warning message when closing the session from custom context.""" + with patch( + "homeassistant.helpers.frame.extract_stack", + return_value=[ + Mock( + filename="/home/paulus/homeassistant/core.py", + lineno="23", + line="do_something()", + ), + Mock( + filename="/home/paulus/config/custom_components/hue/light.py", + lineno="23", + line="await session.aclose()", + ), + Mock( + filename="/home/paulus/aiohue/lights.py", + lineno="2", + line="something()", + ), + ], + ): + httpx_session = client.get_async_client(hass) + await httpx_session.aclose() + assert ( + "Detected integration that closes the Home Assistant httpx client. " + "Please report issue to the custom component author for hue using this method at " + "custom_components/hue/light.py, line 23: await session.aclose()" in caplog.text + ) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 92666335f2..c81ed681d4 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -780,6 +780,32 @@ async def test_wait_template_variables_in(hass): assert not script_obj.is_running +async def test_wait_template_with_utcnow(hass): + """Test the wait template with utcnow.""" + sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ utcnow().hours == 12 }}"}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + start_time = dt_util.utcnow() + timedelta(hours=24) + + try: + hass.async_create_task(script_obj.async_run(context=Context())) + async_fire_time_changed(hass, start_time.replace(hour=5)) + assert not script_obj.is_running + async_fire_time_changed(hass, start_time.replace(hour=12)) + + await asyncio.wait_for(wait_started_flag.wait(), 1) + + assert script_obj.is_running + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + async_fire_time_changed(hass, start_time.replace(hour=3)) + await hass.async_block_till_done() + + assert not script_obj.is_running + + @pytest.mark.parametrize("mode", ["no_timeout", "timeout_finish", "timeout_not_finish"]) @pytest.mark.parametrize("action_type", ["template", "trigger"]) async def test_wait_variables_out(hass, mode, action_type): diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 59e1b0754c..0be89af481 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1539,6 +1539,51 @@ async def test_unique_id_ignore(hass, manager): assert entry.unique_id == "mock-unique-id" +async def test_manual_add_overrides_ignored_entry(hass, manager): + """Test that we can ignore manually add entry, overriding ignored entry.""" + hass.config.components.add("comp") + entry = MockConfigEntry( + domain="comp", + data={"additional": "data", "host": "0.0.0.0"}, + unique_id="mock-unique-id", + state=config_entries.ENTRY_STATE_LOADED, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + + mock_integration( + hass, + MockModule("comp"), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + await self.async_set_unique_id("mock-unique-id") + self._abort_if_unique_id_configured( + updates={"host": "1.1.1.1"}, reload_on_update=False + ) + return self.async_show_form(step_id="step2") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" + ) as async_reload: + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert entry.data["host"] == "1.1.1.1" + assert entry.data["additional"] == "data" + assert len(async_reload.mock_calls) == 0 + + async def test_unignore_step_form(hass, manager): """Test that we can ignore flows that are in progress and have a unique ID, then rediscover them.""" async_setup_entry = AsyncMock(return_value=True)