diff --git a/.coveragerc b/.coveragerc index 76d48720110..9f2fdc80716 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 56b181aa02a..a33cd59f227 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 9a69f0b444c..c96a990433a 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 218bb1132a3..00000000000 --- 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 6976e26ebb2..3d967b25c15 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 27614c3d49d..a660d930128 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 0de30d43c65..cf8323d2e81 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 49cee1ff280..a7ce097ae84 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 242a90088b3..ab09df87ae3 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 47bfd031c8e..1d758bc4398 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 b0f17918cd8..43d6ba21ca5 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 77ce53abef7..5df508d0f33 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 9ef9c74aa1a..9177b1deb7c 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 c215ec7dae9..27706c3d797 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 0df67a94182..95a51741222 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 aa54e582af0..3f6a142e281 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 00000000000..2b9eefdbb6b --- /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 d3e1db007f5..6725df44451 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 fa9ed6b467f..cbccc3a462d 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 4f61322b2c6..90058e254dc 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 e1b95b37e03..9c33637baa8 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 d7d7d6e9b34..ea954b9f0db 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 9291e17e865..fe0319764a7 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 5d4522e8ce1..aa24b5ff975 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 ebbceb69b0a..bed28b62975 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 40cf1ccc0b9..8e638205417 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 1c22344f551..86aaa213a15 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 e1a9306e004..7f3855a7b9c 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 78a0d22878a..50482cb3e61 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 2ac9aabfc91..c6e4fb3ba82 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 084965331e5..14260bd572d 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 16623e4e704..6a675c17248 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 00000000000..7ccc7c7360a --- /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 00000000000..f41ee93aefe --- /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 00000000000..f8879f5715f --- /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 db6c097e1e3..ed5fa26f0c0 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 861e483adb8..096d2c6e24d 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 00000000000..8fcd9c13425 --- /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 1e7d4d0f14e..2aeb94f66d2 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 78db52ade45..a02601759be 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 6f56d996b63..5d8abfc9f56 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 b5c6863a94d..8306b2daf70 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 e2a9646a0aa..0d8a0052406 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 00000000000..37e27fd8394 --- /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 eae6626685d..9d1cd4210f4 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 d4f8fc4bcc6..6ea40d0fd00 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 ce7cbc3f548..f1ef5ef665f 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 16de4fd1039..aa0ac965a84 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 8b3b1949ec3..58d6a4295e9 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 2aba1db84c3..95400de23b4 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 86d678d31af..8b35399bcb0 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 19768cad7db..743a68a010e 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 4fb6a0905cc..a0ed36a7169 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 0d46a0f7643..8cbfd138257 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 5ac31e130f9..98407155f17 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 6f3fd919df5..bf6d7a461ce 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 46eb3a91f04..dd24ee3066f 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 f0a657d33d3..b38568210ad 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 f13c212e25a..e36e6f86ec7 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 ae35beabf6b..6ebb22b565a 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 a047d1e7477..b1469af787e 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 71bea5a4d88..e1c89501394 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 00000000000..1b6e9caa24c --- /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 f5b95a57f06..1a57bfbadf9 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 e8deb533de8..4d60b158c4c 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 7c3467ca550..63012e23da1 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 138b84f6fda..abf4a9f62e4 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 f7830dbe18b..d6732cdddcf 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 913173f98cc..4bdc2959047 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 c00ee65c278..3f1b7ef816e 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 12395b1d078..d09fb725e34 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 00000000000..8d6cb9a2ebf --- /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 ee630bc7a1b..a43e80d3629 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 7d3a3994ace..cc5c604dc8c 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 574ba6b8ba7..c05d9641b9a 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 41ebfb340eb..41738c824fb 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 03606193945..1b0f03b8018 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 94dfbefb86a..44f49239642 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 a576b11e638..6312d4ecd18 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 3492518421d..fb9560832ca 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 88a4a0bdbb2..c39aa7637f8 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 00000000000..591d8c2feaa --- /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 56c8b5f718a..c67faa25f0b 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 51f0033b954..dab15def7b4 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 6adea1af5af..ffcaedeb5a0 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 14170fdd8cd..eca5e91ddeb 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 b7486af50e9..81bb79dc50b 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 00000000000..e9cd136720f --- /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 00000000000..ef392a5a668 --- /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 00000000000..464bad99d5a --- /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 00000000000..d03a77ca1c2 --- /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 00000000000..a55d37ed588 --- /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 00000000000..26c02fabbb4 --- /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 00000000000..7ed3306721c --- /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 00000000000..945f467c4cf --- /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 00000000000..a11488ebca9 --- /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 00000000000..88a7c986152 --- /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 00000000000..e8950d1c714 --- /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 00000000000..486ff0c51e4 --- /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 00000000000..e3f5804cebe --- /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 00000000000..997d60402ca --- /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 00000000000..0ddc466a6f7 --- /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 00000000000..bb1f8e025ca --- /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 00000000000..269e207e8a4 --- /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 05f56150169..92ad0e22663 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 fdeb639b12b..097e3d086d6 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 fd2cb2181ac..853b498a51e 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 2b96bdfa5b8..2ced7577fdf 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 96f135848e1..55478f765e2 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 d34bb36bc00..16752dd0071 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 164e87a9642..b616437aa21 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 c2c383468f6..67649b7edba 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 6a418ccdc93..ae314897e74 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 5560383b710..7daa90fad2c 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 00000000000..95312fe7943 --- /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 aad75b3bed0..336f6ac5f68 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 ea0f1baa067..c0252e045b2 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 83cd4232d42..00000000000 --- 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 22798b09689..00000000000 --- 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 f3b659ddccd..00000000000 --- 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 987b3679b3c..00000000000 --- 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 dab3491b356..00000000000 --- 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 8a418bfb7bd..00000000000 --- 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 06519a5c455..00000000000 --- 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 e00409ffa26..00000000000 --- 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 b23bf7e8970..00000000000 --- 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 77edbd49901..00000000000 --- 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 e46abc942c9..00000000000 --- 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 fc0ece086a7..00000000000 --- 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 832849a7060..00000000000 --- 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 11539723fab..00000000000 --- 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 0e83b893642..00000000000 --- 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 188f82beae9..00000000000 --- 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 50c92e0678a..00000000000 --- 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 ab6504519d4..00000000000 --- 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 657640c2beb..00000000000 --- 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 a5be62bfc13..00000000000 --- 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 3f1edaf4c7d..00000000000 --- 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 dfe3f85a38d..00000000000 --- 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 01003d0e9d0..00000000000 --- 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 566a9e43dc0..00000000000 --- 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 43ffe5960c7..98486a28b09 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 a637b68b0ba..ea99bbf0167 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 8b40a8edefc..11fe9ff88b3 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 d0309140903..4706350cdb3 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 b7cbb547b0f..8ba642263a4 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 32d738d838d..0ed34907b1a 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 07cc81cc0fd..1d7aaa7c74e 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 cd849b9f933..1c940ea7a35 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 bc649dcadf0..50ee7a7a2a9 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 50d5409ef8d..2af1f548447 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 dc8ff4ec511..36dbb530fdb 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 cedcdc27327..9d7fdda1006 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 b7fc26165a0..9c2be6fd04b 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 5d11c2e9a72..b84105745ac 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 d244c316483..5c77add3118 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 ec5ad6c53ca..f5116110a09 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 e2be7cf4097..0b99005c382 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 188effb27af..76c420a584c 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 5736c91714c..3d05dc82abc 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 5f38c420ff1..265ec01b6db 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 c72d1ce40fe..e9f6a0d7f6f 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 31ef2dacf3a..cad5426d548 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 00000000000..a6081d5ccc1 --- /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 d1a44b5e5c9..65dc7fde595 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 fa732b64e77..7f069e741b8 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 d30f1702ae8..0d281e78f14 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 cb17459e105..5bce904e1cd 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 9cf2bca2df5..3fd40f3801c 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 4668b1da6eb..480aac34eb3 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 00000000000..c0c45b814a4 --- /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 00000000000..f194c8a3444 --- /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 cbb42aee925..af652c54509 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 fc725b11adf..0c960a850e2 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 9d37ff831d2..5e5f1367f58 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 eafe98f1541..53dc9ead653 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 e34db6d7262..f915040635f 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 8c72e9d92f8..d64fedecc5f 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 bf246b55b36..45fb03ad040 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 8781b90c3d8..2e0864c9f72 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 0e534147cb1..9bb9ba00261 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 4c07d1a2997..72bd052cc1d 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 5e4c740d66f..f9c19c6be38 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 79dc4c81b2a..d8208e6ce4e 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 0cce690257d..e217787ba19 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 39be96b84d5..5fd61c0bfed 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 d650d6596f7..0c54aecdd88 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 1f27531f769..3eb7feec614 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 f681da4210f..5461f207375 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 2bf2dd68b44..8eaa5185eb9 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 00000000000..94acde2d0a3 --- /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 7ada76c1d21..3fefe08f98b 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 ae182c62dc6..ec35a448407 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 fd2f08c1488..0493a964cc4 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 b4598d64087..af6b0ce54ba 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 159f961c3a6..eebc9bd5fc3 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 00000000000..e328e4f580b --- /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 07463bc8a15..c53ffd83279 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 3a795b60420..8072e06c2e5 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 b76dbcaf20b..e68800efb44 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 91a0dc60be7..90c98e491df 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 af42481b251..9f00493666a 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 972903e53e6..b34daaa6d17 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 1bb74053ea4..7abbefe85ff 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 2a2d383f362..155a39e49b6 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 4b5891359b6..2ac0bc40252 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 3075f6a3f9d..a4d8b84b1ad 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 0a41f8e2a8f..6e0e78839c1 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 07e7748e133..19f8f40b9d5 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 5dfc087c7bb..a2bea167b5e 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 320ca70b810..fbe13abc41e 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 585811f0eb4..63779e7fa94 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 00000000000..0acb1e6a9a6 --- /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 68b18568156..809dad5da46 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 00000000000..158cd3f3f74 --- /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 00000000000..b7c7b438804 --- /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 e84966b8d53..1be70def034 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 1653a11c3ed..f9a5783cd91 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 f52e877a9d4..bc955f119e9 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 19a57c3180e..908dfaa448c 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 ce7cbc3f548..f13cad90edc 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 03f9cb3cfbc..42278561d58 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 00000000000..e03867478c4 --- /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 7d68d78641e..679d9360a82 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 7c5675dad31..04b25ad7cb8 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 a69871a1ef6..ebf31967cc7 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 b39d7f27c55..5e0e1b5761a 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 1d9ede292f6..bbac113eb44 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 617aed245e0..dd9b538ae8b 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 1949bd98b26..b1a19792a08 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 507b48da9db..fec7b82e365 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 3e1e1748737..98e3864e191 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 ce6d6df8906..725ce07a1b6 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 f6695fc2af9..335aa73a67c 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 a8b9cb0ac4d..5a6ce5f5c64 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 31085292fbb..44cbd69bcd2 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 b5990dede21..73fe0f2152d 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 34a23569b96..4a00952aaa5 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 fab16780a57..1aaa5b04072 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 99c0652d982..a28c8bdbbb8 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 6bb5ae14603..87042b6500e 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 e80c32afd01..b0e44bedac4 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 96d52b57e85..e5ee9029302 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 67803ec56be..3f51a9c0884 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 ea46ea44846..3a7d26435ff 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 b8a454fbaba..ca6b9a6542c 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 3b5744ba1ae..fadb459a3d3 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 af953a1caaa..8af2ff4345a 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 f0b6c3eade4..989db1c2564 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 09ab0e6711c..56c91431f13 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 ab790dafe9b..4584a383136 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 96a09567650..7880adf5fff 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 9be7ac31e60..e19ff18b364 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 11f83d80179..b7fd193afad 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 3f200f4109e..ceb6c92004c 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 a4b3bd2fd86..bb1d109bb80 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 912deb7ffea..f0899598351 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 cc1877fb5bb..78cd317bb3e 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 364953d39d6..a85293e93a0 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 41edcd176da..ba31fa36fd2 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 00000000000..ce8a9287272 --- /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 00000000000..94c31d0e156 --- /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 e35c96a7bf7..cbbc3dc8f53 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 ce7cbc3f548..7fe3a6078c3 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 855cefaa774..ce7a1201223 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 d2c23f46093..a71c124c633 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 4d9fe2eba2a..ca685f36a13 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 5da4b6d7c09..73083d2b735 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 94396bbf883..6bb7dc1a870 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 38d6b4577b6..040744b27aa 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 20bba0ede4b..f6e4d5f5dc4 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 54cad2049fd..e9789c855d0 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 4d10424216e..74974604453 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 c4d1cc35cc1..0bf8113ccaa 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 e25b4cd7c8f..8f301b73b3e 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 2f61dbc37e0..48d278ac354 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 8c9b894c4b7..479685ff7e8 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 8c4ac5a0d73..ee877f78967 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 86b06148977..da6765368ae 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 8bb0486cd24..4baa6aaf047 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 775443d8f5f..eefa1fd2ddd 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 bc51c7f0bbc..6000b968e5e 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 b339013a69f..9e9760560d5 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 a2b50f20eb6..64b4fdf66ad 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 00000000000..c03615a39ff --- /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 00000000000..352e98916f1 --- /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 00000000000..aafc2e2b303 --- /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 a12754a87f4..fcfb4cf7ff1 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 5d831d7aaa2..d3501c496ef 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 e010af99a0b..6ff4d786447 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 a60ef4fdebf..4e719a7957f 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 f1b873d58a8..97d2b594cc3 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 00000000000..737fbc5ff53 --- /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 00000000000..63c887ff281 --- /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 5a4635e1ed8..af1ceba2c97 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 00000000000..c78c9a5aba5 --- /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 00000000000..e38e7f99169 --- /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 b69e8de8f7c..ed0ef205ff0 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 1e3d9138c84..c2bf0536ccf 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 6f398062876..38215675701 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 8bf65ef6ee6..cad7d736a9d 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 4a071063d47..c2bf0536ccf 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 e7dc1f6cd27..cd2934170c9 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 c7081cd694a..219446a038d 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 8d3dfe38d4d..e9b298e14ef 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 88e8628848c..0ac0bac013b 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 09e13bc2007..7eacd09c9d9 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 608c5bbcba7..734498585f3 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 a1924296f75..45b73cf58ee 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 a5b5cd26dc2..9d2eadee61c 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 b09eac77619..7b85df577ef 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 e06d3b881f7..4879842ee22 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 3eeac8bd8dd..d2fe23a8112 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 caf309e6718..241b07fd591 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 b3b468ff3ec..2d9b2f9e9c5 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 98180e11248..250400b6e22 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 4e202834624..11e5023bae9 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 0bef632c5a8..4ffe6730453 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 00000000000..d252c078a2c --- /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 98180e11248..88f4021b4ab 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 ce80aa8786a..ef2fec9f84f 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 00000000000..391a8c1affe --- /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 0f1e0b522e1..8150e309b20 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 9a552502afa..8dea1f5e013 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 abc49b1f5a0..86f05b8987e 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 6888266716c..011ddd0b6f7 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 163407ffce6..2d0906f73ab 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 f1d7d60315e..26bf8386d66 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 d80e3bcae1e..038cbdc20a3 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 1b35ab98993..8bc909e2bab 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 00000000000..47e36006adb --- /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 826cfc22d4d..68d6ee44b0b 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 f01728783cc..4bbc28bfedd 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 00000000000..72430b5e15c --- /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 4b8668e08a0..d72bc9bc015 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 491d400411c..69e4ce0c016 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 fb36312cf1e..4c534a90ae1 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 f7195cd0bff..0d8cc552dd2 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 0054edbdb0d..d81ca02f6ba 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 80db678f279..d79c03f0179 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 00000000000..ce945ba68d2 --- /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 b033c16afb6..5c25a8cb98c 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 48664bff395..00633422939 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 1658fcec1f5..69b276fc75f 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 8602afcabfe..47e4e6e3831 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 324a92cd8b5..9c5448266e6 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 96f2401e8d7..92b56a4804e 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 44adaf970b8..3fbf4a21fb3 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 724903ef360..6a33e3341b0 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 95435bb3bd9..9c645062256 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 ad8f0f41ae7..9f3518bcf8d 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 00000000000..f4e9792a589 --- /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 00000000000..e25888655a9 --- /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 91a0dc60be7..90c98e491df 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 0c5c7760566..9b067d35f89 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 7407782bc3f..27770d690f0 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 0077ceddd46..91def9afb9d 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 e40cef4f940..bf3a1606e6e 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 1ea5c9020e7..fa341509634 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 2a9c91681be..04374af8e82 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 4ab79dd603a..608a2150c61 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 ac804794b48..0cdb9318428 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 4119802eb77..216e8d391b6 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 937b6099bd9..385a0eedff2 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 981cb51c83a..fca08d49d7c 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 973601e744a..06083bae759 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 981cb51c83a..d368ac0fb3c 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 95b4a8a8a61..0d74360b8f3 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 7c5e1d87c9d..92ab6c1c8ff 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 ce7cbc3f548..a8931048295 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 95ddf7e51a7..fe3e8fb7b43 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 00000000000..7a4274b008c --- /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 56ad5128d97..e08a2c5f6df 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 6f398062876..94b8d6526d1 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 8bf65ef6ee6..cad7d736a9d 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 38f487a98a1..301bd1976e6 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 e1ee75297fd..8db8afa3a6b 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 10eb5dfd1e3..22ce4dba676 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 00000000000..a8d0d7ffbd3 --- /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 3d95664d0ab..e929ea2f919 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 00000000000..eb27f259531 --- /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 8f9931c6820..97e3d088af4 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 00000000000..45768a9f127 --- /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 00000000000..338a019019f --- /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 7bf340567f5..c16c2c3baa4 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 00000000000..1ff8ea1b3a9 --- /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 65b70a8463f..9d50e62fcd1 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 5270ac69704..3f12eca0f5f 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 ca41ff6758c..2733d6bd12d 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 00000000000..b5da3fdfc97 --- /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 e3e9247e7c4..0f1093f5b5b 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 d6231efe7ad..1142a476bc5 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 6d0f5f22d79..54e2e9f92a8 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 ed2d3c74d7d..042bc4771c1 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 deeb376e5e0..c4ab5c1e636 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 75c46125c14..6490904a32e 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 12bca8378c3..57aaa2b0b01 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 5c48de975f9..4fb21febb40 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 60d3867d05a..29a06c558fe 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 ce8b44f5702..a8df0107eeb 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 f387e7bfda3..1909ff818b9 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 30ca5165c85..9f045694460 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 72f9f94c210..9047ed9095f 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 d28fe17a691..7d24da73e77 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 21c896182fd..066ce89c2b2 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 fd574784838..b0cd7bb8b8d 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 451720ffb16..7da997f12d6 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 81a8804ee40..34d057a9ab0 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 f0ea5d28ffb..c8b067c887c 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 eef0cc82c15..8eabbbb08cc 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 9a5c0b2b54f..ffb2b3a0e50 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 00000000000..21aa58a9e64 --- /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 00000000000..325e9f2e6a0 --- /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 8b7889f0d12..ef5279e090a 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 85d167fb040..e78e05855c9 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 45e45ab85fb..cbd43a04cfd 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 a965fa38816..df1eb910d23 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 13dd977b7dc..05494d14869 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 aef74e530b1..11ab3289d14 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 9875f3bd918..2bb9ec241e5 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 5aa087c0515..e2989cb973b 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 d8c6a2c352e..5f5e8ea6221 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 180f266f1af..ca7ed238f0b 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 00000000000..50ac384d8ca --- /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 c5358988bac..52e3f0beb52 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 00000000000..95c0f1734cc --- /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 c4c4f512d6f..d1277b411e0 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 bb1ef3e2c03..db3aa75462a 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 e8d6232236b..a225b7f2c47 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 00000000000..8c1cb919d11 --- /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 00000000000..50ccd9f3b63 --- /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 00000000000..ff3170ffb98 --- /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 00000000000..d93018f8a3c --- /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 79c90379f18..e411982b58a 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 e705d115c8d..33b7c927520 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 00000000000..ac9710c6b9b --- /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 fda9ef4bb5b..9e74680a951 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 00000000000..6829f43011a --- /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 00000000000..6f46000e3e2 --- /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 fb9cbe3b7a8..ed003131bf2 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 24825307e76..3b466866334 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 3923f95f71b..aaf1800f748 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 2f9571b68fa..62e123eb84c 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 420196bb050..3e8e4cce2b8 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 3a6f1b64fa9..1c16db77faf 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 783cd16fefe..519c2e42764 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 cecd2aea7e7..e4d2c8f1488 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 eaed455b71a..030af8e090b 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 beef1c7070e..fe5b80f72f1 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 923fdbfb449..d2f73fca37b 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 5358cccb85b..dd69e0ec7c4 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 ea5e5afce21..aceb4ea78d5 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 2318a5eba05..806b5aebc62 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 62e2e1e59c5..9fa766c190b 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 467cc64f561..4dd8ddda76b 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 3f25486c6a4..a9ebd3c23ec 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 d42b858157b..8c0c5441a92 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 00000000000..488ad379942 --- /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 7e0da1fb844..cd5d576d0ad 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 9f522b086fc..7a18da03ddc 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 02353e5fca5..1f312c187cf 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 9fcb91c4627..f5d4446def5 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 00000000000..d252c078a2c --- /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 00000000000..25538aa0036 --- /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 b94faab25c0..ea7a2c4f9b2 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 d14dfa6c65a..99d11e5d6c9 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 b8a454fbaba..36962100519 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 43b43661b6d..9ab55c19a78 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 00000000000..7a4274b008c --- /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 f49de8669d1..363e62a1b5f 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 70d55a74b98..d1474c3cf5f 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 0c5c7760566..db82206819d 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 1d547e895bf..31230315954 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 d9f0f9c0d3a..50d067bf29a 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 c17667cbed2..b1d791e3284 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 ccfb08328fb..157c61cf243 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 f50a90c8a88..a0bf05cb5ec 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 a4aaf909344..11d962f9d15 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 972aed55cc4..64aaf6cbf4a 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 e4b38a6d109..604dc28b571 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 00000000000..dc21c371e60 --- /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 00000000000..d3f0e37a132 --- /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 00000000000..3fc69f85947 --- /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 00000000000..520df7ee4cd --- /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 00000000000..9e7bb472e0d --- /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 00000000000..4c984a55690 --- /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 00000000000..3d5be90042e --- /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 00000000000..0278fe07bfe --- /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 00000000000..b3d6b5d782e --- /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 00000000000..a8ee3fa57ac --- /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 00000000000..e25888655a9 --- /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 00000000000..85a42bf1be5 --- /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 00000000000..0108cb98d64 --- /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 00000000000..49fa9545e94 --- /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 00000000000..90c98e491df --- /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 faba23a52b9..72f11b7b005 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 7b4cedfebad..5d712045c93 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 8b0f4951bf9..e3eb92a426f 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 ae88441f89e..c5e407573ba 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 f4545817c9f..18342aa1d98 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 def025e0cf2..c6ef895b7df 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 cac13ee1653..ed211473e29 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 00000000000..1cc51f400da --- /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 ddf7e61a3f6..26b54def974 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 baa318f891f..d7d8acf4f29 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 1d6f7cb6df4..5891629627e 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 9c848bd8ec8..71370e40068 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 ed704711a66..154e82ec301 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 00000000000..c5a4032636c --- /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 d0238ff7436..b14abdd6b62 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 6ca0b0b1948..93575068121 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 65dc4ff8da7..8c2dcdb53ed 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 14ffab87116..94aa56bb63c 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 00000000000..38375801547 --- /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 f946a859ea2..d08fcdc7fe0 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 00000000000..c8680fcb7e5 --- /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 15c253dd4d4..b5a1552efc2 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 00000000000..ca8388f5bec --- /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 00000000000..9f763d0d6cc --- /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 a30b7b2518b..5807cdd7c16 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 1f8cb29ff0f..5811494fffb 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 12f0a2859e8..13f8c6bd800 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 00000000000..a04f550a71a --- /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 ab4e0832ed6..4e8df0d5e9f 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 2a193a19f96..614256fc701 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 19c41241a5e..508a652ce9b 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 b834cbc0aab..2c17d85f7b3 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 25623dc04a8..67f59434d46 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 6641658bd48..2cfc8af2d6d 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 152999b5574..3034135f847 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 4593a392ee3..f4d7c5dccfa 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 bfbaa828ea7..d642d3c6e0f 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 97c9b589c48..8de4e76c6f6 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 3c88914aafd..8e6b036202f 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 025d58f5197..f53975ecf00 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 0db5c5a422e..74c204b9683 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 00000000000..d974101d0a1 --- /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 77ce7025f70..72d275069c9 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 0675ede61bd..6c3049eff01 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 b8a454fbaba..4348cecf5c3 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 bb070f391a7..a36fd1ca13a 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 0a2f8461bed..493ceb4dfd1 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 c44f51b02e1..301075e0ad4 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 8ff6dfb982e..f5ba52b1a53 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 9d5bdcfa20a..17a20705cd4 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 bfef7be3f11..9ca512ca53a 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 777776ce42d..e0810147cda 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 ccc0fc7c477..d73c17a62cd 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 e653bda9205..b54a6783980 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 f7914177f8a..9e0f8ef51d6 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 72929e1ecb7..e10f1655d2f 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 fbee7d1b439..cb85b45e0e0 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 c80c8f881cd..27f2310c7ce 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 4273be3f435..3087401c3ae 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 e7e665d65f9..14dd36ce5b0 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 84cf711ac97..ce781266a6e 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 81d555806ed..dd637696e77 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 00000000000..f46cc94bd43 --- /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 d9c8a4099ac..d922923d472 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 00000000000..dd1acc230f1 --- /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 00000000000..fe188057e46 --- /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 00000000000..bb61b035201 --- /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 00000000000..545a3547ffc --- /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 00000000000..f8dac159488 --- /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 8c8d23b565b..37925ca6288 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 de7b8b8f0d7..5e9b4f8e690 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 69ab0a3421a..1273b720dd8 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 35d0e1fb42e..edf383a6819 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 339b41a9ddc..e081423d590 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 82e5cb8b272..e8783f74bd4 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 8b762a82f02..c5835f8e7c7 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 c3a78133246..25fcf0ad0d2 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 4fcfd8f66f2..c064cca599d 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 00000000000..03574e6554b --- /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 00000000000..4de2ae4fa6d --- /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 bcc969f0354..f871ac89c2d 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 676252c3134..9fa51bebf09 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 d1e64d44bbc..5452d15aa30 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 14469e415e0..96d5fe720c3 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 2375fb86e5a..393cb2fcf13 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 4796652f57e..00ad2671391 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 bba5605348b..bb10fd52ae7 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 faf987881b9..e6b22da5af0 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 aea1e40b0f9..712f2e0e376 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 4f4380332fd..673eb169b19 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 ffd34cef8c9..1fda8986ef7 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 24c1c6ff3a1..c61c30c922e 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 761f19ef054..76019680110 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 94356ccf778..75f3bb50309 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 2f0e67a9772..325e8dde098 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 586d96a78ac..53d6d391e8f 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 e508f2cb29e..63ceded5654 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 de92aee3171..bfb27361889 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 58ec51c4b1b..1c96b3de266 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 b954a97e8f9..f6265d1b96b 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 907e2e4a08f..65acc9afc71 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 9f75f38f1bc..5a8666e5a2e 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 4a071063d47..14f1703524c 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 9775dc592fd..1d9d3de4f89 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 00000000000..931d7cdb712 --- /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 4d7c4129d81..1698a1d944a 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 f74364bc8bc..449de72b158 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 53948e2b19d..248e455b6da 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 d36e3fa503f..d3ea8a8525c 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 efcbfb8d54c..b083ec1d7df 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 5d71d4889ac..21af0f91d17 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 a6aa19abe26..204adb108a8 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 601af3a68e9..f818135c51f 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 bc53bc93f7a..5d45710f4a6 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 c41d4e6d93a..94fcd3c4cb2 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 61b6ad44dfd..cc633979645 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 abe1d21c90a..b88a9d0cfa4 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 72ce208db3c..0c0aaa5f172 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 1d88e45b2ca..f2fd30f323e 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 989bf9ce137..100237c33e6 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 fcbc0361c24..a788c79ff5d 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 3b7054ea661..3177ed9d8e8 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 b4642359973..0672c9af33f 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 e4a30539673..30ea15c60c3 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 3ab2d0fd09d..96af3b0453f 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 2c5c8c61fef..beddee423a4 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 677bed1565b..ce4156244b7 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 97c9da5794b..1240d30f027 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 56d4ac31b78..d49ec8535cc 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 dfa365a36c3..f0e0b8e05fa 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 cec35eeca29..a643de0e6c9 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 6e457da039c..a74a50b0f36 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 e56d35c1dff..08cb0161bd9 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 6aaa5bcc489..36b0da239a9 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 199dcf425de..e5bd7ea1ca8 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 00000000000..218b01fd71b --- /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 00000000000..32c30f747d2 --- /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 48d9cb00783..cc9be9d7588 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 ee28a0905c3..cd0d66acba8 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 00000000000..664606b9edc --- /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 8be2693325e..f5fb286df7e 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 2df668513e1..34f525ca7a6 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 60293612cd3..7d60bb1cf5d 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 6245c5d83d0..0dcc89e2262 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 9009414c5b4..52490f41f86 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 f945469e26f..6ce529621aa 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 46558f2e89a..08d8cb97454 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 9ab94c993eb..843ce983305 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 3f55b19b255..2bc328ff8f6 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 739d77c8268..6693c2e5614 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 da5d717cb37..4c0b8b2617c 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 2e58ddeeddf..7d22dfd96bf 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 d9a216305e8..47334c4aa62 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 00e949b652b..958eaea039a 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 bd72b659ae7..931b8aa770e 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 69d67c5b4f2..dfaf33b3969 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 79c4276c23b..6da647ac29b 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 4060808c268..4f2e8952566 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 4af5404c63b..25660b4805e 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 797aac82405..5f14558e2b5 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 00000000000..484cdaff6ec --- /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 80d0f8ee66a..a271ca666f4 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 590082b826a..eab1d9741ad 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 9f18963dcb9..387dbe7b26c 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 f9199091a85..e39ecffa8a7 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 588675c670e..e396deabb68 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 4a071063d47..7953cf5625c 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 34450afc84b..0dc0931afe5 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 a7ad0fe1d27..8581b04099d 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 657ce03e544..093b7775829 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 5066f5a2edb..7b480bcc0f7 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 86e059df4ff..4b6597725ef 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 24825307e76..e92d51b2058 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 12bc209815f..865bd1dbd08 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 4a071063d47..7953cf5625c 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 eac51c4cf8f..d04a5b165b1 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 d8585ad7458..d0b55514a63 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 2fd04943e4d..31a0bcd7762 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 00000000000..611744e3191 --- /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 5edf0b18dd1..a856ef0aeed 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 b75bd37958f..7c65e836f9e 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 0f0bdcf4a1c..0f119e7c2ee 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 e012d68a10e..4d6e23022aa 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 f2450bf6f69..2c8402a7169 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 04cb13bf5e8..211f35d62ce 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 4ef73de7232..3fe3d33793f 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 827d8078b55..3b651ce82b0 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 d9a17545c4b..556e9b4136b 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 2665a5c5a85..7d0bce9ff1f 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 926e936a594..bc600f8428c 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 067234c7b54..c3abf6ceba3 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 2ebae1083eb..018f3870c58 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 cc7d8071c2c..f5f1bfd39ed 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 7858092db4f..26fd5f34117 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 6f398062876..38215675701 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 1127dc941ea..5189795ec9c 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 00000000000..3e10b977773 --- /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 99b5a46570e..c2c39e00d68 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 3b715eed0dd..09a3235377d 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 8b2702b6708..ae155ccf2c2 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 00000000000..bd1e14729e0 --- /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 acafb6eee4b..9c606534a5b 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 cfc92a512d4..c3662a032a6 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 6541d8accde..b21982fede8 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 3a94215a7b1..98fbf2d961a 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 49edf8e7d0a..06132e83e88 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 9e3c4d41229..a896b37a26b 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 237733e6870..8ec536e7331 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 59f14ab2ee5..8da530bebda 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 3ff1577c436..2c3e2f7071d 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 558f4adced8..066cee61c05 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 b2f8e272983..4a20aa651cd 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 960e3a9cf5c..85b6e617963 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 35099f2a59b..ea138287c78 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 c408b4bec33..6433111fe81 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 35232fe04db..239b47e2d3e 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 f736ec7e3cc..aabac4f4cfe 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 6f398062876..3bd083e4839 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 c4b70e90076..c4091388b35 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 cf9f264a618..daa12f9e569 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 5e0537b6633..97bf9fc498f 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 3fbf1797b31..7015a44b5f9 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 00000000000..f3784f6de87 --- /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 f557a83009c..43f456f7574 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 923fbab2440..db992a56305 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 dbc8db55d63..ebc78e1346b 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 b89f1472478..6c92b557797 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 6010da17b7b..4553589e36d 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 621e48bab7e..d479efdf95f 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 81a2390cc8b..70eaaaf18df 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 60d64f9afbf..f06c2896bc8 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 cbf9bfb6bd4..c4ea835d86c 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 9729938035d..e4c864d9fd3 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 e03c71ae709..ff3e0a711c5 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 966e1a4065b..89563ff3533 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 a143163ca1b..c9fd17c59bd 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 00000000000..75d85097874 --- /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 b2f5ebd6e8e..07dc84eae07 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 c4d67feb5bf..8da77910c38 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 00000000000..d0a70d57752 --- /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 f4334e1d632..f9ed5469da0 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 cac04acb87a..4b2c14be9d6 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 7efa1e31765..5c0105a762e 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 1e4f4cadc23..411d3a8610b 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 5ac554d2694..1b39556f451 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 584ce708d15..d0c0e9eccc8 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 451e8d6a3c2..7ccec14406a 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 f681da4210f..e56597a400d 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 33788960de3..258a75caa02 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 00000000000..e9890abba2f --- /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 dbfe2075e2d..aec745ea38b 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 4d765cc0508..24e37216b70 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 c5368b675b3..ddd6ef4cdb2 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 81b70bcd082..3b63ab169e2 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 e2e01eb1df7..2282e3584fc 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 00000000000..1dcdb7fe5af --- /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 808e3f3f7ea..dd40927a7c7 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 00000000000..d25f1975cf7 --- /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 f55df964f86..accee16a6f5 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 41b46ae5cb8..ad95609bd9f 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 05a8f96bda7..19f7e265438 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 1e224e5ac51..8ee83eab727 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 59dff606f8f..d0d0b9114fb 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 1ccfdddc1d1..401e10c256a 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 2fd65bb972e..3c928935cce 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 bbab02f959e..710d363f771 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 8fc660e8128..c7dfe6d02b2 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 3841a173df9..93a99ba1d31 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 0c5c7760566..c748619963b 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 8cfa8bdb46b..45edbf2d88e 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 00000000000..c299020ce9a --- /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 85797ab2082..c7d73c344d8 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 f772a8586d0..2e5bed4b668 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 82e6756df11..072d1ea0565 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 13a968114a5..815ee581e69 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 a8bdb2b9c43..a494b96abf1 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 3527a05e5b3..500c243b8c9 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 8ef9413edbf..24a1589db0d 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 71dd785b4ff..5dd638a717c 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 f6f93fada05..185b0e031c5 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 a8b6c3c6ccf..5956937ac2f 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 e2c38ad5d05..3c240d96131 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 ddfcbef493f..77bfa7bfdb1 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 fb3446fb652..32d33f19e80 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 00000000000..d252c078a2c --- /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 fad88a19b34..0b5af0ae432 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 c839cb75933..2f3e8cf4f1a 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 943e1b33199..721fb36fd36 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 9d7c3057939..c9de7eea7d4 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 480d53aa454..815a8601314 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 4c01137c496..626909b9b22 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 f8dbde46919..b800daee779 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 e6c1baf1ca6..1102078fc62 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 cefc44956c7..9b5829cf209 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 57bd346c91b..0600d73d8a1 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 f0d1527a0fb..8e208f57cc6 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 8012bdbb02b..4a6c9dbda6c 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 53304c93218..d66c2aae0e4 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 0cd251c737b..a350b9880fc 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 395fe5b9daa..33d3ddbfafe 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 28d73d189b8..e9deabec71b 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 5771c9da9a9..2fdeb991bfd 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 e1402d12e42..8bbc0de94b4 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 d52e7be1282..5c9a9157ba6 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 6c4932505ba..eb9f73fef99 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 57e7ea502f5..31e872a71cd 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 21e926ec9e1..c90c1aefee7 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 cae09d77621..480780db0a9 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 7ce887b05c2..75615c1cce7 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 c633c114b46..4501b25385e 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 5b37f7e3f9d..9481e954bde 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 ed7f5affc56..abf14268687 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 7f0f920b843..49c10354c51 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 bd35383e981..dd2e29616c7 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 3e4f97d5bc7..f15df428640 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 f048eaa3b47..85d79b6b331 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 6d934ed0e60..1979a10cb8a 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 964b143f1d5..4e04ab16ce2 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 33233840720..752136dac7f 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 ce8a9287272..c962097e6ad 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 00000000000..bdafd86c9a0 --- /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 827def7f302..3da2e5f5384 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 566568ce37c..b1561285ebf 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 4a071063d47..0918f2cca19 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 b9a66540c3b..5eb31bfa792 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 a2c942db57f..ad863f7ff79 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 5e57cd1464f..758c5c68bba 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 9509ace6546..c76871bcecd 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 6e494ce2692..f8e9034292c 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 39b48b91a84..682576b534a 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 6d9000b8669..55b533d4f1c 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 7880adf5fff..e67de509456 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 94e6d6cb489..cfa3a4aa3b4 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 794c67454fb..932e5cadd75 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 9e6453222a9..9918e38670a 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 acfb900218b..9067e2c6f53 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 00b152205f3..f34bce445f7 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 00000000000..9890048c368 --- /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 37dbb151d8e..05cdeb6852b 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 1b5c5cb760e..ae15ec058b5 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 00000000000..561c8d77287 --- /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 8bf65ef6ee6..cad7d736a9d 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 c083048e4cd..e3354267630 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 5ce246347c5..15e61d23627 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 e932e18a2b5..00b442399c1 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 b76995fe39f..e8b6fcfd2c3 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 196be985b6a..e3b78cd8e42 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 356e58f640b..d819bfd4bbd 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 7b4c1e71d57..869599296d5 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 ad96aeba53c..28a2d603dd0 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 a63efaf6dc2..b73a2e57f1a 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 d8305d10553..ce85d07d086 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 5a1d4f2f185..2294960d6f2 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 d04e5796542..4454bd940d4 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 565b9f6c0e8..dfae15e9686 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 298c7e111b2..147d9fb950d 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 53038352d4d..d53f089054a 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 e23e9561c77..b3dd7bb80fe 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 cd747466973..9f5c5b2efc7 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 dac21a6af74..74d0f831c8b 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 b02332eb0b0..d66cc0e5dd9 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 59ac0f5cccb..bf0150523b3 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 67f059c1cd3..ab05cf649d8 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 401c5a540d6..32802248856 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 a208b49150a..9b5df6cf934 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 1b97b800956..4d621d18fa6 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 0e77c8fbd7a..a609492f428 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 4996f9dea9c..37a10c69ec6 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 5e378369465..f1307e2a169 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 aba871acf6b..75c24278a8c 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 77f726a0dcd..4f41b5a1e56 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 6f398062876..38215675701 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 df467dd38b9..d232b491b68 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 2933743c867..7430480cc09 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 41bb76282b0..ca8c6f81eda 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 efab29fd698..9f2ed5a4b90 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 273daf6ef0a..1252313a438 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 38544eb2cec..4ccc36bcc2a 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 30951f88d0d..35952af999b 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 d3fe05bce10..ec9b5681e76 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 e66bf3b4043..31890269925 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 01078bbddfe..52bbd75e9bf 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 3f6e78fd3b4..3414e37a657 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 00000000000..5307276a71d --- /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 01c1db919cb..18cf04cf5a5 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 88cfc4a797f..a37c510a366 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 b8f53a74ff3..b97772a8d46 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 93ee4fc9b8f..3439684f977 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 00000000000..2d37a0b0dce --- /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 4b9fe3b564d..f9b64dc8483 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 16659304045..3dfb1649557 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 728e54b456f..2fc83ea71de 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 00000000000..00a2738f4fe --- /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 696412ac3c7..e7308558127 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 69450c4c4dc..ea84bf34586 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 00000000000..1becc929adc --- /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 d614776778e..14328953367 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 2bb48d39f29..57bc6e68436 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 00000000000..592ccd85589 --- /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 4768da884da..71390930e35 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 39f14020aec..88fe5330ee0 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 ce7cbc3f548..24a12833313 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 00000000000..db0e0c2a137 --- /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 b73aaade30c..ddb334d7545 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 b47280e3a9e..31a9d3ce950 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 703ac8614c4..0c0c184b5fe 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 00000000000..c299020ce9a --- /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 e9459bf08f5..e88b4ec3923 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 7730d8b34c4..78764ccf4e7 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 b285cafcfa9..32567e6d134 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 1b0c86468ea..c9a99f3c205 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 96b9ad519d8..ce15c28f47b 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 ef3f1224a4b..e4450e7a306 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 0210147a489..fffb248573d 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 b6a10d7cde5..bfd393bbbb8 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 eee2386a921..8e2ec3d36c0 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 b459d4e6bfd..0719e226cd6 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 0b138211240..a56222e22eb 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 c7307d119a0..c543f155e4d 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 00000000000..19a6909de48 --- /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 27656c260d3..670f5e66146 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 24087b50617..667bf6dbd12 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 85942f812b0..067374f6c10 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 00000000000..23fe89c73b4 --- /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 00000000000..f46e17923ad --- /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 00000000000..3e10b977773 --- /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 00000000000..1b08426f631 --- /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 f8cb25f7df5..87bf347795c 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 40313a41553..084811c8fa0 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 7f0213be4ef..165e0bfd98a 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 a31ea74fb0b..fbbc85c4a1e 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 47c01a0309f..303321ea94c 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 4264b1e4a08..9745f897e05 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 4f50be27955..d5e78faf91c 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 3b08a7afe14..9ea39b63888 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 1aa6bcdea75..00f193f8663 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 44a0f551ae0..228ac48bcb2 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 00000000000..279633b07b1 --- /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 3e0c79ad65e..423205f15b2 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 9fc7b198054..95c524b0433 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 d91896a4e12..03900fdeeb5 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 56be5eb0123..4e8f69b17c8 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 4a071063d47..7953cf5625c 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 59e2d80c561..9126e0e4ea4 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 1a99db5c24c..3fcdb6426fb 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 321dce9a296..6c385181aaa 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 7d78491ad14..30b9a2066cd 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 f06d815e5c5..463b1c65a98 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 362149e9fca..bdcc00dc764 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 00000000000..c76efd0e898 --- /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 00000000000..3df19f11fa9 --- /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 1431fc8e1b7..477eb0ffa9c 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 fc592c9e5c0..b6ca7881615 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 29f6ade8af4..80d9b50932e 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 8bdeef25118..b617826411d 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 a4e0adc81a8..5e2b06564dd 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 768f114ac4f..a1f6f595a04 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 95bd22cbecf..649de0f86e4 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 6030c972448..cde0a2ad9c7 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 9feea6d6288..ec945015278 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 67ad3cc5e59..09100c355c2 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 0df67a94182..c249c325adc 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 cd8edbc3e56..670f57df8ba 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 23f4662a4c1..941089ee0cb 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 31529d69a2d..26c57268689 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 e266cf06266..bfafaa77b42 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 4f4dd8a0956..d9060a719d8 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 e5a72f35e2b..a64a64ab74e 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 9ecaef216f9..e4aaaa39138 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 1883a5ab055..3a015b5ad6c 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 daf5ff0ec18..46f6f6cf162 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 d399370847c..3c17682089a 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 e88d982b8a1..2fac2ac142d 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 00000000000..3d0630027a8 --- /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 71d22d66cb0..2204e7c3323 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 57f58f05993..5c6bf76a169 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 e4cf0e97879..a92f8d4dbd6 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 21b232b757f..9a48c1bc525 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 a68c7635501..c3d4131d995 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 fc75254a9e2..5329ceb31ec 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 d891eea20a8..908cf287eeb 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 99cf4be4fff..1dda4ea6df7 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 07e72a29609..4cdcdfced79 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 75c84a5337e..46756b18cb8 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 3107b919e5a..cd8da781870 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 52f502b546f..967b38cdb82 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 e25dcb40d42..9ef1c325d1e 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 7c90b937329..b128be67087 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 1277d8fca89..639f5834922 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 38f054ae4c4..d0c1a3ca188 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 ba4810c3f96..a24c1dbe265 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 b8a454fbaba..566746538c0 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 31e2791c9f4..b98c6c8e9cd 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 4879603af6c..b07ad70adac 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 d2af1633f99..5a4de08033c 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 a5f4ff11f09..ff3887c840d 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 f2fb4c0926c..08871c3108e 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 2ae8c2863af..27ba9bb29c7 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 00000000000..451ff82e74b --- /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 a5a1d76bfbb..997757d2bc6 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 630afb02973..0776d7cb0e5 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 00000000000..2b4c70a0bad --- /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 00000000000..861ee57283c --- /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 00000000000..abed97c8933 --- /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 00000000000..14365f988bd --- /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 a325d458acb..7e6a113e1e0 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 354870a0d51..7a0a8e1a1fd 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 b17ad0b5511..d87f8cf51e0 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 34295b718ca..e280388eeca 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 0c5c7760566..ae100e45845 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 e4809c9b63a..b121c005fa7 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 ffdd1e0dd58..76bbc705690 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 00000000000..a2f32087684 --- /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 008b007e2fe..64423efed3e 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 9a4ed9e7782..6b25ec7d123 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 d9013bea391..c6c872c85e6 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 00000000000..94b13c6bc7d --- /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 28469cd1d93..f9bbe99d9ce 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 d9de9b9d558..68f762a54fc 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 96950484588..4afb098caa6 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 7293ce97611..b8d7031ee12 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 8cd8b0672cf..2348d42a0d3 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 239396b2d0c..fff58433a9c 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 73b27ee7d2a..a69e1fb95d8 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 00000000000..89dcfa396aa --- /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 28efb64c71e..228c8c6c176 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 13c29364975..6260f4a9ffc 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 437e45ba72a..ac7c8f40e8d 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 5cf1a0dcd00..fb4e459281c 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 02cffeefc44..264ad237af1 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 ce7cbc3f548..9f7a5918551 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 abbc12e6d8f..b266e25b39c 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 61c9ca54854..4c06c89692a 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 f2b24b2c553..ddb68ec09fa 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 c5e0b6386b8..de00cf0fce6 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 b1a4f0d7b36..b8259aca07f 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 74d6a858d84..257ed829b6a 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 00000000000..ef455299de6 --- /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 48f3ad6d172..f5573973728 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 00000000000..2278e7701aa --- /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 00000000000..3010c9e622b --- /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 c656926ecff..75ca322b9a3 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 b5ef3dc528b..b6690ed6d28 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 00000000000..e7c0712272c --- /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 1bc477277c9..0dca71a0d8d 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 6aac2be6dda..1362c7d483c 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 e986913fc70..dc04926004a 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 fc00d4ea8b5..50926e07a11 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 00000000000..7a4274b008c --- /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 0b9966135b1..4be83508478 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 0b323a55dc9..0077ceddd46 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 ae2956cc5e9..ea02e179337 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 8859e831d57..0a86501c8f6 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 5fc7e1050ae..2dd7407ad92 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 b80d6630c35..1fe7083ecfd 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 394a42c5fd6..cd917f42b47 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 a6e5cd46cbb..313c9057da0 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 37c74d07f51..0073bb22484 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 1bfae6cb900..39cd7127402 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 7953cf5625c..308f60400aa 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 13eb90b55db..2a0dbc2544e 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 70d66053209..4fb25c766cc 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 49ccd378c1d..4736fc91bf0 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 00000000000..38f070ab3db --- /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 477bd13374c..07fc710408f 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 a1340f587c9..a800e4d57c6 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 cd1059436d1..582aea354c6 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 5c127b797e7..65edf2dbe31 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 fd77eb4df82..95499fb7b82 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 2a9ae1187b2..ab76d14a69a 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 4ede7753310..8bab9bd19b1 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 e4a8cc8062f..6d350188989 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 b19ebdb40f8..d9bf3c123b4 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 68300adbcfe..fdf4b98faf8 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 753ac2a2441..654eec820c3 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 28597b3859e..6e3d70b0815 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 2ab4bc127c4..89f60faf84e 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 4f9b559bc19..54b70d78673 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 fdbf8971238..dfc337fc844 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 00000000000..e25888655a9 --- /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 91a0dc60be7..90c98e491df 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 ba95a0e4bc6..48f35e035f0 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 c6019c10843..2dbd1629487 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 0760427d46b..0326f18ac69 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 dc06d01e596..d105572c182 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 03812be0548..5b3a4778fcd 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 ac832aacc61..1647c5ce52d 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 16223582c33..c8827e20e01 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 9a357d76eb7..600493e8a12 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 e37987bc821..7c600d98401 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 87c22b160f4..32e4902799e 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 12d928e172a..1d3f767353b 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 05a12bc2284..e071a523321 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 812ac168d48..c57c7269723 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 e2b4056cfaa..4dcccc98c05 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 9983967f764..b25d1c1aa39 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 6b3a39d0926..32b8a064054 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 bcaa4038de1..54fceda03a6 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 00000000000..b4772e51742 --- /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 a594ffe3545..1dd51cd7e62 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 e62d61ac8ea..65825074313 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 b3a87510e5a..039513f100e 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 50a9b5fedbf..b40ea7917f8 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 00000000000..f8fa0efe967 --- /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 49be02c195c..74942081884 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 fdb263fd5f7..f5c07a9efc9 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 af82db0ffbb..601ce1efbfe 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 ac4f07a7086..4baa7e7bc3a 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 9eeaf6fccca..6b657f600d8 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 833f11190b6..9e204e91da5 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 6efa44e304f..49527666f53 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 526a774cc39..c4d7de3839e 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 cc8f9a17827..6e8c09bbd60 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 ddd1847f6a8..7b38c102253 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 4582fc5f3b6..44f5c9c56f7 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 661e1a11b56..f06ac8aca3f 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 00000000000..0f1719b388d --- /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 48a662e3a81..77c842a27fe 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 bed0d2b8d17..279bc0f686f 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 6dabfdf0447..ba29ff4a8da 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 7aa59bd5836..11e7dd89918 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 b3af06ad070..cebfd95591f 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 ece1877ea75..cbe339fd835 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 f1426cebca8..8e3b891c2e7 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 8ec5a611f1d..e4553a2498b 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 bd9819319f5..466c072f4a9 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 a8b10eb4501..e479f5e9ac1 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 f627346c67b..130fd2cc245 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 389e380af85..7500483ec53 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 d5d133d4a3a..c9acb4d82d7 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 ebd284eaa01..568f77d622a 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 66303ad96b3..ce07f5ab615 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 d4aae9a94fc..185f6024886 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 29828bddc17..197864b807c 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 fca2761f2f3..24a98cbf155 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 d7d45bbd7e3..46dc5510b18 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 28f2aca4fbb..cb0ccf268f7 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 45b98d7c27c..abc53294bbc 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 d9c1a5a40cd..bc007fefb84 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 6b48c313fcc..45991375ba0 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 c5212855038..00000000000 --- 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 2dba3c3c11d..00000000000 --- 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 00000000000..e1243fe2c0a --- /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 00000000000..ae32feec7b1 --- /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 0e9f6c13e3d..4f75e93faef 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 e54a5dcde01..ce761952921 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 ad99f13e8f6..060188d65aa 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 d2cec93df95..d57828fdfa4 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 039002ca7a7..9ae49419bf4 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 ceccc7d8c39..76a9a5bb070 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 77105dc73db..c4e4c91087c 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 0aa390223ca..1c3b4b0d672 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 00000000000..c58b8b12b53 --- /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 27e62fafc73..ebe34c2aa95 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 9f3946e6ade..534168fa78e 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 00000000000..89a8b224f1a --- /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 c8a9cd6d50f..326990e12c6 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 60dc293c4fd..59d65977066 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 e371fa6fe25..acb45bca85f 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 c1a956f3f4d..d05e36ed0eb 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 fe7b0c7783f..a49effdb75d 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 38156354cda..d3f852d7a49 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 420977cd40c..c3922666665 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 7ef0e3d6703..a35576ed353 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 31e62a1a719..0e69a67cdbf 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 8ab62019c3d..b4dbd0d140e 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 034ca33aece..f2b3dfba32c 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 a2febcca2a5..31a6c49eeb3 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 807a3829e7b..481b7957849 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 5366f6e14d1..4636a9ad59c 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 b57d8c97617..db8eb6b2456 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 3aa70aa48b3..6c1d9312f91 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 faa3e7115b8..4a25026959c 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 4d049753f43..0a9c1dc6104 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 00000000000..4ee6986e599 --- /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 86b905f2b0f..82a88de918b 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 31c2cddd09d..6954eb1b7af 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 182ef98e529..00000000000 --- 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 4a018305bcf..69b413ba51a 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 bf6716ec966..886b67f8e2a 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 cd3a06a5afa..65a37563911 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 6573b17980e..aad5621935e 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 89dccf6c31e..b7c75862153 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 12314f60561..7295d134087 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 00000000000..f85fcdaa749 --- /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 00000000000..cb17f81d18a --- /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 491b9bd9e07..ecc37bbe244 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 6037bde5afd..58e090db20f 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 00000000000..654f87cbceb --- /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 bc8cf1defc0..be740b13fb5 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 a09808316c4..aee84f9fe2b 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 751ef106147..ad9580f34ed 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 3a1f2eb9f7a..0c70ad3c9fc 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 a7be6ddcf6b..e0f4c6eda99 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 0febf142d02..8ef11335199 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 4e22591d3b0..8a03f13beda 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 bec87b72ee4..ca3fbe8be56 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 fcda7b0bb67..41c1f52b993 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 48d13a716ab..2638477ef79 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 71bcbedda88..16d3f8ba0ac 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 6ba045d60a6..04545a1a422 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 f2da007b5e0..4ab2991bd43 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 a3cda6afa69..16e4a434dc3 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 fe6a567e32e..2850be11450 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 d4ba26bd484..ec7ad592f15 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 00000000000..39dd068f5a6 --- /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 2ea2c0508ee..646e7741530 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 5cadc20218e..1aca8c84e07 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 e966188afd2..5f33aa2be4a 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 996d46e08a7..0d11ec2289c 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 00000000000..33bdcacd37d --- /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 00000000000..573de21c692 --- /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 00000000000..16a2f8b3f0d --- /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 00000000000..1bf6f0f3bef --- /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 00000000000..38055ba972c --- /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 00000000000..2af91c0fe32 --- /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 00000000000..1a36e5421ec --- /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 00000000000..45fdd01a643 --- /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 00000000000..05151d38be8 --- /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 8767953b363..6b79c552911 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 871f91d447c..77fdfb7d48a 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 afae1b661ab..e0c31d38bbb 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 709b9a0ff22..6fcc369182d 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 b12b9249373..65be13fd96c 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 00000000000..947bad37e01 --- /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 80412d95fb7..da3037f720d 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 136af1f4be9..1ea52d4e604 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 fa390f9bf3b..d8fb9f2914b 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 9c2a1b1e371..beb7e42400f 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 157bbf3bc23..f2f2db37d7f 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 1b9ddc52191..9c7ddb09f85 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 00000000000..5444cd4643d --- /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 92666335f28..c81ed681d42 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 59e1b0754c0..0be89af4810 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)