diff --git a/.coveragerc b/.coveragerc index 1d861d69c1d..ad001e56048 100644 --- a/.coveragerc +++ b/.coveragerc @@ -31,7 +31,6 @@ omit = homeassistant/components/amcrest/* homeassistant/components/ampio/* homeassistant/components/android_ip_webcam/* - homeassistant/components/androidtv/* homeassistant/components/anel_pwrctrl/switch.py homeassistant/components/anthemav/media_player.py homeassistant/components/apache_kafka/* @@ -51,6 +50,7 @@ omit = homeassistant/components/asterisk_cdr/mailbox.py homeassistant/components/asterisk_mbox/* homeassistant/components/asuswrt/device_tracker.py + homeassistant/components/atome/* homeassistant/components/august/* homeassistant/components/aurora_abb_powerone/sensor.py homeassistant/components/automatic/device_tracker.py @@ -58,6 +58,7 @@ omit = homeassistant/components/avion/light.py homeassistant/components/azure_event_hub/* homeassistant/components/baidu/tts.py + homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bbb_gpio/* homeassistant/components/bbox/device_tracker.py homeassistant/components/bbox/sensor.py @@ -93,6 +94,7 @@ omit = homeassistant/components/canary/camera.py homeassistant/components/cast/* homeassistant/components/cert_expiry/sensor.py + homeassistant/components/cert_expiry/helper.py homeassistant/components/channels/media_player.py homeassistant/components/cisco_ios/device_tracker.py homeassistant/components/cisco_mobility_express/device_tracker.py @@ -247,6 +249,7 @@ omit = homeassistant/components/greeneye_monitor/sensor.py homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py + homeassistant/components/growatt_server/sensor.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py homeassistant/components/gtt/sensor.py @@ -285,6 +288,10 @@ omit = homeassistant/components/hydrawise/* homeassistant/components/hyperion/light.py homeassistant/components/ialarm/alarm_control_panel.py + homeassistant/components/iaqualink/climate.py + homeassistant/components/iaqualink/light.py + homeassistant/components/iaqualink/sensor.py + homeassistant/components/iaqualink/switch.py homeassistant/components/icloud/device_tracker.py homeassistant/components/idteck_prox/* homeassistant/components/ifttt/* @@ -338,6 +345,7 @@ omit = homeassistant/components/limitlessled/light.py homeassistant/components/linksys_ap/device_tracker.py homeassistant/components/linksys_smart/device_tracker.py + homeassistant/components/linky/__init__.py homeassistant/components/linky/sensor.py homeassistant/components/linode/* homeassistant/components/linux_battery/sensor.py @@ -427,6 +435,7 @@ omit = homeassistant/components/nut/sensor.py homeassistant/components/nx584/alarm_control_panel.py homeassistant/components/nzbget/sensor.py + homeassistant/components/obihai/* homeassistant/components/octoprint/* homeassistant/components/oem/climate.py homeassistant/components/oasa_telematics/sensor.py @@ -467,8 +476,7 @@ omit = homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* - homeassistant/components/plex/media_player.py - homeassistant/components/plex/sensor.py + homeassistant/components/plex/* homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py @@ -563,6 +571,7 @@ omit = homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* homeassistant/components/slack/notify.py + homeassistant/components/slide/* homeassistant/components/sma/sensor.py homeassistant/components/smappee/* homeassistant/components/smarty/* @@ -572,6 +581,7 @@ omit = homeassistant/components/snmp/* homeassistant/components/sochain/sensor.py homeassistant/components/socialblade/sensor.py + homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py homeassistant/components/solax/sensor.py @@ -667,6 +677,7 @@ omit = homeassistant/components/ue_smart_radio/media_player.py homeassistant/components/upcloud/* homeassistant/components/upnp/* + homeassistant/components/upc_connect/* homeassistant/components/ups/sensor.py homeassistant/components/uptimerobot/binary_sensor.py homeassistant/components/uscis/sensor.py @@ -689,6 +700,8 @@ omit = homeassistant/components/vesync/const.py homeassistant/components/vesync/switch.py homeassistant/components/viaggiatreno/sensor.py + homeassistant/components/vicare/* + homeassistant/components/vivotek/camera.py homeassistant/components/vizio/media_player.py homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 22bd4384b23..e78a8e6851c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,35 +1,33 @@ // See https://aka.ms/vscode-remote/devcontainer.json for format details. { - "name": "Home Assistant Dev", - "context": "..", - "dockerFile": "../Dockerfile.dev", - "postCreateCommand": "pip3 install -e .", - "appPort": 8123, - "runArgs": [ - "-e", - "GIT_EDITOR=\"code --wait\"" - ], - "extensions": [ - "ms-python.python", - "ms-azure-devops.azure-pipelines", - "redhat.vscode-yaml" - ], - "settings": { - "python.pythonPath": "/usr/local/bin/python", - "python.linting.pylintEnabled": true, - "python.linting.enabled": true, - "python.formatting.provider": "black", - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true, - "terminal.integrated.shell.linux": "/bin/bash", - "yaml.customTags": [ - "!secret scalar", - "!include_dir_named scalar", - "!include_dir_list scalar", - "!include_dir_merge_list scalar", - "!include_dir_merge_named scalar" - ] - } -} \ No newline at end of file + "name": "Home Assistant Dev", + "context": "..", + "dockerFile": "../Dockerfile.dev", + "postCreateCommand": "mkdir -p config && pip3 install -e .", + "appPort": 8123, + "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], + "extensions": [ + "ms-python.python", + "ms-azure-devops.azure-pipelines", + "redhat.vscode-yaml", + "esbenp.prettier-vscode" + ], + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.shell.linux": "/bin/bash", + "yaml.customTags": [ + "!secret scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ] + } +} diff --git a/.gitignore b/.gitignore index 5389954ca59..15f0896975d 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,7 @@ nosetests.xml htmlcov/ test-reports/ test-results.xml +test-output.xml # Translations *.mo diff --git a/.travis.yml b/.travis.yml index 3447571a3e8..525a4c8e72c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,18 +16,14 @@ addons: matrix: fast_finish: true include: - - python: "3.6.0" + - python: "3.6.1" env: TOXENV=lint - dist: trusty - - python: "3.6.0" + - python: "3.6.1" env: TOXENV=pylint - dist: trusty - - python: "3.6.0" + - python: "3.6.1" env: TOXENV=typing - dist: trusty - - python: "3.6.0" + - python: "3.6.1" env: TOXENV=py36 - dist: trusty - python: "3.7" env: TOXENV=py37 diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e6f38920d7d..151868a1663 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,92 +1,105 @@ { - "version": "2.0.0", - "tasks": [ - { - "label": "Preview", - "type": "shell", - "command": "hass -c ./config", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pytest", - "type": "shell", - "command": "pytest --timeout=10 tests", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Flake8", - "type": "shell", - "command": "flake8 homeassistant tests", - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Pylint", - "type": "shell", - "command": "pylint homeassistant", - "dependsOn": [ - "Install all Requirements" - ], - "group": { - "kind": "test", - "isDefault": true, - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Generate Requirements", - "type": "shell", - "command": "./script/gen_requirements_all.py", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - }, - { - "label": "Install all Requirements", - "type": "shell", - "command": "pip3 install -r requirements_all.txt -c homeassistant/package_constraints.txt", - "group": { - "kind": "build", - "isDefault": true - }, - "presentation": { - "reveal": "always", - "panel": "new" - }, - "problemMatcher": [] - } - ] + "version": "2.0.0", + "tasks": [ + { + "label": "Preview", + "type": "shell", + "command": "hass -c ./config", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Pytest", + "type": "shell", + "command": "pytest --timeout=10 tests", + "dependsOn": ["Install all Test Requirements"], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Flake8", + "type": "shell", + "command": "flake8 homeassistant tests", + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Pylint", + "type": "shell", + "command": "pylint homeassistant", + "dependsOn": ["Install all Requirements"], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Generate Requirements", + "type": "shell", + "command": "./script/gen_requirements_all.py", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Install all Requirements", + "type": "shell", + "command": "pip3 install -r requirements_all.txt -c homeassistant/package_constraints.txt", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + }, + { + "label": "Install all Test Requirements", + "type": "shell", + "command": "pip3 install -r requirements_test_all.txt -c homeassistant/package_constraints.txt", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] + } + ] } diff --git a/CODEOWNERS b/CODEOWNERS index 81c5aafed30..1e45bcee4ec 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -28,6 +28,7 @@ homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff homeassistant/components/arest/* @fabaff homeassistant/components/asuswrt/* @kennedyshead +homeassistant/components/atome/* @baqs homeassistant/components/aurora_abb_powerone/* @davet2001 homeassistant/components/auth/* @home-assistant/core homeassistant/components/automatic/* @armills @@ -37,6 +38,7 @@ homeassistant/components/awair/* @danielsjf homeassistant/components/aws/* @awarecan @robbiet480 homeassistant/components/axis/* @kane610 homeassistant/components/azure_event_hub/* @eavanvalkenburg +homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot @@ -46,6 +48,7 @@ homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties +homeassistant/components/cert_expiry/* @cereal2nd homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl @@ -107,6 +110,7 @@ homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 homeassistant/components/gpsd/* @fabaff homeassistant/components/group/* @home-assistant/core +homeassistant/components/growatt_server/* @indykoning homeassistant/components/gtfs/* @robbiet480 homeassistant/components/harmony/* @ehendrix23 homeassistant/components/hassio/* @home-assistant/hass-io @@ -119,12 +123,14 @@ homeassistant/components/hive/* @Rendili @KJonline homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homematic/* @pvizeli @danielperna84 +homeassistant/components/homematicip_cloud/* @SukramJ homeassistant/components/honeywell/* @zxdavb homeassistant/components/html5/* @robbiet480 homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob +homeassistant/components/iaqualink/* @flz homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/incomfort/* @zxdavb homeassistant/components/influxdb/* @fabaff @@ -150,7 +156,7 @@ homeassistant/components/life360/* @pnbruckner homeassistant/components/lifx/* @amelchio homeassistant/components/lifx_cloud/* @amelchio homeassistant/components/lifx_legacy/* @amelchio -homeassistant/components/linky/* @tiste @Quentame +homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt homeassistant/components/logger/* @home-assistant/core @@ -188,7 +194,10 @@ homeassistant/components/no_ip/* @fabaff homeassistant/components/notify/* @home-assistant/core homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 -homeassistant/components/nuki/* @pschmitt +homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte +homeassistant/components/nuki/* @pvizeli +homeassistant/components/nws/* @MatthewFlamm +homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/onboarding/* @home-assistant/core homeassistant/components/opentherm_gw/* @mvn23 @@ -203,6 +212,7 @@ homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel +homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 @@ -232,6 +242,7 @@ homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/simplisafe/* @bachya +homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc homeassistant/components/smartthings/* @andrewsayre @@ -281,15 +292,18 @@ homeassistant/components/twentemilieu/* @frenck homeassistant/components/twilio_call/* @robbiet480 homeassistant/components/twilio_sms/* @robbiet480 homeassistant/components/unifi/* @kane610 +homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core homeassistant/components/upnp/* @robbiet480 homeassistant/components/uptimerobot/* @ludeeus +homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe +homeassistant/components/vicare/* @oischinger homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/waqi/* @andrey-git @@ -298,6 +312,7 @@ homeassistant/components/weather/* @fabaff homeassistant/components/weblink/* @home-assistant/core homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo +homeassistant/components/withings/* @vangorra homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya homeassistant/components/xfinity/* @cisasteelersfan diff --git a/Dockerfile.dev b/Dockerfile.dev index 00f5576bdbb..eb76fe5b16b 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -23,9 +23,10 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ WORKDIR /workspaces -# Install Python dependencies from requirements.txt if it exists -COPY requirements_test_all.txt homeassistant/package_constraints.txt /workspaces/ -RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt +# Install Python dependencies from requirements +COPY requirements_test.txt homeassistant/package_constraints.txt ./ +RUN pip3 install -r requirements_test.txt -c package_constraints.txt \ + && rm -f requirements_test.txt package_constraints.txt # Set the default shell to bash instead of sh ENV SHELL /bin/bash diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 0ee272f900d..558c0c39f66 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -113,7 +113,7 @@ stages: pip uninstall -y typing - script: | . venv/bin/activate - pytest --timeout=9 --durations=10 --junitxml=test-results.xml -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar tests script/check_dirty displayName: 'Run pytest for python $(python.container)' condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) @@ -121,22 +121,11 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 --junitxml=test-results.xml --cov --cov-report=xml -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - - task: PublishTestResults@2 - condition: succeededOrFailed() - inputs: - testResultsFiles: 'test-results.xml' - testRunTitle: 'Publish test results for Python $(python.container)' - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: cobertura - summaryFileLocation: coverage.xml - displayName: 'publish coverage artifact' - condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - stage: 'FullCheck' dependsOn: diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 7c88e615fa5..29e68a5d7ac 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -43,7 +43,7 @@ stages: release="$(Build.SourceBranchName)" created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')" - if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then + if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480|bramkragten)$ ]]; then exit 0 fi diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml new file mode 100644 index 00000000000..2fd49c056f7 --- /dev/null +++ b/azure-pipelines-translation.yml @@ -0,0 +1,66 @@ +# https://dev.azure.com/home-assistant + +trigger: + batch: true + branches: + include: + - dev +pr: none +schedules: + - cron: "30 0 * * *" + displayName: "translation update" + branches: + include: + - dev + always: true +variables: +- group: translation +resources: + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' + + +jobs: + +- job: 'Upload' + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: '3.7' + - script: | + export LOKALISE_TOKEN="$(lokaliseToken)" + export AZURE_BRANCH="$(Build.SourceBranchName)" + + ./script/translations_upload + displayName: 'Upload Translation' + +- job: 'Download' + dependsOn: + - 'Upload' + condition: or(eq(variables['Build.Reason'], 'Schedule'), eq(variables['Build.Reason'], 'Manual')) + pool: + vmImage: 'ubuntu-latest' + steps: + - task: UsePythonVersion@0 + displayName: 'Use Python 3.7' + inputs: + versionSpec: '3.7' + - template: templates/azp-step-git-init.yaml@azure + - script: | + export LOKALISE_TOKEN="$(lokaliseToken)" + export AZURE_BRANCH="$(Build.SourceBranchName)" + + ./script/translations_download + displayName: 'Download Translation' + - script: | + git checkout dev + git add homeassistant + git commit -am "[ci skip] Translation update" + git push + displayName: 'Update translation' diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index b1e6ff6a0a5..eec3f678981 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -10,7 +10,7 @@ trigger: - requirements_all.txt pr: none schedules: -- cron: '0 */8 * * *' +- cron: '0 */4 * * *' displayName: 'daily builds' branches: include: @@ -30,7 +30,8 @@ jobs: - template: templates/azp-job-wheels.yaml@azure parameters: builderVersion: '$(versionWheels)' - builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev' + builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev' + builderPip: 'Cython;numpy' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' preBuild: @@ -65,5 +66,6 @@ jobs: sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} + sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} done displayName: 'Prepare requirements files for Hass.io' diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 8ec2a8c2d3c..f7e24d69884 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -7,7 +7,7 @@ import platform import subprocess import sys import threading -from typing import List, Dict, Any, TYPE_CHECKING # noqa pylint: disable=unused-import +from typing import List, Dict, Any, TYPE_CHECKING from homeassistant import monkey_patch from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE @@ -168,7 +168,7 @@ def get_arguments() -> argparse.Namespace: parser.add_argument( "--runner", action="store_true", - help="On restart exit with code {}".format(RESTART_EXIT_CODE), + help=f"On restart exit with code {RESTART_EXIT_CODE}", ) parser.add_argument( "--script", nargs=argparse.REMAINDER, help="Run one of the embedded scripts" @@ -216,7 +216,7 @@ def check_pid(pid_file: str) -> None: try: with open(pid_file, "r") as file: pid = int(file.readline()) - except IOError: + except OSError: # PID File does not exist return @@ -239,8 +239,8 @@ def write_pid(pid_file: str) -> None: try: with open(pid_file, "w") as file: file.write(str(pid)) - except IOError: - print("Fatal Error: Unable to write pid file {}".format(pid_file)) + except OSError: + print(f"Fatal Error: Unable to write pid file {pid_file}") sys.exit(1) @@ -258,7 +258,7 @@ def closefds_osx(min_fd: int, max_fd: int) -> None: val = fcntl(_fd, F_GETFD) if not val & FD_CLOEXEC: fcntl(_fd, F_SETFD, val | FD_CLOEXEC) - except IOError: + except OSError: pass @@ -280,7 +280,7 @@ async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: hass = core.HomeAssistant() if args.demo_mode: - config = {"frontend": {}, "demo": {}} # type: Dict[str, Any] + config: Dict[str, Any] = {"frontend": {}, "demo": {}} bootstrap.async_from_config_dict( config, hass, @@ -326,7 +326,7 @@ def try_to_restart() -> None: thread.is_alive() and not thread.daemon for thread in threading.enumerate() ) if nthreads > 1: - sys.stderr.write("Found {} non-daemonic threads.\n".format(nthreads)) + sys.stderr.write(f"Found {nthreads} non-daemonic threads.\n") # Somehow we sometimes seem to trigger an assertion in the python threading # module. It seems we find threads that have no associated OS level thread diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 2641f0b8f7e..ee0d6c08441 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -47,7 +47,7 @@ async def auth_manager_from_config( else: providers = () # So returned auth providers are in same order as config - provider_hash = OrderedDict() # type: _ProviderDict + provider_hash: _ProviderDict = OrderedDict() for provider in providers: key = (provider.type, provider.id) provider_hash[key] = provider @@ -59,7 +59,7 @@ async def auth_manager_from_config( else: modules = () # So returned auth modules are in same order as config - module_hash = OrderedDict() # type: _MfaModuleDict + module_hash: _MfaModuleDict = OrderedDict() for module in modules: module_hash[module.id] = module @@ -168,11 +168,11 @@ class AuthManager: async def async_create_user(self, name: str) -> models.User: """Create a user.""" - kwargs = { + kwargs: Dict[str, Any] = { "name": name, "is_active": True, "group_ids": [GROUP_ID_ADMIN], - } # type: Dict[str, Any] + } if await self._user_should_be_owner(): kwargs["is_owner"] = True @@ -238,7 +238,7 @@ class AuthManager: group_ids: Optional[List[str]] = None, ) -> None: """Update a user.""" - kwargs = {} # type: Dict[str,Any] + kwargs: Dict[str, Any] = {} if name is not None: kwargs["name"] = name if group_ids is not None: @@ -278,9 +278,7 @@ class AuthManager: module = self.get_auth_mfa_module(mfa_module_id) if module is None: - raise ValueError( - "Unable find multi-factor auth module: {}".format(mfa_module_id) - ) + raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}") await module.async_setup_user(user.id, data) @@ -295,15 +293,13 @@ class AuthManager: module = self.get_auth_mfa_module(mfa_module_id) if module is None: - raise ValueError( - "Unable find multi-factor auth module: {}".format(mfa_module_id) - ) + raise ValueError(f"Unable find multi-factor auth module: {mfa_module_id}") await module.async_depose_user(user.id) async def async_get_enabled_mfa(self, user: models.User) -> Dict[str, str]: """List enabled mfa modules for user.""" - modules = OrderedDict() # type: Dict[str, str] + modules: Dict[str, str] = OrderedDict() for module_id, module in self._mfa_modules.items(): if await module.async_is_user_setup(user.id): modules[module_id] = module.name @@ -356,7 +352,7 @@ class AuthManager: ): # Each client_name can only have one # long_lived_access_token type of refresh token - raise ValueError("{} already exists".format(client_name)) + raise ValueError(f"{client_name} already exists") return await self._store.async_create_refresh_token( user, diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 82db0bcf7a9..4c64730edda 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -4,7 +4,7 @@ from collections import OrderedDict from datetime import timedelta import hmac from logging import getLogger -from typing import Any, Dict, List, Optional # noqa: F401 +from typing import Any, Dict, List, Optional from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION from homeassistant.core import HomeAssistant, callback @@ -13,7 +13,7 @@ from homeassistant.util import dt as dt_util from . import models from .const import GROUP_ID_ADMIN, GROUP_ID_USER, GROUP_ID_READ_ONLY from .permissions import PermissionLookup, system_policies -from .permissions.types import PolicyType # noqa: F401 +from .permissions.types import PolicyType STORAGE_VERSION = 1 STORAGE_KEY = "auth" @@ -34,9 +34,9 @@ class AuthStore: def __init__(self, hass: HomeAssistant) -> None: """Initialize the auth store.""" self.hass = hass - self._users = None # type: Optional[Dict[str, models.User]] - self._groups = None # type: Optional[Dict[str, models.Group]] - self._perm_lookup = None # type: Optional[PermissionLookup] + self._users: Optional[Dict[str, models.User]] = None + self._groups: Optional[Dict[str, models.Group]] = None + self._perm_lookup: Optional[PermissionLookup] = None self._store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) @@ -94,16 +94,16 @@ class AuthStore: for group_id in group_ids or []: group = self._groups.get(group_id) if group is None: - raise ValueError("Invalid group specified {}".format(group_id)) + raise ValueError(f"Invalid group specified {group_id}") groups.append(group) - kwargs = { + kwargs: Dict[str, Any] = { "name": name, # Until we get group management, we just put everyone in the # same group. "groups": groups, "perm_lookup": self._perm_lookup, - } # type: Dict[str, Any] + } if is_owner is not None: kwargs["is_owner"] = is_owner @@ -210,12 +210,12 @@ class AuthStore: access_token_expiration: timedelta = ACCESS_TOKEN_EXPIRATION, ) -> models.RefreshToken: """Create a new token for a user.""" - kwargs = { + kwargs: Dict[str, Any] = { "user": user, "client_id": client_id, "token_type": token_type, "access_token_expiration": access_token_expiration, - } # type: Dict[str, Any] + } if client_name: kwargs["client_name"] = client_name if client_icon: @@ -307,8 +307,8 @@ class AuthStore: self._set_defaults() return - users = OrderedDict() # type: Dict[str, models.User] - groups = OrderedDict() # type: Dict[str, models.Group] + users: Dict[str, models.User] = OrderedDict() + groups: Dict[str, models.Group] = OrderedDict() # Soft-migrating data as we load. We are going to make sure we have a # read only group and an admin group. There are two states that we can @@ -325,7 +325,7 @@ class AuthStore: # was added. for group_dict in data.get("groups", []): - policy = None # type: Optional[PolicyType] + policy: Optional[PolicyType] = None if group_dict["id"] == GROUP_ID_ADMIN: has_admin_group = True @@ -503,11 +503,11 @@ class AuthStore: groups = [] for group in self._groups.values(): - g_dict = { + g_dict: Dict[str, Any] = { "id": group.id, # Name not read for sys groups. Kept here for backwards compat "name": group.name, - } # type: Dict[str, Any] + } if not group.system_generated: g_dict["policy"] = group.policy @@ -558,7 +558,7 @@ class AuthStore: """Set default values for auth store.""" self._users = OrderedDict() - groups = OrderedDict() # type: Dict[str, models.Group] + groups: Dict[str, models.Group] = OrderedDict() admin_group = _system_admin_group() groups[admin_group.id] = admin_group user_group = _system_user_group() diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 5481b8fe08b..9d49f67df82 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -109,7 +109,7 @@ class SetupFlow(data_entry_flow.FlowHandler): Return self.async_show_form(step_id='init') if user_input is None. Return self.async_create_entry(data={'result': result}) if finish. """ - errors = {} # type: Dict[str, str] + errors: Dict[str, str] = {} if user_input: result = await self._auth_module.async_setup_user(self._user_id, user_input) @@ -144,15 +144,13 @@ async def auth_mfa_module_from_config( async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.ModuleType: """Load an mfa auth module.""" - module_path = "homeassistant.auth.mfa_modules.{}".format(module_name) + module_path = f"homeassistant.auth.mfa_modules.{module_name}" try: module = importlib.import_module(module_path) except ImportError as err: _LOGGER.error("Unable to load mfa module %s: %s", module_name, err) - raise HomeAssistantError( - "Unable to load mfa module {}: {}".format(module_name, err) - ) + raise HomeAssistantError(f"Unable to load mfa module {module_name}: {err}") if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index 4a41ff03ef6..a6a754fc2a6 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -95,7 +95,7 @@ class NotifyAuthModule(MultiFactorAuthModule): def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) - self._user_settings = None # type: Optional[_UsersDict] + self._user_settings: Optional[_UsersDict] = None self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) @@ -279,18 +279,18 @@ class NotifySetupFlow(SetupFlow): """Initialize the setup flow.""" super().__init__(auth_module, setup_schema, user_id) # to fix typing complaint - self._auth_module = auth_module # type: NotifyAuthModule + self._auth_module: NotifyAuthModule = auth_module self._available_notify_services = available_notify_services - self._secret = None # type: Optional[str] - self._count = None # type: Optional[int] - self._notify_service = None # type: Optional[str] - self._target = None # type: Optional[str] + self._secret: Optional[str] = None + self._count: Optional[int] = None + self._notify_service: Optional[str] = None + self._target: Optional[str] = None async def async_step_init( self, user_input: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: """Let user select available notify services.""" - errors = {} # type: Dict[str, str] + errors: Dict[str, str] = {} hass = self._auth_module.hass if user_input: @@ -304,7 +304,7 @@ class NotifySetupFlow(SetupFlow): if not self._available_notify_services: return self.async_abort(reason="no_available_service") - schema = OrderedDict() # type: Dict[str, Any] + schema: Dict[str, Any] = OrderedDict() schema["notify_service"] = vol.In(self._available_notify_services) schema["target"] = vol.Optional(str) @@ -316,7 +316,7 @@ class NotifySetupFlow(SetupFlow): self, user_input: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: """Verify user can recevie one-time password.""" - errors = {} # type: Dict[str, str] + errors: Dict[str, str] = {} hass = self._auth_module.hass if user_input: diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 22d153e3420..d6d901ac3b1 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -2,7 +2,7 @@ import asyncio import logging from io import BytesIO -from typing import Any, Dict, Optional, Tuple # noqa: F401 +from typing import Any, Dict, Optional, Tuple import voluptuous as vol @@ -75,7 +75,7 @@ class TotpAuthModule(MultiFactorAuthModule): def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None: """Initialize the user data store.""" super().__init__(hass, config) - self._users = None # type: Optional[Dict[str, str]] + self._users: Optional[Dict[str, str]] = None self._user_store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) @@ -107,7 +107,7 @@ class TotpAuthModule(MultiFactorAuthModule): """Create a ota_secret for user.""" import pyotp - ota_secret = secret or pyotp.random_base32() # type: str + ota_secret: str = secret or pyotp.random_base32() self._users[user_id] = ota_secret # type: ignore return ota_secret @@ -181,9 +181,9 @@ class TotpSetupFlow(SetupFlow): """Initialize the setup flow.""" super().__init__(auth_module, setup_schema, user.id) # to fix typing complaint - self._auth_module = auth_module # type: TotpAuthModule + self._auth_module: TotpAuthModule = auth_module self._user = user - self._ota_secret = None # type: Optional[str] + self._ota_secret: Optional[str] = None self._url = None # type Optional[str] self._image = None # type Optional[str] @@ -197,7 +197,7 @@ class TotpSetupFlow(SetupFlow): """ import pyotp - errors = {} # type: Dict[str, str] + errors: Dict[str, str] = {} if user_input: verified = await self.hass.async_add_executor_job( # type: ignore diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 533d7672ee4..6889d17a25f 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -1,6 +1,6 @@ """Auth models.""" from datetime import datetime, timedelta -from typing import Dict, List, NamedTuple, Optional # noqa: F401 +from typing import Dict, List, NamedTuple, Optional import uuid import attr @@ -20,7 +20,7 @@ TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" class Group: """A group.""" - name = attr.ib(type=str) # type: Optional[str] + name = attr.ib(type=Optional[str]) policy = attr.ib(type=perm_mdl.PolicyType) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) system_generated = attr.ib(type=bool, default=False) @@ -30,24 +30,20 @@ class Group: class User: """A user.""" - name = attr.ib(type=str) # type: Optional[str] - perm_lookup = attr.ib( - type=perm_mdl.PermissionLookup, cmp=False - ) # type: perm_mdl.PermissionLookup + name = attr.ib(type=Optional[str]) + perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, cmp=False) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) is_owner = attr.ib(type=bool, default=False) is_active = attr.ib(type=bool, default=False) system_generated = attr.ib(type=bool, default=False) - groups = attr.ib(type=List, factory=list, cmp=False) # type: List[Group] + groups = attr.ib(type=List[Group], factory=list, cmp=False) # List of credentials of a user. - credentials = attr.ib(type=list, factory=list, cmp=False) # type: List[Credentials] + credentials = attr.ib(type=List["Credentials"], factory=list, cmp=False) # Tokens associated with a user. - refresh_tokens = attr.ib( - type=dict, factory=dict, cmp=False - ) # type: Dict[str, RefreshToken] + refresh_tokens = attr.ib(type=Dict[str, "RefreshToken"], factory=dict, cmp=False) _permissions = attr.ib( type=Optional[perm_mdl.PolicyPermissions], init=False, cmp=False, default=None diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index 2708693743a..add9913abf3 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -1,6 +1,6 @@ """Entity permissions.""" from collections import OrderedDict -from typing import Callable, Optional # noqa: F401 +from typing import Callable, Optional import voluptuous as vol @@ -8,8 +8,7 @@ from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType -# pylint: disable=unused-import -from .util import SubCatLookupType, lookup_all, compile_policy # noqa +from .util import SubCatLookupType, lookup_all, compile_policy SINGLE_ENTITY_SCHEMA = vol.Any( True, @@ -90,7 +89,7 @@ def compile_entities( policy: CategoryType, perm_lookup: PermissionLookup ) -> Callable[[str, str], bool]: """Compile policy into a function that tests policy.""" - subcategories = OrderedDict() # type: SubCatLookupType + subcategories: SubCatLookupType = OrderedDict() subcategories[ENTITY_ENTITY_IDS] = _lookup_entity_id subcategories[ENTITY_DEVICE_IDS] = _lookup_device subcategories[ENTITY_AREAS] = _lookup_area diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py index f8b3639ad5a..3cf02e05771 100644 --- a/homeassistant/auth/permissions/merge.py +++ b/homeassistant/auth/permissions/merge.py @@ -1,13 +1,13 @@ """Merging of policies.""" -from typing import cast, Dict, List, Set # noqa: F401 +from typing import cast, Dict, List, Set from .types import PolicyType, CategoryType def merge_policies(policies: List[PolicyType]) -> PolicyType: """Merge policies.""" - new_policy = {} # type: Dict[str, CategoryType] - seen = set() # type: Set[str] + new_policy: Dict[str, CategoryType] = {} + seen: Set[str] = set() for policy in policies: for category in policy: if category in seen: @@ -33,8 +33,8 @@ def _merge_policies(sources: List[CategoryType]) -> CategoryType: # If there are multiple sources with a dict as policy, we recursively # merge each key in the source. - policy = None # type: CategoryType - seen = set() # type: Set[str] + policy: CategoryType = None + seen: Set[str] = set() for source in sources: if source is None: continue diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 6b44cbf61d4..109a5dc04ae 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -1,7 +1,7 @@ """Helpers to deal with permissions.""" from functools import wraps -from typing import Callable, Dict, List, Optional, cast # noqa: F401 +from typing import Callable, Dict, List, Optional, cast from .const import SUBCAT_ALL from .models import PermissionLookup @@ -45,7 +45,7 @@ def compile_policy( assert isinstance(policy, dict) - funcs = [] # type: List[Callable[[str, str], Optional[bool]]] + funcs: List[Callable[[str, str], Optional[bool]]] = [] for key, lookup_func in subcategories.items(): lookup_value = policy.get(key) @@ -85,7 +85,7 @@ def _gen_dict_test_func( def test_value(object_id: str, key: str) -> Optional[bool]: """Test if permission is allowed based on the keys.""" - schema = lookup_func(perm_lookup, lookup_dict, object_id) # type: ValueType + schema: ValueType = lookup_func(perm_lookup, lookup_dict, object_id) if schema is None or isinstance(schema, bool): return schema diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index c35af2e0b96..3e25003ad00 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -16,7 +16,7 @@ from homeassistant.util.decorator import Registry from ..auth_store import AuthStore from ..const import MFA_SESSION_EXPIRATION -from ..models import Credentials, User, UserMeta # noqa: F401 +from ..models import Credentials, User, UserMeta _LOGGER = logging.getLogger(__name__) DATA_REQS = "auth_prov_reqs_processed" @@ -144,14 +144,10 @@ async def load_auth_provider_module( ) -> types.ModuleType: """Load an auth provider.""" try: - module = importlib.import_module( - "homeassistant.auth.providers.{}".format(provider) - ) + module = importlib.import_module(f"homeassistant.auth.providers.{provider}") except ImportError as err: _LOGGER.error("Unable to load auth provider %s: %s", provider, err) - raise HomeAssistantError( - "Unable to load auth provider {}: {}".format(provider, err) - ) + raise HomeAssistantError(f"Unable to load auth provider {provider}: {err}") if hass.config.skip_pip or not hasattr(module, "REQUIREMENTS"): return module @@ -166,7 +162,7 @@ async def load_auth_provider_module( # https://github.com/python/mypy/issues/1424 reqs = module.REQUIREMENTS # type: ignore await requirements.async_process_requirements( - hass, "auth provider {}".format(provider), reqs + hass, f"auth provider {provider}", reqs ) processed.add(provider) @@ -179,12 +175,12 @@ class LoginFlow(data_entry_flow.FlowHandler): def __init__(self, auth_provider: AuthProvider) -> None: """Initialize the login flow.""" self._auth_provider = auth_provider - self._auth_module_id = None # type: Optional[str] + self._auth_module_id: Optional[str] = None self._auth_manager = auth_provider.hass.auth # type: ignore - self.available_mfa_modules = {} # type: Dict[str, str] + self.available_mfa_modules: Dict[str, str] = {} self.created_at = dt_util.utcnow() self.invalid_mfa_times = 0 - self.user = None # type: Optional[User] + self.user: Optional[User] = None async def async_step_init( self, user_input: Optional[Dict[str, str]] = None @@ -259,10 +255,10 @@ class LoginFlow(data_entry_flow.FlowHandler): if not errors: return await self.async_finish(self.user) - description_placeholders = { + description_placeholders: Dict[str, Optional[str]] = { "mfa_module_name": auth_module.name, "mfa_module_id": auth_module.id, - } # type: Dict[str, Optional[str]] + } return self.async_show_form( step_id="mfa", diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index cdf1a533412..58a2cac1fc5 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -53,7 +53,7 @@ class CommandLineAuthProvider(AuthProvider): attributes provided by external programs. """ super().__init__(*args, **kwargs) - self._user_meta = {} # type: Dict[str, Dict[str, Any]] + self._user_meta: Dict[str, Dict[str, Any]] = {} async def async_login_flow(self, context: Optional[dict]) -> LoginFlow: """Return a flow to login.""" @@ -85,7 +85,7 @@ class CommandLineAuthProvider(AuthProvider): raise InvalidAuthError if self.config[CONF_META]: - meta = {} # type: Dict[str, str] + meta: Dict[str, str] = {} for _line in stdout.splitlines(): try: line = _line.decode().lstrip() @@ -146,7 +146,7 @@ class CommandLineLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema = collections.OrderedDict() # type: Dict[str, type] + schema: Dict[str, type] = collections.OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index df38810fc29..265a24a4b28 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -4,7 +4,7 @@ import base64 from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional, Set, cast # noqa: F401 +from typing import Any, Dict, List, Optional, Set, cast import bcrypt import voluptuous as vol @@ -53,7 +53,7 @@ class Data: self._store = hass.helpers.storage.Store( STORAGE_VERSION, STORAGE_KEY, private=True ) - self._data = None # type: Optional[Dict[str, Any]] + self._data: Optional[Dict[str, Any]] = None # Legacy mode will allow usernames to start/end with whitespace # and will compare usernames case-insensitive. # Remove in 2020 or when we launch 1.0. @@ -74,7 +74,7 @@ class Data: if data is None: data = {"users": []} - seen = set() # type: Set[str] + seen: Set[str] = set() for user in data["users"]: username = user["username"] @@ -210,7 +210,7 @@ class HassAuthProvider(AuthProvider): def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize an Home Assistant auth provider.""" super().__init__(*args, **kwargs) - self.data = None # type: Optional[Data] + self.data: Optional[Data] = None self._init_lock = asyncio.Lock() async def async_initialize(self) -> None: @@ -296,7 +296,7 @@ class HassLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema = OrderedDict() # type: Dict[str, type] + schema: Dict[str, type] = OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 35524c3f5fc..37859f5ed0e 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -112,7 +112,7 @@ class ExampleLoginFlow(LoginFlow): user_input.pop("password") return await self.async_finish(user_input) - schema = OrderedDict() # type: Dict[str, type] + schema: Dict[str, type] = OrderedDict() schema["username"] = str schema["password"] = str diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b0eab0da0f3..7c4ec731b49 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -97,6 +97,17 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) + if sys.version_info[:3] < (3, 6, 1): + msg = ( + "Python 3.6.0 support is deprecated and will " + "be removed in the first release after October 2. Please " + "upgrade Python to 3.6.1 or higher." + ) + _LOGGER.warning(msg) + hass.components.persistent_notification.async_create( + msg, "Python version", "python_version" + ) + return hass @@ -163,7 +174,7 @@ def async_enable_logging( # ensure that the handlers it sets up wraps the correct streams. logging.basicConfig(level=logging.INFO) - colorfmt = "%(log_color)s{}%(reset)s".format(fmt) + colorfmt = f"%(log_color)s{fmt}%(reset)s" logging.getLogger().handlers[0].setFormatter( ColoredFormatter( colorfmt, @@ -206,9 +217,9 @@ def async_enable_logging( ): if log_rotate_days: - err_handler = logging.handlers.TimedRotatingFileHandler( + err_handler: logging.FileHandler = logging.handlers.TimedRotatingFileHandler( err_log_path, when="midnight", backupCount=log_rotate_days - ) # type: logging.FileHandler + ) else: err_handler = logging.FileHandler(err_log_path, mode="w", delay=True) @@ -335,7 +346,7 @@ async def _async_set_up_integrations( ) # Load all integrations - after_dependencies = {} # type: Dict[str, Set[str]] + after_dependencies: Dict[str, Set[str]] = {} for int_or_exc in await asyncio.gather( *(loader.async_get_integration(hass, domain) for domain in stage_2_domains), diff --git a/homeassistant/components/adguard/.translations/it.json b/homeassistant/components/adguard/.translations/it.json index 6cd8767334d..57f81dc1d99 100644 --- a/homeassistant/components/adguard/.translations/it.json +++ b/homeassistant/components/adguard/.translations/it.json @@ -1,21 +1,30 @@ { "config": { "abort": { + "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." }, "error": { "connection_error": "Impossibile connettersi." }, "step": { + "hassio_confirm": { + "description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon} ?", + "title": "AdGuard Home tramite il componente aggiuntivo di Hass.io" + }, "user": { "data": { "host": "Host", "password": "Password", "port": "Porta", "ssl": "AdGuard Home utilizza un certificato SSL", - "username": "Nome utente" - } + "username": "Nome utente", + "verify_ssl": "AdGuard Home utilizza un certificato appropriato" + }, + "description": "Configura l'istanza di AdGuard Home per consentire il monitoraggio e il controllo.", + "title": "Collega la tua AdGuard Home." } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json index 199b621c81b..e58c901f364 100644 --- a/homeassistant/components/adguard/.translations/pl.json +++ b/homeassistant/components/adguard/.translations/pl.json @@ -5,11 +5,11 @@ "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." }, "error": { - "connection_error": "Po\u0142\u0105czenie nieudane." + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia." }, "step": { "hassio_confirm": { - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z AdGuard Home przez dodatek Hass.io {addon}?", "title": "AdGuard Home przez dodatek Hass.io" }, "user": { @@ -21,7 +21,7 @@ "username": "Nazwa u\u017cytkownika", "verify_ssl": "AdGuard Home u\u017cywa odpowiedniego certyfikatu." }, - "description": "Skonfiguruj swoj\u0105 instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i nadz\u00f3r sieci.", + "description": "Skonfiguruj instancj\u0119 AdGuard Home, aby umo\u017cliwi\u0107 monitorowanie i kontrol\u0119.", "title": "Po\u0142\u0105cz sw\u00f3j AdGuard Home" } }, diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index 17e53270f25..e0c86e42d26 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -132,7 +132,7 @@ class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" percentage = await self.adguard.stats.blocked_percentage() - self._state = "{:.2f}".format(percentage) + self._state = f"{percentage:.2f}" class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor): @@ -205,7 +205,7 @@ class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor): async def _adguard_update(self) -> None: """Update AdGuard Home entity.""" average = await self.adguard.stats.avg_processing_time() - self._state = "{:.2f}".format(average) + self._state = f"{average:.2f}" class AdGuardHomeRulesCountSensor(AdGuardHomeSensor): diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index ec2dea69031..20e5196c0f1 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -194,7 +194,7 @@ class AirVisualSensor(Entity): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return "{0}_{1}_{2}".format(self._location_id, self._locale, self._type) + return f"{self._location_id}_{self._locale}_{self._type}" @property def unit_of_measurement(self): @@ -210,7 +210,7 @@ class AirVisualSensor(Entity): return if self._type == SENSOR_TYPE_LEVEL: - aqi = data["aqi{0}".format(self._locale)] + aqi = data[f"aqi{self._locale}"] [level] = [ i for i in POLLUTANT_LEVEL_MAPPING @@ -219,9 +219,9 @@ class AirVisualSensor(Entity): self._state = level["label"] self._icon = level["icon"] elif self._type == SENSOR_TYPE_AQI: - self._state = data["aqi{0}".format(self._locale)] + self._state = data[f"aqi{self._locale}"] elif self._type == SENSOR_TYPE_POLLUTANT: - symbol = data["main{0}".format(self._locale)] + symbol = data[f"main{self._locale}"] self._state = POLLUTANT_MAPPING[symbol]["label"] self._attrs.update( { diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index 770b663e8a3..b3da4fb4cbc 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -85,7 +85,7 @@ class AladdinDevice(CoverDevice): @property def unique_id(self): """Return a unique ID.""" - return "{}-{}".format(self._device_id, self._number) + return f"{self._device_id}-{self._number}" @property def name(self): diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 42f839bcd60..288c1dfd1c7 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -13,13 +13,17 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from . import DATA_AD, SIGNAL_PANEL_MESSAGE +from . import DATA_AD, DOMAIN as DOMAIN_ALARMDECODER, SIGNAL_PANEL_MESSAGE _LOGGER = logging.getLogger(__name__) SERVICE_ALARM_TOGGLE_CHIME = "alarmdecoder_alarm_toggle_chime" ALARM_TOGGLE_CHIME_SCHEMA = vol.Schema({vol.Required(ATTR_CODE): cv.string}) +SERVICE_ALARM_KEYPRESS = "alarm_keypress" +ATTR_KEYPRESS = "keypress" +ALARM_KEYPRESS_SCHEMA = vol.Schema({vol.Required(ATTR_KEYPRESS): cv.string}) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up for AlarmDecoder alarm panels.""" @@ -38,6 +42,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): schema=ALARM_TOGGLE_CHIME_SCHEMA, ) + def alarm_keypress_handler(service): + """Register keypress handler.""" + keypress = service.data[ATTR_KEYPRESS] + device.alarm_keypress(keypress) + + hass.services.register( + DOMAIN_ALARMDECODER, + SERVICE_ALARM_KEYPRESS, + alarm_keypress_handler, + schema=ALARM_KEYPRESS_SCHEMA, + ) + class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" @@ -124,24 +140,29 @@ class AlarmDecoderAlarmPanel(alarm.AlarmControlPanel): def alarm_disarm(self, code=None): """Send disarm command.""" if code: - self.hass.data[DATA_AD].send("{!s}1".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}1") def alarm_arm_away(self, code=None): """Send arm away command.""" if code: - self.hass.data[DATA_AD].send("{!s}2".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}2") def alarm_arm_home(self, code=None): """Send arm home command.""" if code: - self.hass.data[DATA_AD].send("{!s}3".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}3") def alarm_arm_night(self, code=None): """Send arm night command.""" if code: - self.hass.data[DATA_AD].send("{!s}33".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}33") def alarm_toggle_chime(self, code=None): """Send toggle chime command.""" if code: - self.hass.data[DATA_AD].send("{!s}9".format(code)) + self.hass.data[DATA_AD].send(f"{code!s}9") + + def alarm_keypress(self, keypress): + """Send custom keypresses.""" + if keypress: + self.hass.data[DATA_AD].send(keypress) diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index e69de29bb2d..55451d42f13 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -0,0 +1,9 @@ +alarm_keypress: + description: Send custom keypresses to the alarm. + fields: + entity_id: + description: Name of the alarm control panel to trigger. + example: 'alarm_control_panel.downstairs' + keypress: + description: 'String to send to the alarm panel.' + example: '*71' diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 56202b23e20..8c2fa692267 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -40,7 +40,7 @@ class AlexaInvalidEndpointError(AlexaError): def __init__(self, endpoint_id): """Initialize invalid endpoint error.""" - msg = "The endpoint {} does not exist".format(endpoint_id) + msg = f"The endpoint {endpoint_id} does not exist" AlexaError.__init__(self, msg) self.endpoint_id = endpoint_id @@ -73,7 +73,7 @@ class AlexaTempRangeError(AlexaError): "maximumValue": {"value": max_temp, "scale": API_TEMP_UNITS[unit]}, } payload = {"validRange": temp_range} - msg = "The requested temperature {} is out of range".format(temp) + msg = f"The requested temperature {temp} is out of range" AlexaError.__init__(self, msg, payload) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index cd5b56d60e2..1e636b96ee5 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -744,7 +744,7 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): presets = entity.attributes.get(climate.ATTR_PRESET_MODES, []) if ha_preset not in presets: - msg = "The requested thermostat mode {} is not supported".format(ha_preset) + msg = f"The requested thermostat mode {ha_preset} is not supported" raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_PRESET_MODE @@ -754,7 +754,7 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): operation_list = entity.attributes.get(climate.ATTR_HVAC_MODES) ha_mode = next((k for k, v in API_THERMOSTAT_MODES.items() if v == mode), None) if ha_mode not in operation_list: - msg = "The requested thermostat mode {} is not supported".format(mode) + msg = f"The requested thermostat mode {mode} is not supported" raise AlexaUnsupportedThermostatModeError(msg) service = climate.SERVICE_SET_HVAC_MODE diff --git a/homeassistant/components/alexa/intent.py b/homeassistant/components/alexa/intent.py index edeb6865aad..4cb75c65bc9 100644 --- a/homeassistant/components/alexa/intent.py +++ b/homeassistant/components/alexa/intent.py @@ -113,7 +113,7 @@ async def async_handle_message(hass, message): handler = HANDLERS.get(req_type) if not handler: - raise UnknownRequest("Received unknown request {}".format(req_type)) + raise UnknownRequest(f"Received unknown request {req_type}") return await handler(hass, message) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index fbf928fd23e..b7ff9d17fe8 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -60,7 +60,7 @@ async def async_send_changereport_message( """ token = await config.async_get_access_token() - headers = {"Authorization": "Bearer {}".format(token)} + headers = {"Authorization": f"Bearer {token}"} endpoint = alexa_entity.alexa_id() @@ -125,7 +125,7 @@ async def async_send_add_or_update_message(hass, config, entity_ids): """ token = await config.async_get_access_token() - headers = {"Authorization": "Bearer {}".format(token)} + headers = {"Authorization": f"Bearer {token}"} endpoints = [] @@ -155,7 +155,7 @@ async def async_send_delete_message(hass, config, entity_ids): """ token = await config.async_get_access_token() - headers = {"Authorization": "Bearer {}".format(token)} + headers = {"Authorization": f"Bearer {token}"} endpoints = [] diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 6d790e0719b..188567e4cf4 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -168,7 +168,7 @@ class AlphaVantageForeignExchange(Entity): if CONF_NAME in config: self._name = config.get(CONF_NAME) else: - self._name = "{}/{}".format(self._to_currency, self._from_currency) + self._name = f"{self._to_currency}/{self._from_currency}" self._unit_of_measurement = self._to_currency self._icon = ICONS.get(self._from_currency, "USD") self.values = None diff --git a/homeassistant/components/ambiclimate/.translations/it.json b/homeassistant/components/ambiclimate/.translations/it.json index b062eb67c1f..a13874b3676 100644 --- a/homeassistant/components/ambiclimate/.translations/it.json +++ b/homeassistant/components/ambiclimate/.translations/it.json @@ -1,7 +1,22 @@ { "config": { "abort": { - "already_setup": "L'account Ambiclimate \u00e8 configurato." + "access_token": "Errore sconosciuto durante la generazione di un token di accesso.", + "already_setup": "L'account Ambiclimate \u00e8 configurato.", + "no_config": "\u00c8 necessario configurare Ambiclimate prima di poter eseguire l'autenticazione con esso. [Leggere le istruzioni] (https://www.home-assistant.io/components/ambiclimate/)." + }, + "create_entry": { + "default": "Autenticato con successo con Ambiclimate" + }, + "error": { + "follow_link": "Si prega di seguire il link e di autenticarsi prima di premere Invia", + "no_token": "Non autenticato con Ambiclimate" + }, + "step": { + "auth": { + "description": "Segui questo [link]({authorization_url}) e Consenti accesso al tuo account Ambiclimate, quindi torna indietro e premi Invia qui sotto. \n (Assicurati che l'URL di richiamata specificato sia {cb_url})", + "title": "Autenticare Ambiclimate" + } }, "title": "Ambiclimate" } diff --git a/homeassistant/components/ambiclimate/.translations/pl.json b/homeassistant/components/ambiclimate/.translations/pl.json index dac6e52dda2..7ba95b007c9 100644 --- a/homeassistant/components/ambiclimate/.translations/pl.json +++ b/homeassistant/components/ambiclimate/.translations/pl.json @@ -3,18 +3,18 @@ "abort": { "access_token": "Nieznany b\u0142\u0105d podczas generowania tokena dost\u0119pu.", "already_setup": "Konto Ambiclimate jest skonfigurowane.", - "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)." + "no_config": "Musisz skonfigurowa\u0107 Ambiclimate, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 w nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Ambiclimate" }, "error": { "follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij", - "no_token": "Nie uwierzytelniony z Ambiclimate" + "no_token": "Nieuwierzytelniony z Ambiclimate" }, "step": { "auth": { - "description": "Kliknij poni\u017cszy [link]({authorization_url}) i Zezw\u00f3l na dost\u0119p do swojego konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})", + "description": "Kliknij poni\u017cszy [link]({authorization_url}) i Zezw\u00f3l na dost\u0119p do konta Ambiclimate, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n(Upewnij si\u0119, \u017ce podany adres URL to {cb_url})", "title": "Uwierzytelnienie Ambiclimate" } }, diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index db6d42d1d5c..99563dcb97d 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -130,7 +130,7 @@ class AmbiclimateFlowHandler(config_entries.ConfigFlow): return oauth def _cb_url(self): - return "{}{}".format(self.hass.config.api.base_url, AUTH_CALLBACK_PATH) + return f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" async def _get_authorize_url(self): oauth = self._generate_oauth() diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index e0d4e29a8e5..8e5ddb924ca 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ambiclimate", "requirements": [ - "ambiclimate==0.2.0" + "ambiclimate==0.2.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/ambient_station/.translations/it.json b/homeassistant/components/ambient_station/.translations/it.json index f87c987a79f..b468ba3673c 100644 --- a/homeassistant/components/ambient_station/.translations/it.json +++ b/homeassistant/components/ambient_station/.translations/it.json @@ -13,6 +13,7 @@ }, "title": "Inserisci i tuoi dati" } - } + }, + "title": "PWS ambientale" } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/pl.json b/homeassistant/components/ambient_station/.translations/pl.json index 2140b4e29fe..6ebd0848a63 100644 --- a/homeassistant/components/ambient_station/.translations/pl.json +++ b/homeassistant/components/ambient_station/.translations/pl.json @@ -11,7 +11,7 @@ "api_key": "Klucz API", "app_key": "Klucz aplikacji" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "Ambient PWS" diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 82c29f79983..bff03eb422b 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -492,7 +492,7 @@ class AmbientWeatherEntity(Entity): @property def name(self): """Return the name of the sensor.""" - return "{0}_{1}".format(self._station_name, self._sensor_name) + return f"{self._station_name}_{self._sensor_name}" @property def should_poll(self): @@ -502,7 +502,7 @@ class AmbientWeatherEntity(Entity): @property def unique_id(self): """Return a unique, unchanging string that represents this sensor.""" - return "{0}_{1}".format(self._mac_address, self._sensor_type) + return f"{self._mac_address}_{self._sensor_type}" async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index 483bdb2c7cf..f75a5adbe9c 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -490,7 +490,7 @@ class AmcrestCam(Camera): self._api.go_to_preset(action="start", preset_point_number=preset) except AmcrestError as error: log_update_error( - _LOGGER, "move", self.name, "camera to preset {}".format(preset), error + _LOGGER, "move", self.name, f"camera to preset {preset}", error ) def _set_color_bw(self, cbw): @@ -499,7 +499,7 @@ class AmcrestCam(Camera): self._api.day_night_color = _CBW.index(cbw) except AmcrestError as error: log_update_error( - _LOGGER, "set", self.name, "camera color mode to {}".format(cbw), error + _LOGGER, "set", self.name, f"camera color mode to {cbw}", error ) else: self._color_bw = cbw diff --git a/homeassistant/components/amcrest/helpers.py b/homeassistant/components/amcrest/helpers.py index d24d6e0e707..a40d6ace50a 100644 --- a/homeassistant/components/amcrest/helpers.py +++ b/homeassistant/components/amcrest/helpers.py @@ -4,7 +4,7 @@ from .const import DOMAIN def service_signal(service, ident=None): """Encode service and identifier into signal.""" - signal = "{}_{}".format(DOMAIN, service) + signal = f"{DOMAIN}_{service}" if ident: signal += "_{}".format(ident.replace(".", "_")) return signal diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index f55f20fc150..e63f59839a8 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -57,7 +57,7 @@ class AmpioSmogQuality(AirQualityEntity): @property def unique_id(self): """Return unique_name.""" - return "ampio_smog_{}".format(self._station_id) + return f"ampio_smog_{self._station_id}" @property def particulate_matter_2_5(self): diff --git a/homeassistant/components/android_ip_webcam/binary_sensor.py b/homeassistant/components/android_ip_webcam/binary_sensor.py index d7bf009701d..0e9cca46afb 100644 --- a/homeassistant/components/android_ip_webcam/binary_sensor.py +++ b/homeassistant/components/android_ip_webcam/binary_sensor.py @@ -25,7 +25,7 @@ class IPWebcamBinarySensor(AndroidIPCamEntity, BinarySensorDevice): self._sensor = sensor self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._unit = None diff --git a/homeassistant/components/android_ip_webcam/sensor.py b/homeassistant/components/android_ip_webcam/sensor.py index 20f4acebca6..05c1fe16c61 100644 --- a/homeassistant/components/android_ip_webcam/sensor.py +++ b/homeassistant/components/android_ip_webcam/sensor.py @@ -39,7 +39,7 @@ class IPWebcamSensor(AndroidIPCamEntity): self._sensor = sensor self._mapped_name = KEY_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._unit = None diff --git a/homeassistant/components/android_ip_webcam/switch.py b/homeassistant/components/android_ip_webcam/switch.py index 5b2f5dad5e1..2d5f2412d85 100644 --- a/homeassistant/components/android_ip_webcam/switch.py +++ b/homeassistant/components/android_ip_webcam/switch.py @@ -39,7 +39,7 @@ class IPWebcamSettingsSwitch(AndroidIPCamEntity, SwitchDevice): self._setting = setting self._mapped_name = KEY_MAP.get(self._setting, self._setting) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = False @property diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 047eaaaf5db..6643faa85bd 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,7 +3,7 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.24" + "androidtv==0.0.27" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index db4ff9e851e..d68f47b1b0a 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -392,7 +392,7 @@ class ADBDevice(MediaPlayerDevice): """Send an ADB command to an Android TV / Fire TV device.""" key = self._keys.get(cmd) if key: - self.aftv.adb_shell("input keyevent {}".format(key)) + self.aftv.adb_shell(f"input keyevent {key}") self._adb_response = None self.schedule_update_ha_state() return @@ -431,8 +431,10 @@ class AndroidTVDevice(ADBDevice): # Try to connect self._available = self.aftv.connect(always_log_errors=False) - # To be safe, wait until the next update to run ADB commands. - return + # To be safe, wait until the next update to run ADB commands if + # using the Python ADB implementation. + if not self.aftv.adb_server_ip: + return # If the ADB connection is not intact, don't update. if not self._available: @@ -443,7 +445,9 @@ class AndroidTVDevice(ADBDevice): self.aftv.update() ) - self._state = ANDROIDTV_STATES[state] + self._state = ANDROIDTV_STATES.get(state) + if self._state is None: + self._available = False @property def is_volume_muted(self): @@ -506,8 +510,10 @@ class FireTVDevice(ADBDevice): # Try to connect self._available = self.aftv.connect(always_log_errors=False) - # To be safe, wait until the next update to run ADB commands. - return + # To be safe, wait until the next update to run ADB commands if + # using the Python ADB implementation. + if not self.aftv.adb_server_ip: + return # If the ADB connection is not intact, don't update. if not self._available: @@ -518,7 +524,9 @@ class FireTVDevice(ADBDevice): self._get_sources ) - self._state = ANDROIDTV_STATES[state] + self._state = ANDROIDTV_STATES.get(state) + if self._state is None: + self._available = False @property def source(self): diff --git a/homeassistant/components/apache_kafka/__init__.py b/homeassistant/components/apache_kafka/__init__.py index caf96c61fb8..e0c8b824913 100644 --- a/homeassistant/components/apache_kafka/__init__.py +++ b/homeassistant/components/apache_kafka/__init__.py @@ -81,7 +81,7 @@ class KafkaManager: self._hass = hass self._producer = AIOKafkaProducer( loop=hass.loop, - bootstrap_servers="{0}:{1}".format(ip_address, port), + bootstrap_servers=f"{ip_address}:{port}", compression_type="gzip", ) self._topic = topic diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index ee991535104..d4faa55ed8c 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -138,7 +138,7 @@ class APIEventStream(HomeAssistantView): if payload is stop_obj: break - msg = "data: {}\n\n".format(payload) + msg = f"data: {payload}\n\n" _LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip()) await response.write(msg.encode("UTF-8")) except asyncio.TimeoutError: @@ -316,7 +316,7 @@ class APIEventView(HomeAssistantView): event_type, event_data, ha.EventOrigin.remote, self.context(request) ) - return self.json_message("Event {} fired.".format(event_type)) + return self.json_message(f"Event {event_type} fired.") class APIServicesView(HomeAssistantView): @@ -388,7 +388,7 @@ class APITemplateView(HomeAssistantView): return tpl.async_render(data.get("variables")) except (ValueError, TemplateError) as ex: return self.json_message( - "Error rendering template: {}".format(ex), HTTP_BAD_REQUEST + f"Error rendering template: {ex}", HTTP_BAD_REQUEST ) diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index 0b95cb9f0cb..dbd45013a3c 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -50,7 +50,7 @@ def get_service(hass, config, discovery_info=None): service = ApnsNotificationService(hass, name, topic, sandbox, cert_file) hass.services.register( - DOMAIN, "apns_{}".format(name), service.register, schema=REGISTER_SERVICE_SCHEMA + DOMAIN, f"apns_{name}", service.register, schema=REGISTER_SERVICE_SCHEMA ) return service @@ -98,7 +98,7 @@ class ApnsDevice: The full id of a device that is tracked by the device tracking component. """ - return "{}.{}".format(DEVICE_TRACKER_DOMAIN, self.tracking_id) + return f"{DEVICE_TRACKER_DOMAIN}.{self.tracking_id}" @property def disabled(self): @@ -124,9 +124,9 @@ def _write_device(out, device): """Write a single device to file.""" attributes = [] if device.name is not None: - attributes.append("name: {}".format(device.name)) + attributes.append(f"name: {device.name}") if device.tracking_device_id is not None: - attributes.append("tracking_device_id: {}".format(device.tracking_device_id)) + attributes.append(f"tracking_device_id: {device.tracking_device_id}") if device.disabled: attributes.append("disabled: True") diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index f21de733376..c391fb0e14b 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -3,7 +3,7 @@ "name": "Apple tv", "documentation": "https://www.home-assistant.io/components/apple_tv", "requirements": [ - "pyatv==0.3.12" + "pyatv==0.3.13" ], "dependencies": ["configurator"], "codeowners": [] diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 8ecaeab424c..9ac5ba77f98 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -127,6 +127,7 @@ class AppleTvDevice(MediaPlayerDevice): const.PLAY_STATE_PAUSED, const.PLAY_STATE_FAST_FORWARD, const.PLAY_STATE_FAST_BACKWARD, + const.PLAY_STATE_STOPPED, ): # Catch fast forward/backward here so "play" is default action return STATE_PAUSED @@ -212,7 +213,7 @@ class AppleTvDevice(MediaPlayerDevice): title = self._playing.title return title if title else "No title" - return "Establishing a connection to {0}...".format(self._name) + return f"Establishing a connection to {self._name}..." @property def supported_features(self): diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index c5ae8ed8414..86b0b6f48af 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -70,7 +70,7 @@ def gps_accuracy(gps, posambiguity: int) -> int: accuracy = round(dist_m) else: - message = "APRS position ambiguity must be 0-4, not '{0}'.".format(posambiguity) + message = f"APRS position ambiguity must be 0-4, not '{posambiguity}'." raise ValueError(message) return accuracy @@ -147,8 +147,7 @@ class AprsListenerThread(threading.Thread): ) self.ais.connect() self.start_complete( - True, - "Connected to {0} with callsign {1}.".format(self.host, self.callsign), + True, f"Connected to {self.host} with callsign {self.callsign}." ) self.ais.consumer(callback=self.rx_msg, immortal=True) except (AprsConnectionError, LoginError) as err: diff --git a/homeassistant/components/arcam_fmj/.translations/ca.json b/homeassistant/components/arcam_fmj/.translations/ca.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/ca.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/da.json b/homeassistant/components/arcam_fmj/.translations/da.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/da.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/de.json b/homeassistant/components/arcam_fmj/.translations/de.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/de.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/it.json b/homeassistant/components/arcam_fmj/.translations/it.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/it.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/ko.json b/homeassistant/components/arcam_fmj/.translations/ko.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/ko.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/nl.json b/homeassistant/components/arcam_fmj/.translations/nl.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/no.json b/homeassistant/components/arcam_fmj/.translations/no.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/no.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/pl.json b/homeassistant/components/arcam_fmj/.translations/pl.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/pl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/ru.json b/homeassistant/components/arcam_fmj/.translations/ru.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/ru.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/sl.json b/homeassistant/components/arcam_fmj/.translations/sl.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/sl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/zh-Hant.json b/homeassistant/components/arcam_fmj/.translations/zh-Hant.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/const.py b/homeassistant/components/arcam_fmj/const.py index b065e1a0833..dc5a576acec 100644 --- a/homeassistant/components/arcam_fmj/const.py +++ b/homeassistant/components/arcam_fmj/const.py @@ -9,5 +9,5 @@ DEFAULT_PORT = 50000 DEFAULT_NAME = "Arcam FMJ" DEFAULT_SCAN_INTERVAL = 5 -DOMAIN_DATA_ENTRIES = "{}.entries".format(DOMAIN) -DOMAIN_DATA_CONFIG = "{}.config".format(DOMAIN) +DOMAIN_DATA_ENTRIES = f"{DOMAIN}.entries" +DOMAIN_DATA_CONFIG = f"{DOMAIN}.config" diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 971abc3e26d..231e9821dc6 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -319,7 +319,7 @@ class ArcamFmj(MediaPlayerDevice): channel = self.media_channel if channel: - value = "{} - {}".format(source.name, channel) + value = f"{source.name} - {channel}" else: value = source.name return value diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 96ffa371864..669a28b7078 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -73,9 +73,7 @@ class ArestBinarySensor(BinarySensorDevice): self._pin = pin if self._pin is not None: - request = requests.get( - "{}/mode/{}/i".format(self._resource, self._pin), timeout=10 - ) + request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) if request.status_code != 200: _LOGGER.error("Can't set mode of %s", self._resource) @@ -112,9 +110,7 @@ class ArestData: def update(self): """Get the latest data from aREST device.""" try: - response = requests.get( - "{}/digital/{}".format(self._resource, self._pin), timeout=10 - ) + response = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) self.data = {"state": response.json()["return_value"]} except requests.exceptions.ConnectionError: _LOGGER.error("No route to device '%s'", self._resource) diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 533adeccb5e..2416eeb0ebb 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -148,9 +148,7 @@ class ArestSensor(Entity): self._renderer = renderer if self._pin is not None: - request = requests.get( - "{}/mode/{}/i".format(self._resource, self._pin), timeout=10 - ) + request = requests.get(f"{self._resource}/mode/{self._pin}/i", timeout=10) if request.status_code != 200: _LOGGER.error("Can't set mode of %s", self._resource) @@ -212,7 +210,7 @@ class ArestData: self.data = {"value": response.json()["return_value"]} except TypeError: response = requests.get( - "{}/digital/{}".format(self._resource, self._pin), timeout=10 + f"{self._resource}/digital/{self._pin}", timeout=10 ) self.data = {"value": response.json()["return_value"]} self.available = True diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index 558df89100e..e1a7edacb7e 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -114,7 +114,7 @@ class ArestSwitchFunction(ArestSwitchBase): super().__init__(resource, location, name) self._func = func - request = requests.get("{}/{}".format(self._resource, self._func), timeout=10) + request = requests.get(f"{self._resource}/{self._func}", timeout=10) if request.status_code != 200: _LOGGER.error("Can't find function") @@ -130,9 +130,7 @@ class ArestSwitchFunction(ArestSwitchBase): def turn_on(self, **kwargs): """Turn the device on.""" request = requests.get( - "{}/{}".format(self._resource, self._func), - timeout=10, - params={"params": "1"}, + f"{self._resource}/{self._func}", timeout=10, params={"params": "1"} ) if request.status_code == 200: @@ -143,9 +141,7 @@ class ArestSwitchFunction(ArestSwitchBase): def turn_off(self, **kwargs): """Turn the device off.""" request = requests.get( - "{}/{}".format(self._resource, self._func), - timeout=10, - params={"params": "0"}, + f"{self._resource}/{self._func}", timeout=10, params={"params": "0"} ) if request.status_code == 200: @@ -158,9 +154,7 @@ class ArestSwitchFunction(ArestSwitchBase): def update(self): """Get the latest data from aREST API and update the state.""" try: - request = requests.get( - "{}/{}".format(self._resource, self._func), timeout=10 - ) + request = requests.get(f"{self._resource}/{self._func}", timeout=10) self._state = request.json()["return_value"] != 0 self._available = True except requests.exceptions.ConnectionError: @@ -177,9 +171,7 @@ class ArestSwitchPin(ArestSwitchBase): self._pin = pin self.invert = invert - request = requests.get( - "{}/mode/{}/o".format(self._resource, self._pin), timeout=10 - ) + request = requests.get(f"{self._resource}/mode/{self._pin}/o", timeout=10) if request.status_code != 200: _LOGGER.error("Can't set mode") self._available = False @@ -188,8 +180,7 @@ class ArestSwitchPin(ArestSwitchBase): """Turn the device on.""" turn_on_payload = int(not self.invert) request = requests.get( - "{}/digital/{}/{}".format(self._resource, self._pin, turn_on_payload), - timeout=10, + f"{self._resource}/digital/{self._pin}/{turn_on_payload}", timeout=10 ) if request.status_code == 200: self._state = True @@ -200,8 +191,7 @@ class ArestSwitchPin(ArestSwitchBase): """Turn the device off.""" turn_off_payload = int(self.invert) request = requests.get( - "{}/digital/{}/{}".format(self._resource, self._pin, turn_off_payload), - timeout=10, + f"{self._resource}/digital/{self._pin}/{turn_off_payload}", timeout=10 ) if request.status_code == 200: self._state = False @@ -211,9 +201,7 @@ class ArestSwitchPin(ArestSwitchBase): def update(self): """Get the latest data from aREST API and update the state.""" try: - request = requests.get( - "{}/digital/{}".format(self._resource, self._pin), timeout=10 - ) + request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10) status_value = int(self.invert) self._state = request.json()["return_value"] != status_value self._available = True diff --git a/homeassistant/components/atome/__init__.py b/homeassistant/components/atome/__init__.py new file mode 100644 index 00000000000..6f524606a81 --- /dev/null +++ b/homeassistant/components/atome/__init__.py @@ -0,0 +1 @@ +"""Support for Atome devices connected to a Linky Energy Meter.""" diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json new file mode 100644 index 00000000000..621faba4fc0 --- /dev/null +++ b/homeassistant/components/atome/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "atome", + "name": "Atome", + "documentation": "https://www.home-assistant.io/components/atome", + "dependencies": [], + "codeowners": ["@baqs"], + "requirements": ["pyatome==0.1.1"] +} diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py new file mode 100644 index 00000000000..c98b634bb21 --- /dev/null +++ b/homeassistant/components/atome/sensor.py @@ -0,0 +1,279 @@ +"""Linky Atome.""" +import logging +from datetime import timedelta + +import voluptuous as vol +from pyatome.client import AtomeClient +from pyatome.client import PyAtomeError + +from homeassistant.const import ( + CONF_PASSWORD, + CONF_USERNAME, + CONF_NAME, + DEVICE_CLASS_POWER, + POWER_WATT, + ENERGY_KILO_WATT_HOUR, +) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "atome" + +LIVE_SCAN_INTERVAL = timedelta(seconds=30) +DAILY_SCAN_INTERVAL = timedelta(seconds=150) +WEEKLY_SCAN_INTERVAL = timedelta(hours=1) +MONTHLY_SCAN_INTERVAL = timedelta(hours=1) +YEARLY_SCAN_INTERVAL = timedelta(days=1) + +LIVE_NAME = "Atome Live Power" +DAILY_NAME = "Atome Daily" +WEEKLY_NAME = "Atome Weekly" +MONTHLY_NAME = "Atome Monthly" +YEARLY_NAME = "Atome Yearly" + +LIVE_TYPE = "live" +DAILY_TYPE = "day" +WEEKLY_TYPE = "week" +MONTHLY_TYPE = "month" +YEARLY_TYPE = "year" + +ICON = "mdi:flash" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Atome sensor.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + try: + atome_client = AtomeClient(username, password) + atome_client.login() + except PyAtomeError as exp: + _LOGGER.error(exp) + return + + data = AtomeData(atome_client) + + sensors = [] + sensors.append(AtomeSensor(data, LIVE_NAME, LIVE_TYPE)) + sensors.append(AtomeSensor(data, DAILY_NAME, DAILY_TYPE)) + sensors.append(AtomeSensor(data, WEEKLY_NAME, WEEKLY_TYPE)) + sensors.append(AtomeSensor(data, MONTHLY_NAME, MONTHLY_TYPE)) + sensors.append(AtomeSensor(data, YEARLY_NAME, YEARLY_TYPE)) + + add_entities(sensors, True) + + +class AtomeData: + """Stores data retrieved from Neurio sensor.""" + + def __init__(self, client: AtomeClient): + """Initialize the data.""" + self.atome_client = client + self._live_power = None + self._subscribed_power = None + self._is_connected = None + self._day_usage = None + self._day_price = None + self._week_usage = None + self._week_price = None + self._month_usage = None + self._month_price = None + self._year_usage = None + self._year_price = None + + @property + def live_power(self): + """Return latest active power value.""" + return self._live_power + + @property + def subscribed_power(self): + """Return latest active power value.""" + return self._subscribed_power + + @property + def is_connected(self): + """Return latest active power value.""" + return self._is_connected + + @Throttle(LIVE_SCAN_INTERVAL) + def update_live_usage(self): + """Return current power value.""" + try: + values = self.atome_client.get_live() + self._live_power = values["last"] + self._subscribed_power = values["subscribed"] + self._is_connected = values["isConnected"] + _LOGGER.debug( + "Updating Atome live data. Got: %d, isConnected: %s, subscribed: %d", + self._live_power, + self._is_connected, + self._subscribed_power, + ) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def day_usage(self): + """Return latest daily usage value.""" + return self._day_usage + + @property + def day_price(self): + """Return latest daily usage value.""" + return self._day_price + + @Throttle(DAILY_SCAN_INTERVAL) + def update_day_usage(self): + """Return current daily power usage.""" + try: + values = self.atome_client.get_consumption(DAILY_TYPE) + self._day_usage = values["total"] / 1000 + self._day_price = values["price"] + _LOGGER.debug("Updating Atome daily data. Got: %d.", self._day_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def week_usage(self): + """Return latest weekly usage value.""" + return self._week_usage + + @property + def week_price(self): + """Return latest weekly usage value.""" + return self._week_price + + @Throttle(WEEKLY_SCAN_INTERVAL) + def update_week_usage(self): + """Return current weekly power usage.""" + try: + values = self.atome_client.get_consumption(WEEKLY_TYPE) + self._week_usage = values["total"] / 1000 + self._week_price = values["price"] + _LOGGER.debug("Updating Atome weekly data. Got: %d.", self._week_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def month_usage(self): + """Return latest monthly usage value.""" + return self._month_usage + + @property + def month_price(self): + """Return latest monthly usage value.""" + return self._month_price + + @Throttle(MONTHLY_SCAN_INTERVAL) + def update_month_usage(self): + """Return current monthly power usage.""" + try: + values = self.atome_client.get_consumption(MONTHLY_TYPE) + self._month_usage = values["total"] / 1000 + self._month_price = values["price"] + _LOGGER.debug("Updating Atome monthly data. Got: %d.", self._month_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + @property + def year_usage(self): + """Return latest yearly usage value.""" + return self._year_usage + + @property + def year_price(self): + """Return latest yearly usage value.""" + return self._year_price + + @Throttle(YEARLY_SCAN_INTERVAL) + def update_year_usage(self): + """Return current yearly power usage.""" + try: + values = self.atome_client.get_consumption(YEARLY_TYPE) + self._year_usage = values["total"] / 1000 + self._year_price = values["price"] + _LOGGER.debug("Updating Atome yearly data. Got: %d.", self._year_usage) + + except KeyError as error: + _LOGGER.error("Missing last value in values: %s: %s", values, error) + + +class AtomeSensor(Entity): + """Representation of a sensor entity for Atome.""" + + def __init__(self, data, name, sensor_type): + """Initialize the sensor.""" + self._name = name + self._data = data + self._state = None + self._attributes = {} + + self._sensor_type = sensor_type + + if sensor_type == LIVE_TYPE: + self._unit_of_measurement = POWER_WATT + else: + self._unit_of_measurement = ENERGY_KILO_WATT_HOUR + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_POWER + + def update(self): + """Update device state.""" + update_function = getattr(self._data, f"update_{self._sensor_type}_usage") + update_function() + + if self._sensor_type == LIVE_TYPE: + self._state = self._data.live_power + self._attributes["subscribed_power"] = self._data.subscribed_power + self._attributes["is_connected"] = self._data.is_connected + else: + self._state = getattr(self._data, f"{self._sensor_type}_usage") + self._attributes["price"] = getattr( + self._data, f"{self._sensor_type}_price" + ) diff --git a/homeassistant/components/august/camera.py b/homeassistant/components/august/camera.py index a8335d1aa52..2492eb75418 100644 --- a/homeassistant/components/august/camera.py +++ b/homeassistant/components/august/camera.py @@ -73,4 +73,4 @@ class AugustCamera(Camera): @property def unique_id(self) -> str: """Get the unique id of the camera.""" - return "{:s}_camera".format(self._doorbell.device_id) + return f"{self._doorbell.device_id:s}_camera" diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index e919c47dd4c..8b8c019eb2d 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -93,4 +93,4 @@ class AugustLock(LockDevice): @property def unique_id(self) -> str: """Get the unique id of the lock.""" - return "{:s}_lock".format(self._lock.device_id) + return f"{self._lock.device_id:s}_lock" diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index 0d983f35e37..a69433c4186 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -64,7 +64,7 @@ class AuroraSensor(BinarySensorDevice): @property def name(self): """Return the name of the sensor.""" - return "{}".format(self._name) + return f"{self._name}" @property def is_on(self): diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 456b5080484..05ed5fa99bf 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -49,7 +49,7 @@ class AuroraABBSolarPVMonitorSensor(Entity): def __init__(self, client, name, typename): """Initialize the sensor.""" - self._name = "{} {}".format(name, typename) + self._name = f"{name} {typename}" self.client = client self._state = None diff --git a/homeassistant/components/auth/.translations/it.json b/homeassistant/components/auth/.translations/it.json index be06f0209c4..dbfe4acd615 100644 --- a/homeassistant/components/auth/.translations/it.json +++ b/homeassistant/components/auth/.translations/it.json @@ -10,7 +10,7 @@ "step": { "init": { "description": "Selezionare uno dei servizi di notifica:", - "title": "Imposta la password one-time fornita dal componente di notifica" + "title": "Imposta la password monouso fornita dal componente di notifica" }, "setup": { "description": "\u00c8 stata inviata una password monouso tramite **notify.{notify_service}**. Per favore, inseriscila qui sotto:", @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Per attivare l'autenticazione a due fattori utilizzando password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Dopo aver scansionato il codice, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con codice ** ` {code} ` **.", + "description": "Per attivare l'autenticazione a due fattori utilizzando le password monouso basate sul tempo, eseguire la scansione del codice QR con l'app di autenticazione. Se non ne hai uno, ti consigliamo [Google Authenticator] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \nDopo la scansione, inserisci il codice a sei cifre dalla tua app per verificare la configurazione. Se riscontri problemi con la scansione del codice QR, esegui una configurazione manuale con il codice ** ` {code} ` **.", "title": "Imposta l'autenticazione a due fattori usando TOTP" } }, diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 6c2e8988d83..1cb70519b20 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [\uad6c\uae00 OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/auth/.translations/pl.json b/homeassistant/components/auth/.translations/pl.json index f0e9f7b71ea..78610a5324f 100644 --- a/homeassistant/components/auth/.translations/pl.json +++ b/homeassistant/components/auth/.translations/pl.json @@ -13,7 +13,7 @@ "title": "Skonfiguruj has\u0142o jednorazowe dostarczone przez komponent powiadomie\u0144" }, "setup": { - "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wpisz je poni\u017cej:", + "description": "Has\u0142o jednorazowe zosta\u0142o wys\u0142ane przez **notify.{notify_service}**. Wprowad\u017a je poni\u017cej:", "title": "Sprawd\u017a konfiguracj\u0119" } }, diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index c18bc276a44..42dab7ebb5a 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -34,7 +34,7 @@ async def async_setup(hass): """Create a setup flow. handler is a mfa module.""" mfa_module = hass.auth.get_auth_mfa_module(handler) if mfa_module is None: - raise ValueError("Mfa module {} is not found".format(handler)) + raise ValueError(f"Mfa module {handler} is not found") user_id = data.pop("user_id") return await mfa_module.async_setup_flow(user_id) @@ -80,9 +80,7 @@ def websocket_setup_mfa( if mfa_module is None: connection.send_message( websocket_api.error_message( - msg["id"], - "no_module", - "MFA module {} is not found".format(mfa_module_id), + msg["id"], "no_module", f"MFA module {mfa_module_id} is not found" ) ) return @@ -117,7 +115,7 @@ def websocket_depose_mfa( websocket_api.error_message( msg["id"], "disable_failed", - "Cannot disable MFA Module {}: {}".format(mfa_module_id, err), + f"Cannot disable MFA Module {mfa_module_id}: {err}", ) ) return diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 5de9336d1d9..03eedd6d162 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -6,6 +6,9 @@ import logging import voluptuous as vol +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -143,7 +146,7 @@ async def async_setup(hass, config): async def turn_onoff_service_handler(service_call): """Handle automation turn on/off service calls.""" tasks = [] - method = "async_{}".format(service_call.service) + method = f"async_{service_call.service}" for entity in await component.async_extract_from_service(service_call): tasks.append(getattr(entity, method)()) @@ -378,7 +381,7 @@ async def _async_process_config(hass, config, component): for list_no, config_block in enumerate(conf): automation_id = config_block.get(CONF_ID) - name = config_block.get(CONF_ALIAS) or "{} {}".format(config_key, list_no) + name = config_block.get(CONF_ALIAS) or f"{config_key} {list_no}" hidden = config_block[CONF_HIDE_ENTITY] initial_state = config_block.get(CONF_INITIAL_STATE) @@ -386,7 +389,7 @@ async def _async_process_config(hass, config, component): action = _async_get_action(hass, config_block.get(CONF_ACTION, {}), name) if CONF_CONDITION in config_block: - cond_func = _async_process_if(hass, config, config_block) + cond_func = await _async_process_if(hass, config, config_block) if cond_func is None: continue @@ -431,20 +434,20 @@ def _async_get_action(hass, config, name): await script_obj.async_run(variables, context) except Exception as err: # pylint: disable=broad-except script_obj.async_log_exception( - _LOGGER, "Error while executing automation {}".format(entity_id), err + _LOGGER, f"Error while executing automation {entity_id}", err ) return action -def _async_process_if(hass, config, p_config): +async def _async_process_if(hass, config, p_config): """Process if checks.""" if_configs = p_config.get(CONF_CONDITION) checks = [] for if_config in if_configs: try: - checks.append(condition.async_from_config(if_config, False)) + checks.append(await condition.async_from_config(hass, if_config, False)) except HomeAssistantError as ex: _LOGGER.warning("Invalid condition: %s", ex) return None @@ -467,7 +470,10 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): for conf in trigger_configs: platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) - remove = await platform.async_trigger(hass, conf, action, info) + try: + remove = await platform.async_trigger(hass, conf, action, info) + except InvalidDeviceAutomationConfig: + remove = False if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index ea63d4ff98a..935cc7a9175 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -4,6 +4,7 @@ "documentation": "https://www.home-assistant.io/components/automation", "requirements": [], "dependencies": [ + "device_automation", "group", "webhook" ], diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index 85b5a0be191..c899e009796 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -150,7 +150,7 @@ class AwairSensor(Entity): """Initialize the sensor.""" self._uuid = device[CONF_UUID] self._device_class = SENSOR_TYPES[sensor_type]["device_class"] - self._name = "Awair {}".format(self._device_class) + self._name = f"Awair {self._device_class}" unit = SENSOR_TYPES[sensor_type]["unit_of_measurement"] self._unit_of_measurement = unit self._data = data @@ -202,7 +202,7 @@ class AwairSensor(Entity): @property def unique_id(self): """Return the unique id of this entity.""" - return "{}_{}".format(self._uuid, self._type) + return f"{self._uuid}_{self._type}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index 2498c28ec33..e979af08836 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -3,6 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "bad_config_file": "Dati errati dal file di configurazione", + "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis" }, "error": { diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 3864ac344e1..f22a169a102 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -72,7 +72,7 @@ class AxisEventBase(AxisEntityBase): @property def name(self): """Return the name of the event.""" - return "{} {} {}".format(self.device.name, self.event.TYPE, self.event.id) + return f"{self.device.name} {self.event.TYPE} {self.event.id}" @property def should_poll(self): @@ -82,4 +82,4 @@ class AxisEventBase(AxisEntityBase): @property def unique_id(self): """Return a unique identifier for this device.""" - return "{}-{}-{}".format(self.device.serial, self.event.topic, self.event.id) + return f"{self.device.serial}-{self.event.topic}-{self.event.id}" diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index e7e0f7459f3..a55e45dd374 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -92,4 +92,4 @@ class AxisCamera(AxisEntityBase, MjpegCamera): @property def unique_id(self): """Return a unique identifier for this device.""" - return "{}-camera".format(self.device.serial) + return f"{self.device.serial}-camera" diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 4b54982244b..3b5efe96760 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -56,8 +56,7 @@ def configured_devices(hass): } -@config_entries.HANDLERS.register(DOMAIN) -class AxisFlowHandler(config_entries.ConfigFlow): +class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Axis config flow.""" VERSION = 1 @@ -138,9 +137,9 @@ class AxisFlowHandler(config_entries.ConfigFlow): if entry.data[CONF_MODEL] == self.model ] - self.name = "{}".format(self.model) + self.name = f"{self.model}" for idx in range(len(same_model) + 1): - self.name = "{} {}".format(self.model, idx) + self.name = f"{self.model} {idx}" if self.name not in same_model: break @@ -151,7 +150,7 @@ class AxisFlowHandler(config_entries.ConfigFlow): CONF_MODEL: self.model, } - title = "{} - {}".format(self.model, self.serial_number) + title = f"{self.model} - {self.serial_number}" return self.async_create_entry(title=title, data=data) async def _update_entry(self, entry, host): diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 465d8c73b74..3b91f7e1474 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -65,7 +65,7 @@ class AxisNetworkDevice: connections={(CONNECTION_NETWORK_MAC, self.serial)}, identifiers={(DOMAIN, self.serial)}, manufacturer="Axis Communications AB", - model="{} {}".format(self.model, self.product_type), + model=f"{self.model} {self.product_type}", name=self.name, sw_version=self.fw_version, ) @@ -115,7 +115,7 @@ class AxisNetworkDevice: @property def event_new_address(self): """Device specific event to signal new device address.""" - return "axis_new_address_{}".format(self.serial) + return f"axis_new_address_{self.serial}" @staticmethod async def async_new_address_callback(hass, entry): @@ -131,7 +131,7 @@ class AxisNetworkDevice: @property def event_reachable(self): """Device specific event to signal a change in connection status.""" - return "axis_reachable_{}".format(self.serial) + return f"axis_reachable_{self.serial}" @callback def async_connection_status_callback(self, status): @@ -149,7 +149,7 @@ class AxisNetworkDevice: @property def event_new_sensor(self): """Device specific event to signal new sensor available.""" - return "axis_add_sensor_{}".format(self.serial) + return f"axis_add_sensor_{self.serial}" @callback def async_event_callback(self, action, event_id): diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index b565d05685f..89449aeab45 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -2,6 +2,7 @@ from collections import namedtuple from datetime import timedelta import logging +from typing import List import voluptuous as vol @@ -41,12 +42,11 @@ class BboxDeviceScanner(DeviceScanner): def __init__(self, config): """Get host from config.""" - from typing import List # noqa: pylint: disable=unused-import self.host = config[CONF_HOST] """Initialize the scanner.""" - self.last_results = [] # type: List[Device] + self.last_results: List[Device] = [] self.success_init = self._update_info() _LOGGER.info("Scanner initialized") diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 76621b7792b..ba38f8d2607 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -13,7 +13,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -BANDWIDTH_MEGABITS_SECONDS = "Mb/s" # type: str +BANDWIDTH_MEGABITS_SECONDS = "Mb/s" ATTRIBUTION = "Powered by Bouygues Telecom" @@ -91,7 +91,7 @@ class BboxSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/beewi_smartclim/__init__.py b/homeassistant/components/beewi_smartclim/__init__.py new file mode 100644 index 00000000000..f907ce95ae6 --- /dev/null +++ b/homeassistant/components/beewi_smartclim/__init__.py @@ -0,0 +1 @@ +"""The beewi_smartclim component.""" diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json new file mode 100644 index 00000000000..3e9ad732b74 --- /dev/null +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "beewi_smartclim", + "name": "BeeWi SmartClim BLE sensor", + "documentation": "https://www.home-assistant.io/components/beewi_smartclim", + "requirements": [ + "beewi_smartclim==0.0.7" + ], + "dependencies": [], + "codeowners": [ + "@alemuro" + ] +} \ No newline at end of file diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py new file mode 100644 index 00000000000..7bfa8883013 --- /dev/null +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -0,0 +1,108 @@ +"""Platform for beewi_smartclim integration.""" +import logging + +from beewi_smartclim import BeewiSmartClimPoller +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ( + CONF_NAME, + CONF_MAC, + TEMP_CELSIUS, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_BATTERY, +) +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +# Default values +DEFAULT_NAME = "BeeWi SmartClim" + +# Sensor config +SENSOR_TYPES = [ + [DEVICE_CLASS_TEMPERATURE, "Temperature", TEMP_CELSIUS], + [DEVICE_CLASS_HUMIDITY, "Humidity", "%"], + [DEVICE_CLASS_BATTERY, "Battery", "%"], +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_MAC): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the beewi_smartclim platform.""" + + mac = config[CONF_MAC] + prefix = config[CONF_NAME] + poller = BeewiSmartClimPoller(mac) + + sensors = [] + + for sensor_type in SENSOR_TYPES: + device = sensor_type[0] + name = sensor_type[1] + unit = sensor_type[2] + # `prefix` is the name configured by the user for the sensor, we're appending + # the device type at the end of the name (garden -> garden temperature) + if prefix: + name = f"{prefix} {name}" + + sensors.append(BeewiSmartclimSensor(poller, name, mac, device, unit)) + + add_entities(sensors) + + +class BeewiSmartclimSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, poller, name, mac, device, unit): + """Initialize the sensor.""" + self._poller = poller + self._name = name + self._mac = mac + self._device = device + self._unit = unit + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor. State is returned in Celsius.""" + return self._state + + @property + def device_class(self): + """Device class of this entity.""" + return self._device + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return f"{self._mac}_{self._device}" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + def update(self): + """Fetch new state data from the poller.""" + self._poller.update_sensor() + self._state = None + if self._device == DEVICE_CLASS_TEMPERATURE: + self._state = self._poller.get_temperature() + if self._device == DEVICE_CLASS_HUMIDITY: + self._state = self._poller.get_humidity() + if self._device == DEVICE_CLASS_BATTERY: + self._state = self._poller.get_battery() diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index a77fad69663..eca7fa84f50 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -99,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for zone_id, extra in config[CONF_ZONES].items(): _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) - unique_id = "{}-{}".format(connection, zone_id) + unique_id = f"{connection}-{zone_id}" device = BlackbirdZone(blackbird, sources, zone_id, extra[CONF_NAME]) hass.data[DATA_BLACKBIRD][unique_id] = device devices.append(device) diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index adcefeddf23..b1c9f6a7ec0 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -55,7 +55,7 @@ class BlinkSyncModule(AlarmControlPanel): @property def name(self): """Return the name of the panel.""" - return "{} {}".format(BLINK_DATA, self._name) + return f"{BLINK_DATA} {self._name}" @property def device_state_attributes(self): diff --git a/homeassistant/components/blink/binary_sensor.py b/homeassistant/components/blink/binary_sensor.py index 4c268989d32..e8c01953bff 100644 --- a/homeassistant/components/blink/binary_sensor.py +++ b/homeassistant/components/blink/binary_sensor.py @@ -26,11 +26,11 @@ class BlinkBinarySensor(BinarySensorDevice): self.data = data self._type = sensor_type name, icon = BINARY_SENSORS[sensor_type] - self._name = "{} {} {}".format(BLINK_DATA, camera, name) + self._name = f"{BLINK_DATA} {camera} {name}" self._icon = icon self._camera = data.cameras[camera] self._state = None - self._unique_id = "{}-{}".format(self._camera.serial, self._type) + self._unique_id = f"{self._camera.serial}-{self._type}" @property def name(self): diff --git a/homeassistant/components/blink/camera.py b/homeassistant/components/blink/camera.py index 5e8b5323f89..52043324a40 100644 --- a/homeassistant/components/blink/camera.py +++ b/homeassistant/components/blink/camera.py @@ -30,9 +30,9 @@ class BlinkCamera(Camera): """Initialize a camera.""" super().__init__() self.data = data - self._name = "{} {}".format(BLINK_DATA, name) + self._name = f"{BLINK_DATA} {name}" self._camera = camera - self._unique_id = "{}-camera".format(camera.serial) + self._unique_id = f"{camera.serial}-camera" self.response = None self.current_image = None self.last_image = None diff --git a/homeassistant/components/blink/sensor.py b/homeassistant/components/blink/sensor.py index fba2d0bd493..81616b463ec 100644 --- a/homeassistant/components/blink/sensor.py +++ b/homeassistant/components/blink/sensor.py @@ -28,7 +28,7 @@ class BlinkSensor(Entity): def __init__(self, data, camera, sensor_type): """Initialize sensors from Blink camera.""" name, units, icon = SENSORS[sensor_type] - self._name = "{} {} {}".format(BLINK_DATA, camera, name) + self._name = f"{BLINK_DATA} {camera} {name}" self._camera_name = name self._type = sensor_type self.data = data @@ -36,7 +36,7 @@ class BlinkSensor(Entity): self._state = None self._unit_of_measurement = units self._icon = icon - self._unique_id = "{}-{}".format(self._camera.serial, self._type) + self._unique_id = f"{self._camera.serial}-{self._type}" self._sensor_key = self._type if self._type == "temperature": self._sensor_key = "temperature_calibrated" diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index 9fee72662c6..e626a73d287 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -51,7 +51,7 @@ class BlinktLight(Light): Default brightness and white color. """ self._blinkt = blinkt - self._name = "{}_{}".format(name, index) + self._name = f"{name}_{index}" self._index = index self._is_on = False self._brightness = 255 diff --git a/homeassistant/components/bloomsky/__init__.py b/homeassistant/components/bloomsky/__init__.py index dc0723730c4..6373471fe7a 100644 --- a/homeassistant/components/bloomsky/__init__.py +++ b/homeassistant/components/bloomsky/__init__.py @@ -63,7 +63,7 @@ class BloomSky: """Use the API to retrieve a list of devices.""" _LOGGER.debug("Fetching BloomSky update") response = requests.get( - "{}?{}".format(self.API_URL, self._endpoint_argument), + f"{self.API_URL}?{self._endpoint_argument}", headers={AUTHORIZATION: self._api_key}, timeout=10, ) diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 3a8242929c5..99951fcf5c5 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -42,7 +42,7 @@ class BloomSkySensor(BinarySensorDevice): self._sensor_name = sensor_name self._name = "{} {}".format(device["DeviceName"], sensor_name) self._state = None - self._unique_id = "{}-{}".format(self._device_id, self._sensor_name) + self._unique_id = f"{self._device_id}-{self._sensor_name}" @property def unique_id(self): diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index cca57bcae82..18f60036397 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -72,7 +72,7 @@ class BloomSkySensor(Entity): self._sensor_name = sensor_name self._name = "{} {}".format(device["DeviceName"], sensor_name) self._state = None - self._unique_id = "{}-{}".format(self._device_id, self._sensor_name) + self._unique_id = f"{self._device_id}-{self._sensor_name}" @property def unique_id(self): @@ -103,6 +103,6 @@ class BloomSkySensor(Entity): state = self._bloomsky.devices[self._device_id]["Data"][self._sensor_name] if self._sensor_name in FORMAT_NUMBERS: - self._state = "{0:.2f}".format(state) + self._state = f"{state:.2f}" else: self._state = state diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index e5f264b5f73..bf0568aed16 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -336,7 +336,7 @@ class BluesoundPlayer(MediaPlayerDevice): if method[0] == "/": method = method[1:] - url = "http://{}:{}/{}".format(self.host, self.port, method) + url = f"http://{self.host}:{self.port}/{method}" _LOGGER.debug("Calling URL: %s", url) response = None @@ -380,8 +380,8 @@ class BluesoundPlayer(MediaPlayerDevice): etag = self._status.get("@etag", "") if etag != "": - url = "Status?etag={}&timeout=120.0".format(etag) - url = "http://{}:{}/{}".format(self.host, self.port, url) + url = f"Status?etag={etag}&timeout=120.0" + url = f"http://{self.host}:{self.port}/{url}" _LOGGER.debug("Calling URL: %s", url) @@ -595,7 +595,7 @@ class BluesoundPlayer(MediaPlayerDevice): if not url: return if url[0] == "/": - url = "http://{}:{}{}".format(self.host, self.port, url) + url = f"http://{self.host}:{self.port}{url}" return url @@ -843,13 +843,13 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_add_slave(self, slave_device): """Add slave to master.""" return await self.send_bluesound_command( - "/AddSlave?slave={}&port={}".format(slave_device.host, slave_device.port) + f"/AddSlave?slave={slave_device.host}&port={slave_device.port}" ) async def async_remove_slave(self, slave_device): """Remove slave to master.""" return await self.send_bluesound_command( - "/RemoveSlave?slave={}&port={}".format(slave_device.host, slave_device.port) + f"/RemoveSlave?slave={slave_device.host}&port={slave_device.port}" ) async def async_increase_timer(self): @@ -870,7 +870,7 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_set_shuffle(self, shuffle): """Enable or disable shuffle mode.""" value = "1" if shuffle else "0" - return await self.send_bluesound_command("/Shuffle?state={}".format(value)) + return await self.send_bluesound_command(f"/Shuffle?state={value}") async def async_select_source(self, source): """Select input source.""" @@ -967,7 +967,7 @@ class BluesoundPlayer(MediaPlayerDevice): if self.is_grouped and not self.is_master: return - url = "Play?url={}".format(media_id) + url = f"Play?url={media_id}" if kwargs.get(ATTR_MEDIA_ENQUEUE): return await self.send_bluesound_command(url) diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 65db87fa072..e760f91070a 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -54,7 +54,7 @@ def setup_scanner(hass, config, see, discovery_info=None): if rssi is not None: attributes["rssi"] = rssi see( - mac="{}{}".format(BT_PREFIX, mac), + mac=f"{BT_PREFIX}{mac}", host_name=name, attributes=attributes, source_type=SOURCE_TYPE_BLUETOOTH, diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index bdd91e6dfe1..ee4e1731156 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -147,7 +147,7 @@ class BME280Sensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 58b343b3de0..a36b35ea9d4 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -171,7 +171,7 @@ def _setup_bme680(config): sensor.select_gas_heater_profile(0) else: sensor.set_gas_status(bme680.DISABLE_GAS_MEAS) - except (RuntimeError, IOError): + except (RuntimeError, OSError): _LOGGER.error("BME680 sensor not detected at 0x%02x", i2c_address) return None @@ -331,7 +331,7 @@ class BME680Sensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index c257470bb2d..160c8a5e455 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -142,7 +142,7 @@ class BMWConnectedDriveAccount: self.account.update_vehicle_states() for listener in self._update_listeners: listener() - except IOError as exception: + except OSError as exception: _LOGGER.error( "Could not connect to the BMW Connected Drive portal. " "The vehicle state could not be updated." diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 418ccbabffe..c13de455984 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,7 +9,7 @@ from . import DOMAIN as BMW_DOMAIN _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "lids": ["Doors", "opening", "mdi:car-door"], + "lids": ["Doors", "opening", "mdi:car-door-lock"], "windows": ["Windows", "opening", "mdi:car-door"], "door_lock_state": ["Door lock state", "safety", "mdi:car-key"], "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], @@ -61,8 +61,8 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = "{} {}".format(self._vehicle.name, self._attribute) - self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) + self._name = f"{self._vehicle.name} {self._attribute}" + self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._sensor_name = sensor_name self._device_class = device_class self._icon = icon @@ -122,8 +122,9 @@ class BMWConnectedDriveSensor(BinarySensorDevice): for report in vehicle_state.condition_based_services: result.update(self._format_cbs_report(report)) elif self._attribute == "check_control_messages": - check_control_messages = vehicle_state.has_check_control_messages - if check_control_messages: + check_control_messages = vehicle_state.check_control_messages + has_check_control_messages = vehicle_state.has_check_control_messages + if has_check_control_messages: cbs_list = [] for message in check_control_messages: cbs_list.append(message["ccmDescriptionShort"]) @@ -177,18 +178,16 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def _format_cbs_report(self, report): result = {} service_type = report.service_type.lower().replace("_", " ") - result["{} status".format(service_type)] = report.state.value + result[f"{service_type} status"] = report.state.value if report.due_date is not None: - result["{} date".format(service_type)] = report.due_date.strftime( - "%Y-%m-%d" - ) + result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d") if report.due_distance is not None: distance = round( self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS) ) - result["{} distance".format(service_type)] = "{} {}".format( - distance, self.hass.config.units.length_unit - ) + result[ + f"{service_type} distance" + ] = f"{distance} {self.hass.config.units.length_unit}" return result def update_callback(self): diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index a16dbc6b341..2055b442dcd 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -30,8 +30,8 @@ class BMWLock(LockDevice): self._account = account self._vehicle = vehicle self._attribute = attribute - self._name = "{} {}".format(self._vehicle.name, self._attribute) - self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) + self._name = f"{self._vehicle.name} {self._attribute}" + self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._sensor_name = sensor_name self._state = None diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 8248ded4f8b..96d541b1955 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -17,10 +17,10 @@ _LOGGER = logging.getLogger(__name__) ATTR_TO_HA_METRIC = { "mileage": ["mdi:speedometer", LENGTH_KILOMETERS], - "remaining_range_total": ["mdi:ruler", LENGTH_KILOMETERS], - "remaining_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], - "remaining_range_fuel": ["mdi:ruler", LENGTH_KILOMETERS], - "max_range_electric": ["mdi:ruler", LENGTH_KILOMETERS], + "remaining_range_total": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_KILOMETERS], + "max_range_electric": ["mdi:map-marker-distance", LENGTH_KILOMETERS], "remaining_fuel": ["mdi:gas-station", VOLUME_LITERS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], @@ -28,10 +28,10 @@ ATTR_TO_HA_METRIC = { ATTR_TO_HA_IMPERIAL = { "mileage": ["mdi:speedometer", LENGTH_MILES], - "remaining_range_total": ["mdi:ruler", LENGTH_MILES], - "remaining_range_electric": ["mdi:ruler", LENGTH_MILES], - "remaining_range_fuel": ["mdi:ruler", LENGTH_MILES], - "max_range_electric": ["mdi:ruler", LENGTH_MILES], + "remaining_range_total": ["mdi:map-marker-distance", LENGTH_MILES], + "remaining_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], + "remaining_range_fuel": ["mdi:map-marker-distance", LENGTH_MILES], + "max_range_electric": ["mdi:map-marker-distance", LENGTH_MILES], "remaining_fuel": ["mdi:gas-station", VOLUME_GALLONS], "charging_time_remaining": ["mdi:update", "h"], "charging_status": ["mdi:battery-charging", None], @@ -68,8 +68,8 @@ class BMWConnectedDriveSensor(Entity): self._account = account self._attribute = attribute self._state = None - self._name = "{} {}".format(self._vehicle.name, self._attribute) - self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) + self._name = f"{self._vehicle.name} {self._attribute}" + self._unique_id = f"{self._vehicle.vin}-{self._attribute}" self._attribute_info = attribute_info @property diff --git a/homeassistant/components/bom/camera.py b/homeassistant/components/bom/camera.py index 3a5d6cdc503..f417cf769a4 100644 --- a/homeassistant/components/bom/camera.py +++ b/homeassistant/components/bom/camera.py @@ -84,7 +84,7 @@ def _validate_schema(config): LOCATIONS_MSG = "Set '{}' to one of: {}".format( CONF_LOCATION, ", ".join(sorted(LOCATIONS)) ) -XOR_MSG = "Specify exactly one of '{}' or '{}'".format(CONF_ID, CONF_LOCATION) +XOR_MSG = f"Specify exactly one of '{CONF_ID}' or '{CONF_LOCATION}'" PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( @@ -106,7 +106,7 @@ PLATFORM_SCHEMA = vol.All( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up BOM radar-loop camera component.""" location = config.get(CONF_LOCATION) or "ID {}".format(config.get(CONF_ID)) - name = config.get(CONF_NAME) or "BOM Radar Loop - {}".format(location) + name = config.get(CONF_NAME) or f"BOM Radar Loop - {location}" args = [ config.get(x) for x in (CONF_LOCATION, CONF_ID, CONF_DELTA, CONF_FRAMES, CONF_OUTFILE) diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index 790b2ddc74f..33444f10996 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -117,7 +117,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): CONF_WMO_ID, ) elif zone_id and wmo_id: - station = "{}.{}".format(zone_id, wmo_id) + station = f"{zone_id}.{wmo_id}" else: station = closest_station( config.get(CONF_LATITUDE), diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 5fb5af2732b..589da62feaa 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -64,7 +64,7 @@ def async_setup_service(hass, host, device): packet = await hass.async_add_executor_job(device.check_data) if packet: data = b64encode(packet).decode("utf8") - log_msg = "Received packet is: {}".format(data) + log_msg = f"Received packet is: {data}" _LOGGER.info(log_msg) hass.components.persistent_notification.async_create( log_msg, title="Broadlink switch" diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 277260c0336..d60331aaa44 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -103,9 +103,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def _get_mp1_slot_name(switch_friendly_name, slot): """Get slot name.""" - if not slots["slot_{}".format(slot)]: - return "{} slot {}".format(switch_friendly_name, slot) - return slots["slot_{}".format(slot)] + if not slots[f"slot_{slot}"]: + return f"{switch_friendly_name} slot {slot}" + return slots[f"slot_{slot}"] if switch_type in RM_TYPES: broadlink_device = broadlink.rm((ip_addr, 80), mac_addr, None) @@ -371,7 +371,7 @@ class BroadlinkMP1Switch: """Get status of outlet from cached status list.""" if self._states is None: return None - return self._states["s{}".format(slot)] + return self._states[f"s{slot}"] @Throttle(TIME_BETWEEN_UPDATES) def update(self): diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index 1db9e2beaf9..cdf202bbafd 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -81,13 +81,13 @@ class BuienradarCam(Camera): # invariant: this condition is private to and owned by this instance. self._condition = asyncio.Condition() - self._last_image = None # type: Optional[bytes] + self._last_image: Optional[bytes] = None # value of the last seen last modified header - self._last_modified = None # type: Optional[str] + self._last_modified: Optional[str] = None # loading status self._loading = False # deadline for image refresh - self.delta after last successful load - self._deadline = None # type: Optional[datetime] + self._deadline: Optional[datetime] = None @property def name(self) -> str: diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 841cc428bac..ef65db74f16 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -401,7 +401,7 @@ class BrSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 597d67fcdee..68cd1f51dda 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -134,7 +134,7 @@ async def async_request_stream(hass, entity_id, fmt): if not source: raise HomeAssistantError( - "{} does not support play stream service".format(camera.entity_id) + f"{camera.entity_id} does not support play stream service" ) return request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) @@ -534,9 +534,7 @@ class CameraMjpegStream(CameraView): # Compose camera stream from stills interval = float(request.query.get("interval")) if interval < MIN_STREAM_INTERVAL: - raise ValueError( - "Stream interval must be be > {}".format(MIN_STREAM_INTERVAL) - ) + raise ValueError(f"Stream interval must be be > {MIN_STREAM_INTERVAL}") return await camera.handle_async_still_stream(request, interval) except ValueError: raise web.HTTPBadRequest() @@ -588,7 +586,7 @@ async def ws_camera_stream(hass, connection, msg): if not source: raise HomeAssistantError( - "{} does not support play stream service".format(camera.entity_id) + f"{camera.entity_id} does not support play stream service" ) fmt = msg["format"] @@ -670,7 +668,7 @@ async def async_handle_play_stream_service(camera, service_call): if not source: raise HomeAssistantError( - "{} does not support play stream service".format(camera.entity_id) + f"{camera.entity_id} does not support play stream service" ) hass = camera.hass @@ -681,7 +679,7 @@ async def async_handle_play_stream_service(camera, service_call): url = request_stream(hass, source, fmt=fmt, keepalive=camera_prefs.preload_stream) data = { ATTR_ENTITY_ID: entity_ids, - ATTR_MEDIA_CONTENT_ID: "{}{}".format(hass.config.api.base_url, url), + ATTR_MEDIA_CONTENT_ID: f"{hass.config.api.base_url}{url}", ATTR_MEDIA_CONTENT_TYPE: FORMAT_CONTENT_TYPE[fmt], } @@ -696,9 +694,7 @@ async def async_handle_record_service(camera, call): source = await camera.stream_source() if not source: - raise HomeAssistantError( - "{} does not support record service".format(camera.entity_id) - ) + raise HomeAssistantError(f"{camera.entity_id} does not support record service") hass = camera.hass filename = call.data[CONF_FILENAME] diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index dcb54a772a3..6bb01c9d114 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -53,7 +53,7 @@ class CanarySensor(Entity): self._sensor_value = None sensor_type_name = sensor_type[0].replace("_", " ").title() - self._name = "{} {} {}".format(location.name, device.name, sensor_type_name) + self._name = f"{location.name} {device.name} {sensor_type_name}" @property def name(self): diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 32c744c8f20..71dee3afec5 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "no_devices_found": "Googgle Cast \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 Google Cast \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "\uad6c\uae00 \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 \uad6c\uae00 \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "step": { "confirm": { - "description": "Google Cast\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "Google Cast" + "description": "\uad6c\uae00 \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" } }, - "title": "Google Cast" + "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" } } \ No newline at end of file diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index cc112984f88..4dfb58ef3b7 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -1,6 +1,7 @@ """Component to embed Google Cast.""" from homeassistant import config_entries +from . import home_assistant_cast from .const import DOMAIN @@ -20,8 +21,10 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): +async def async_setup_entry(hass, entry: config_entries.ConfigEntry): """Set up Cast from a config entry.""" + await home_assistant_cast.async_setup_ha_cast(hass, entry) + hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "media_player") ) diff --git a/homeassistant/components/cast/const.py b/homeassistant/components/cast/const.py index e9f9ba4c39d..c6164484dbb 100644 --- a/homeassistant/components/cast/const.py +++ b/homeassistant/components/cast/const.py @@ -1,3 +1,26 @@ """Consts for Cast integration.""" DOMAIN = "cast" +DEFAULT_PORT = 8009 + +# Stores a threading.Lock that is held by the internal pychromecast discovery. +INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running" +# Stores all ChromecastInfo we encountered through discovery or config as a set +# If we find a chromecast with a new host, the old one will be removed again. +KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts" +# Stores UUIDs of cast devices that were added as entities. Doesn't store +# None UUIDs. +ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices" +# Stores an audio group manager. +CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager" + +# Dispatcher signal fired with a ChromecastInfo every time we discover a new +# Chromecast or receive it through configuration +SIGNAL_CAST_DISCOVERED = "cast_discovered" + +# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is +# removed +SIGNAL_CAST_REMOVED = "cast_removed" + +# Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view. +SIGNAL_HASS_CAST_SHOW_VIEW = "cast_show_view" diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py new file mode 100644 index 00000000000..d3097b3cc29 --- /dev/null +++ b/homeassistant/components/cast/discovery.py @@ -0,0 +1,99 @@ +"""Deal with Cast discovery.""" +import logging +import threading + +import pychromecast + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import ( + KNOWN_CHROMECAST_INFO_KEY, + SIGNAL_CAST_DISCOVERED, + INTERNAL_DISCOVERY_RUNNING_KEY, + SIGNAL_CAST_REMOVED, +) +from .helpers import ChromecastInfo, ChromeCastZeroconf + +_LOGGER = logging.getLogger(__name__) + + +def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo): + """Discover a Chromecast.""" + if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: + _LOGGER.debug("Discovered previous chromecast %s", info) + + # Either discovered completely new chromecast or a "moved" one. + info = info.fill_out_missing_chromecast_info() + _LOGGER.debug("Discovered chromecast %s", info) + + if info.uuid is not None: + # Remove previous cast infos with same uuid from known chromecasts. + same_uuid = set( + x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid + ) + hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid + + hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) + dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) + + +def _remove_chromecast(hass: HomeAssistant, info: ChromecastInfo): + # Removed chromecast + _LOGGER.debug("Removed chromecast %s", info) + + dispatcher_send(hass, SIGNAL_CAST_REMOVED, info) + + +def setup_internal_discovery(hass: HomeAssistant) -> None: + """Set up the pychromecast internal discovery.""" + if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() + + if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): + # Internal discovery is already running + return + + def internal_add_callback(name): + """Handle zeroconf discovery of a new chromecast.""" + mdns = listener.services[name] + discover_chromecast( + hass, + ChromecastInfo( + service=name, + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + ), + ) + + def internal_remove_callback(name, mdns): + """Handle zeroconf discovery of a removed chromecast.""" + _remove_chromecast( + hass, + ChromecastInfo( + service=name, + host=mdns[0], + port=mdns[1], + uuid=mdns[2], + model_name=mdns[3], + friendly_name=mdns[4], + ), + ) + + _LOGGER.debug("Starting internal pychromecast discovery.") + listener, browser = pychromecast.start_discovery( + internal_add_callback, internal_remove_callback + ) + ChromeCastZeroconf.set_zeroconf(browser.zc) + + def stop_discovery(event): + """Stop discovery of new chromecasts.""" + _LOGGER.debug("Stopping internal pychromecast discovery.") + pychromecast.stop_discovery(browser) + hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py new file mode 100644 index 00000000000..ea5c77ebc1a --- /dev/null +++ b/homeassistant/components/cast/helpers.py @@ -0,0 +1,246 @@ +"""Helpers to deal with Cast devices.""" +from typing import Optional, Tuple + +import attr +from pychromecast import dial + +from .const import DEFAULT_PORT + + +@attr.s(slots=True, frozen=True) +class ChromecastInfo: + """Class to hold all data about a chromecast for creating connections. + + This also has the same attributes as the mDNS fields by zeroconf. + """ + + host = attr.ib(type=str) + port = attr.ib(type=int) + service = attr.ib(type=Optional[str], default=None) + uuid = attr.ib( + type=Optional[str], converter=attr.converters.optional(str), default=None + ) # always convert UUID to string if not None + manufacturer = attr.ib(type=str, default="") + model_name = attr.ib(type=str, default="") + friendly_name = attr.ib(type=Optional[str], default=None) + is_dynamic_group = attr.ib(type=Optional[bool], default=None) + + @property + def is_audio_group(self) -> bool: + """Return if this is an audio group.""" + return self.port != DEFAULT_PORT + + @property + def is_information_complete(self) -> bool: + """Return if all information is filled out.""" + want_dynamic_group = self.is_audio_group + have_dynamic_group = self.is_dynamic_group is not None + have_all_except_dynamic_group = all( + attr.astuple( + self, + filter=attr.filters.exclude( + attr.fields(ChromecastInfo).is_dynamic_group + ), + ) + ) + return have_all_except_dynamic_group and ( + not want_dynamic_group or have_dynamic_group + ) + + @property + def host_port(self) -> Tuple[str, int]: + """Return the host+port tuple.""" + return self.host, self.port + + def fill_out_missing_chromecast_info(self) -> "ChromecastInfo": + """Return a new ChromecastInfo object with missing attributes filled in. + + Uses blocking HTTP. + """ + if self.is_information_complete: + # We have all information, no need to check HTTP API. Or this is an + # audio group, so checking via HTTP won't give us any new information. + return self + + # Fill out missing information via HTTP dial. + if self.is_audio_group: + is_dynamic_group = False + http_group_status = None + dynamic_groups = [] + if self.uuid: + http_group_status = dial.get_multizone_status( + self.host, + services=[self.service], + zconf=ChromeCastZeroconf.get_zeroconf(), + ) + if http_group_status is not None: + dynamic_groups = [ + str(g.uuid) for g in http_group_status.dynamic_groups + ] + is_dynamic_group = self.uuid in dynamic_groups + + return ChromecastInfo( + service=self.service, + host=self.host, + port=self.port, + uuid=self.uuid, + friendly_name=self.friendly_name, + manufacturer=self.manufacturer, + model_name=self.model_name, + is_dynamic_group=is_dynamic_group, + ) + + http_device_status = dial.get_device_status( + self.host, services=[self.service], zconf=ChromeCastZeroconf.get_zeroconf() + ) + if http_device_status is None: + # HTTP dial didn't give us any new information. + return self + + return ChromecastInfo( + service=self.service, + host=self.host, + port=self.port, + uuid=(self.uuid or http_device_status.uuid), + friendly_name=(self.friendly_name or http_device_status.friendly_name), + manufacturer=(self.manufacturer or http_device_status.manufacturer), + model_name=(self.model_name or http_device_status.model_name), + ) + + def same_dynamic_group(self, other: "ChromecastInfo") -> bool: + """Test chromecast info is same dynamic group.""" + return ( + self.is_audio_group + and other.is_dynamic_group + and self.friendly_name == other.friendly_name + ) + + +class ChromeCastZeroconf: + """Class to hold a zeroconf instance.""" + + __zconf = None + + @classmethod + def set_zeroconf(cls, zconf): + """Set zeroconf.""" + cls.__zconf = zconf + + @classmethod + def get_zeroconf(cls): + """Get zeroconf.""" + return cls.__zconf + + +class CastStatusListener: + """Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__(self, cast_device, chromecast, mz_mgr): + """Initialize the status listener.""" + self._cast_device = cast_device + self._uuid = chromecast.uuid + self._valid = True + self._mz_mgr = mz_mgr + + chromecast.register_status_listener(self) + chromecast.socket_client.media_controller.register_status_listener(self) + chromecast.register_connection_listener(self) + # pylint: disable=protected-access + if cast_device._cast_info.is_audio_group: + self._mz_mgr.add_multizone(chromecast) + else: + self._mz_mgr.register_listener(chromecast.uuid, self) + + def new_cast_status(self, cast_status): + """Handle reception of a new CastStatus.""" + if self._valid: + self._cast_device.new_cast_status(cast_status) + + def new_media_status(self, media_status): + """Handle reception of a new MediaStatus.""" + if self._valid: + self._cast_device.new_media_status(media_status) + + def new_connection_status(self, connection_status): + """Handle reception of a new ConnectionStatus.""" + if self._valid: + self._cast_device.new_connection_status(connection_status) + + @staticmethod + def added_to_multizone(group_uuid): + """Handle the cast added to a group.""" + pass + + def removed_from_multizone(self, group_uuid): + """Handle the cast removed from a group.""" + if self._valid: + self._cast_device.multizone_new_media_status(group_uuid, None) + + def multizone_new_cast_status(self, group_uuid, cast_status): + """Handle reception of a new CastStatus for a group.""" + pass + + def multizone_new_media_status(self, group_uuid, media_status): + """Handle reception of a new MediaStatus for a group.""" + if self._valid: + self._cast_device.multizone_new_media_status(group_uuid, media_status) + + def invalidate(self): + """Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + # pylint: disable=protected-access + if self._cast_device._cast_info.is_audio_group: + self._mz_mgr.remove_multizone(self._uuid) + else: + self._mz_mgr.deregister_listener(self._uuid, self) + self._valid = False + + +class DynamicGroupCastStatusListener: + """Helper class to handle pychromecast status callbacks. + + Necessary because a CastDevice entity can create a new socket client + and therefore callbacks from multiple chromecast connections can + potentially arrive. This class allows invalidating past chromecast objects. + """ + + def __init__(self, cast_device, chromecast, mz_mgr): + """Initialize the status listener.""" + self._cast_device = cast_device + self._uuid = chromecast.uuid + self._valid = True + self._mz_mgr = mz_mgr + + chromecast.register_status_listener(self) + chromecast.socket_client.media_controller.register_status_listener(self) + chromecast.register_connection_listener(self) + self._mz_mgr.add_multizone(chromecast) + + def new_cast_status(self, cast_status): + """Handle reception of a new CastStatus.""" + pass + + def new_media_status(self, media_status): + """Handle reception of a new MediaStatus.""" + if self._valid: + self._cast_device.new_dynamic_group_media_status(media_status) + + def new_connection_status(self, connection_status): + """Handle reception of a new ConnectionStatus.""" + if self._valid: + self._cast_device.new_dynamic_group_connection_status(connection_status) + + def invalidate(self): + """Invalidate this status listener. + + All following callbacks won't be forwarded. + """ + self._mz_mgr.remove_multizone(self._uuid) + self._valid = False diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py new file mode 100644 index 00000000000..d5d35ba7c9f --- /dev/null +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -0,0 +1,74 @@ +"""Home Assistant Cast integration for Cast.""" +from typing import Optional + +import voluptuous as vol + +from pychromecast.controllers.homeassistant import HomeAssistantController + +from homeassistant import auth, config_entries, core +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.helpers import config_validation as cv, dispatcher + +from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW + +SERVICE_SHOW_VIEW = "show_lovelace_view" +ATTR_VIEW_PATH = "view_path" + + +async def async_setup_ha_cast( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): + """Set up Home Assistant Cast.""" + user_id: Optional[str] = entry.data.get("user_id") + user: Optional[auth.models.User] = None + + if user_id is not None: + user = await hass.auth.async_get_user(user_id) + + if user is None: + user = await hass.auth.async_create_system_user( + "Home Assistant Cast", [auth.GROUP_ID_ADMIN] + ) + hass.config_entries.async_update_entry( + entry, data={**entry.data, "user_id": user.id} + ) + + if user.refresh_tokens: + refresh_token: auth.models.RefreshToken = list(user.refresh_tokens.values())[0] + else: + refresh_token = await hass.auth.async_create_refresh_token(user) + + async def handle_show_view(call: core.ServiceCall): + """Handle a Show View service call.""" + hass_url = hass.config.api.base_url + + # Home Assistant Cast only works with https urls. If user has no configured + # base url, use their remote url. + if not hass_url.lower().startswith("https://"): + try: + hass_url = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass + + controller = HomeAssistantController( + # If you are developing Home Assistant Cast, uncomment and set to your dev app id. + # app_id="5FE44367", + hass_url=hass_url, + client_id=None, + refresh_token=refresh_token.token, + ) + + dispatcher.async_dispatcher_send( + hass, + SIGNAL_HASS_CAST_SHOW_VIEW, + controller, + call.data[ATTR_ENTITY_ID], + call.data[ATTR_VIEW_PATH], + ) + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_SHOW_VIEW, + handle_show_view, + vol.Schema({ATTR_ENTITY_ID: cv.entity_id, ATTR_VIEW_PATH: str}), + ) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index ff9e8907ec5..84a6a6e2934 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,9 +3,7 @@ "name": "Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/components/cast", - "requirements": [ - "pychromecast==3.2.2" - ], + "requirements": ["pychromecast==4.0.1"], "dependencies": [], "zeroconf": ["_googlecast._tcp.local."], "codeowners": [] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index af9f39f8ed4..c2d847fd09b 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -1,10 +1,15 @@ """Provide functionality to interact with Cast devices on the network.""" import asyncio import logging -import threading -from typing import Optional, Tuple +from typing import Optional -import attr +import pychromecast +from pychromecast.socket_client import ( + CONNECTION_STATUS_CONNECTED, + CONNECTION_STATUS_DISCONNECTED, +) +from pychromecast.controllers.multizone import MultizoneManager +from pychromecast.controllers.homeassistant import HomeAssistantController import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice @@ -35,22 +40,34 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro -from . import DOMAIN as CAST_DOMAIN - -DEPENDENCIES = ("cast",) +from .const import ( + DOMAIN as CAST_DOMAIN, + ADDED_CAST_DEVICES_KEY, + SIGNAL_CAST_DISCOVERED, + KNOWN_CHROMECAST_INFO_KEY, + CAST_MULTIZONE_MANAGER_KEY, + DEFAULT_PORT, + SIGNAL_CAST_REMOVED, + SIGNAL_HASS_CAST_SHOW_VIEW, +) +from .helpers import ( + ChromecastInfo, + CastStatusListener, + DynamicGroupCastStatusListener, + ChromeCastZeroconf, +) +from .discovery import setup_internal_discovery, discover_chromecast _LOGGER = logging.getLogger(__name__) CONF_IGNORE_CEC = "ignore_cec" CAST_SPLASH = "https://home-assistant.io/images/cast/splash.png" -DEFAULT_PORT = 8009 - SUPPORT_CAST = ( SUPPORT_PAUSE | SUPPORT_PLAY @@ -62,24 +79,6 @@ SUPPORT_CAST = ( | SUPPORT_VOLUME_SET ) -# Stores a threading.Lock that is held by the internal pychromecast discovery. -INTERNAL_DISCOVERY_RUNNING_KEY = "cast_discovery_running" -# Stores all ChromecastInfo we encountered through discovery or config as a set -# If we find a chromecast with a new host, the old one will be removed again. -KNOWN_CHROMECAST_INFO_KEY = "cast_known_chromecasts" -# Stores UUIDs of cast devices that were added as entities. Doesn't store -# None UUIDs. -ADDED_CAST_DEVICES_KEY = "cast_added_cast_devices" -# Stores an audio group manager. -CAST_MULTIZONE_MANAGER_KEY = "cast_multizone_manager" - -# Dispatcher signal fired with a ChromecastInfo every time we discover a new -# Chromecast or receive it through configuration -SIGNAL_CAST_DISCOVERED = "cast_discovered" - -# Dispatcher signal fired with a ChromecastInfo every time a Chromecast is -# removed -SIGNAL_CAST_REMOVED = "cast_removed" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -89,212 +88,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -@attr.s(slots=True, frozen=True) -class ChromecastInfo: - """Class to hold all data about a chromecast for creating connections. - - This also has the same attributes as the mDNS fields by zeroconf. - """ - - host = attr.ib(type=str) - port = attr.ib(type=int) - service = attr.ib(type=Optional[str], default=None) - uuid = attr.ib( - type=Optional[str], converter=attr.converters.optional(str), default=None - ) # always convert UUID to string if not None - manufacturer = attr.ib(type=str, default="") - model_name = attr.ib(type=str, default="") - friendly_name = attr.ib(type=Optional[str], default=None) - is_dynamic_group = attr.ib(type=Optional[bool], default=None) - - @property - def is_audio_group(self) -> bool: - """Return if this is an audio group.""" - return self.port != DEFAULT_PORT - - @property - def is_information_complete(self) -> bool: - """Return if all information is filled out.""" - want_dynamic_group = self.is_audio_group - have_dynamic_group = self.is_dynamic_group is not None - have_all_except_dynamic_group = all( - attr.astuple( - self, - filter=attr.filters.exclude( - attr.fields(ChromecastInfo).is_dynamic_group - ), - ) - ) - return have_all_except_dynamic_group and ( - not want_dynamic_group or have_dynamic_group - ) - - @property - def host_port(self) -> Tuple[str, int]: - """Return the host+port tuple.""" - return self.host, self.port - - -def _is_matching_dynamic_group( - our_info: ChromecastInfo, new_info: ChromecastInfo -) -> bool: - return ( - our_info.is_audio_group - and new_info.is_dynamic_group - and our_info.friendly_name == new_info.friendly_name - ) - - -def _fill_out_missing_chromecast_info(info: ChromecastInfo) -> ChromecastInfo: - """Fill out missing attributes of ChromecastInfo using blocking HTTP.""" - if info.is_information_complete: - # We have all information, no need to check HTTP API. Or this is an - # audio group, so checking via HTTP won't give us any new information. - return info - - # Fill out missing information via HTTP dial. - from pychromecast import dial - - if info.is_audio_group: - is_dynamic_group = False - http_group_status = None - dynamic_groups = [] - if info.uuid: - http_group_status = dial.get_multizone_status( - info.host, - services=[info.service], - zconf=ChromeCastZeroconf.get_zeroconf(), - ) - if http_group_status is not None: - dynamic_groups = [str(g.uuid) for g in http_group_status.dynamic_groups] - is_dynamic_group = info.uuid in dynamic_groups - - return ChromecastInfo( - service=info.service, - host=info.host, - port=info.port, - uuid=info.uuid, - friendly_name=info.friendly_name, - manufacturer=info.manufacturer, - model_name=info.model_name, - is_dynamic_group=is_dynamic_group, - ) - - http_device_status = dial.get_device_status( - info.host, services=[info.service], zconf=ChromeCastZeroconf.get_zeroconf() - ) - if http_device_status is None: - # HTTP dial didn't give us any new information. - return info - - return ChromecastInfo( - service=info.service, - host=info.host, - port=info.port, - uuid=(info.uuid or http_device_status.uuid), - friendly_name=(info.friendly_name or http_device_status.friendly_name), - manufacturer=(info.manufacturer or http_device_status.manufacturer), - model_name=(info.model_name or http_device_status.model_name), - ) - - -def _discover_chromecast(hass: HomeAssistantType, info: ChromecastInfo): - if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: - _LOGGER.debug("Discovered previous chromecast %s", info) - - # Either discovered completely new chromecast or a "moved" one. - info = _fill_out_missing_chromecast_info(info) - _LOGGER.debug("Discovered chromecast %s", info) - - if info.uuid is not None: - # Remove previous cast infos with same uuid from known chromecasts. - same_uuid = set( - x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid - ) - hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid - - hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) - dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) - - -def _remove_chromecast(hass: HomeAssistantType, info: ChromecastInfo): - # Removed chromecast - _LOGGER.debug("Removed chromecast %s", info) - - dispatcher_send(hass, SIGNAL_CAST_REMOVED, info) - - -class ChromeCastZeroconf: - """Class to hold a zeroconf instance.""" - - __zconf = None - - @classmethod - def set_zeroconf(cls, zconf): - """Set zeroconf.""" - cls.__zconf = zconf - - @classmethod - def get_zeroconf(cls): - """Get zeroconf.""" - return cls.__zconf - - -def _setup_internal_discovery(hass: HomeAssistantType) -> None: - """Set up the pychromecast internal discovery.""" - if INTERNAL_DISCOVERY_RUNNING_KEY not in hass.data: - hass.data[INTERNAL_DISCOVERY_RUNNING_KEY] = threading.Lock() - - if not hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].acquire(blocking=False): - # Internal discovery is already running - return - - import pychromecast - - def internal_add_callback(name): - """Handle zeroconf discovery of a new chromecast.""" - mdns = listener.services[name] - _discover_chromecast( - hass, - ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], - ), - ) - - def internal_remove_callback(name, mdns): - """Handle zeroconf discovery of a removed chromecast.""" - _remove_chromecast( - hass, - ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], - ), - ) - - _LOGGER.debug("Starting internal pychromecast discovery.") - listener, browser = pychromecast.start_discovery( - internal_add_callback, internal_remove_callback - ) - ChromeCastZeroconf.set_zeroconf(browser.zc) - - def stop_discovery(event): - """Stop discovery of new chromecasts.""" - _LOGGER.debug("Stopping internal pychromecast discovery.") - pychromecast.stop_discovery(browser) - hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() - - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) - - @callback def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): """Create a CastDevice Entity from the chromecast object. @@ -357,8 +150,6 @@ async def _async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info ): """Set up the cast platform.""" - import pychromecast - # Import CEC IGNORE attributes pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) @@ -390,9 +181,9 @@ async def _async_setup_platform( if info is None or info.is_audio_group: # If we were a) explicitly told to enable discovery or # b) have an audio group cast device, we need internal discovery. - hass.async_add_job(_setup_internal_discovery, hass) + hass.async_add_executor_job(setup_internal_discovery, hass) else: - info = await hass.async_add_job(_fill_out_missing_chromecast_info, info) + info = await hass.async_add_executor_job(info.fill_out_missing_chromecast_info) if info.friendly_name is None: _LOGGER.debug( "Cannot retrieve detail information for chromecast" @@ -400,121 +191,7 @@ async def _async_setup_platform( info, ) - hass.async_add_job(_discover_chromecast, hass, info) - - -class CastStatusListener: - """Helper class to handle pychromecast status callbacks. - - Necessary because a CastDevice entity can create a new socket client - and therefore callbacks from multiple chromecast connections can - potentially arrive. This class allows invalidating past chromecast objects. - """ - - def __init__(self, cast_device, chromecast, mz_mgr): - """Initialize the status listener.""" - self._cast_device = cast_device - self._uuid = chromecast.uuid - self._valid = True - self._mz_mgr = mz_mgr - - chromecast.register_status_listener(self) - chromecast.socket_client.media_controller.register_status_listener(self) - chromecast.register_connection_listener(self) - # pylint: disable=protected-access - if cast_device._cast_info.is_audio_group: - self._mz_mgr.add_multizone(chromecast) - else: - self._mz_mgr.register_listener(chromecast.uuid, self) - - def new_cast_status(self, cast_status): - """Handle reception of a new CastStatus.""" - if self._valid: - self._cast_device.new_cast_status(cast_status) - - def new_media_status(self, media_status): - """Handle reception of a new MediaStatus.""" - if self._valid: - self._cast_device.new_media_status(media_status) - - def new_connection_status(self, connection_status): - """Handle reception of a new ConnectionStatus.""" - if self._valid: - self._cast_device.new_connection_status(connection_status) - - @staticmethod - def added_to_multizone(group_uuid): - """Handle the cast added to a group.""" - pass - - def removed_from_multizone(self, group_uuid): - """Handle the cast removed from a group.""" - if self._valid: - self._cast_device.multizone_new_media_status(group_uuid, None) - - def multizone_new_cast_status(self, group_uuid, cast_status): - """Handle reception of a new CastStatus for a group.""" - pass - - def multizone_new_media_status(self, group_uuid, media_status): - """Handle reception of a new MediaStatus for a group.""" - if self._valid: - self._cast_device.multizone_new_media_status(group_uuid, media_status) - - def invalidate(self): - """Invalidate this status listener. - - All following callbacks won't be forwarded. - """ - # pylint: disable=protected-access - if self._cast_device._cast_info.is_audio_group: - self._mz_mgr.remove_multizone(self._uuid) - else: - self._mz_mgr.deregister_listener(self._uuid, self) - self._valid = False - - -class DynamicGroupCastStatusListener: - """Helper class to handle pychromecast status callbacks. - - Necessary because a CastDevice entity can create a new socket client - and therefore callbacks from multiple chromecast connections can - potentially arrive. This class allows invalidating past chromecast objects. - """ - - def __init__(self, cast_device, chromecast, mz_mgr): - """Initialize the status listener.""" - self._cast_device = cast_device - self._uuid = chromecast.uuid - self._valid = True - self._mz_mgr = mz_mgr - - chromecast.register_status_listener(self) - chromecast.socket_client.media_controller.register_status_listener(self) - chromecast.register_connection_listener(self) - self._mz_mgr.add_multizone(chromecast) - - def new_cast_status(self, cast_status): - """Handle reception of a new CastStatus.""" - pass - - def new_media_status(self, media_status): - """Handle reception of a new MediaStatus.""" - if self._valid: - self._cast_device.new_dynamic_group_media_status(media_status) - - def new_connection_status(self, connection_status): - """Handle reception of a new ConnectionStatus.""" - if self._valid: - self._cast_device.new_dynamic_group_connection_status(connection_status) - - def invalidate(self): - """Invalidate this status listener. - - All following callbacks won't be forwarded. - """ - self._mz_mgr.remove_multizone(self._uuid) - self._valid = False + hass.async_add_executor_job(discover_chromecast, hass, info) class CastDevice(MediaPlayerDevice): @@ -525,106 +202,51 @@ class CastDevice(MediaPlayerDevice): "elected leader" itself. """ - def __init__(self, cast_info): + def __init__(self, cast_info: ChromecastInfo): """Initialize the cast device.""" - import pychromecast # noqa: pylint: disable=unused-import - self._cast_info = cast_info # type: ChromecastInfo + self._cast_info = cast_info self.services = None if cast_info.service: self.services = set() self.services.add(cast_info.service) - self._chromecast = None # type: Optional[pychromecast.Chromecast] + self._chromecast: Optional[pychromecast.Chromecast] = None self.cast_status = None self.media_status = None self.media_status_received = None - self._dynamic_group_cast_info = None # type: ChromecastInfo - self._dynamic_group_cast = None # type: Optional[pychromecast.Chromecast] + self._dynamic_group_cast_info: ChromecastInfo = None + self._dynamic_group_cast: Optional[pychromecast.Chromecast] = None self.dynamic_group_media_status = None self.dynamic_group_media_status_received = None self.mz_media_status = {} self.mz_media_status_received = {} self.mz_mgr = None - self._available = False # type: bool - self._dynamic_group_available = False # type: bool - self._status_listener = None # type: Optional[CastStatusListener] - self._dynamic_group_status_listener = ( - None - ) # type: Optional[DynamicGroupCastStatusListener] + self._available = False + self._dynamic_group_available = False + self._status_listener: Optional[CastStatusListener] = None + self._dynamic_group_status_listener: Optional[ + DynamicGroupCastStatusListener + ] = None + self._hass_cast_controller: Optional[HomeAssistantController] = None + self._add_remove_handler = None self._del_remove_handler = None + self._cast_view_remove_handler = None async def async_added_to_hass(self): """Create chromecast object when added to hass.""" - - @callback - def async_cast_discovered(discover: ChromecastInfo): - """Handle discovery of new Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - if _is_matching_dynamic_group(self._cast_info, discover): - _LOGGER.debug("Discovered matching dynamic group: %s", discover) - self.hass.async_create_task( - async_create_catching_coro(self.async_set_dynamic_group(discover)) - ) - return - - if self._cast_info.uuid != discover.uuid: - # Discovered is not our device. - return - if self.services is None: - _LOGGER.warning( - "[%s %s (%s:%s)] Received update for manually added Cast", - self.entity_id, - self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, - ) - return - _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - self.hass.async_create_task( - async_create_catching_coro(self.async_set_cast_info(discover)) - ) - - def async_cast_removed(discover: ChromecastInfo): - """Handle removal of Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - if ( - self._dynamic_group_cast_info is not None - and self._dynamic_group_cast_info.uuid == discover.uuid - ): - _LOGGER.debug("Removed matching dynamic group: %s", discover) - self.hass.async_create_task( - async_create_catching_coro(self.async_del_dynamic_group()) - ) - return - if self._cast_info.uuid != discover.uuid: - # Removed is not our device. - return - _LOGGER.debug("Removed chromecast with same UUID: %s", discover) - self.hass.async_create_task( - async_create_catching_coro(self.async_del_cast_info(discover)) - ) - - async def async_stop(event): - """Disconnect socket on Home Assistant stop.""" - await self._async_disconnect() - self._add_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered + self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered ) self._del_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_REMOVED, async_cast_removed + self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed ) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.hass.async_create_task( async_create_catching_coro(self.async_set_cast_info(self._cast_info)) ) for info in self.hass.data[KNOWN_CHROMECAST_INFO_KEY]: - if _is_matching_dynamic_group(self._cast_info, info): + if self._cast_info.same_dynamic_group(info): _LOGGER.debug( "[%s %s (%s:%s)] Found dynamic group: %s", self.entity_id, @@ -638,6 +260,10 @@ class CastDevice(MediaPlayerDevice): ) break + self._cast_view_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view + ) + async def async_will_remove_from_hass(self) -> None: """Disconnect Chromecast object when removed.""" await self._async_disconnect() @@ -647,12 +273,16 @@ class CastDevice(MediaPlayerDevice): self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) if self._add_remove_handler: self._add_remove_handler() + self._add_remove_handler = None if self._del_remove_handler: self._del_remove_handler() + self._del_remove_handler = None + if self._cast_view_remove_handler: + self._cast_view_remove_handler() + self._cast_view_remove_handler = None async def async_set_cast_info(self, cast_info): """Set the cast information and set up the chromecast object.""" - import pychromecast self._cast_info = cast_info @@ -717,9 +347,8 @@ class CastDevice(MediaPlayerDevice): self._chromecast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: - from pychromecast.controllers.multizone import MultizoneManager - self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() + self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr) @@ -744,7 +373,6 @@ class CastDevice(MediaPlayerDevice): async def async_set_dynamic_group(self, cast_info): """Set the cast information and set up the chromecast object.""" - import pychromecast _LOGGER.debug( "[%s %s (%s:%s)] Connecting to dynamic group by host %s", @@ -773,9 +401,8 @@ class CastDevice(MediaPlayerDevice): self._dynamic_group_cast = chromecast if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: - from pychromecast.controllers.multizone import MultizoneManager - self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() + mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] self._dynamic_group_status_listener = DynamicGroupCastStatusListener( @@ -839,6 +466,7 @@ class CastDevice(MediaPlayerDevice): self.mz_media_status = {} self.mz_media_status_received = {} self.mz_mgr = None + self._hass_cast_controller = None if self._status_listener is not None: self._status_listener.invalidate() self._status_listener = None @@ -866,11 +494,6 @@ class CastDevice(MediaPlayerDevice): def new_connection_status(self, connection_status): """Handle updates of connection status.""" - from pychromecast.socket_client import ( - CONNECTION_STATUS_CONNECTED, - CONNECTION_STATUS_DISCONNECTED, - ) - _LOGGER.debug( "[%s %s (%s:%s)] Received cast device connection status: %s", self.entity_id, @@ -901,7 +524,7 @@ class CastDevice(MediaPlayerDevice): info = self._cast_info if info.friendly_name is None and not info.is_audio_group: # We couldn't find friendly_name when the cast was added, retry - self._cast_info = _fill_out_missing_chromecast_info(info) + self._cast_info = info.fill_out_missing_chromecast_info() self._available = new_available self.schedule_update_ha_state() @@ -913,11 +536,6 @@ class CastDevice(MediaPlayerDevice): def new_dynamic_group_connection_status(self, connection_status): """Handle updates of connection status.""" - from pychromecast.socket_client import ( - CONNECTION_STATUS_CONNECTED, - CONNECTION_STATUS_DISCONNECTED, - ) - _LOGGER.debug( "[%s %s (%s:%s)] Received dynamic group connection status: %s", self.entity_id, @@ -991,7 +609,6 @@ class CastDevice(MediaPlayerDevice): def turn_on(self): """Turn on the cast device.""" - import pychromecast if not self._chromecast.is_idle: # Already turned on @@ -1276,3 +893,69 @@ class CastDevice(MediaPlayerDevice): def unique_id(self) -> Optional[str]: """Return a unique ID.""" return self._cast_info.uuid + + async def _async_cast_discovered(self, discover: ChromecastInfo): + """Handle discovery of new Chromecast.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + + if self._cast_info.same_dynamic_group(discover): + _LOGGER.debug("Discovered matching dynamic group: %s", discover) + await self.async_set_dynamic_group(discover) + return + + if self._cast_info.uuid != discover.uuid: + # Discovered is not our device. + return + + if self.services is None: + _LOGGER.warning( + "[%s %s (%s:%s)] Received update for manually added Cast", + self.entity_id, + self._cast_info.friendly_name, + self._cast_info.host, + self._cast_info.port, + ) + return + + _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) + await self.async_set_cast_info(discover) + + async def _async_cast_removed(self, discover: ChromecastInfo): + """Handle removal of Chromecast.""" + if self._cast_info.uuid is None: + # We can't handle empty UUIDs + return + + if ( + self._dynamic_group_cast_info is not None + and self._dynamic_group_cast_info.uuid == discover.uuid + ): + _LOGGER.debug("Removed matching dynamic group: %s", discover) + await self.async_del_dynamic_group() + return + + if self._cast_info.uuid != discover.uuid: + # Removed is not our device. + return + + _LOGGER.debug("Removed chromecast with same UUID: %s", discover) + await self.async_del_cast_info(discover) + + async def _async_stop(self, event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() + + def _handle_signal_show_view( + self, controller: HomeAssistantController, entity_id: str, view_path: str + ): + """Handle a show view signal.""" + if entity_id != self.entity_id: + return + + if self._hass_cast_controller is None: + self._hass_cast_controller = controller + self._chromecast.register_handler(controller) + + self._hass_cast_controller.show_lovelace_view(view_path) diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml new file mode 100644 index 00000000000..24bc7b16a90 --- /dev/null +++ b/homeassistant/components/cast/services.yaml @@ -0,0 +1,9 @@ +show_lovelace_view: + description: Show a Lovelace view on a Chromecast. + fields: + entity_id: + description: Media Player entity to show the Lovelace view on. + example: "media_player.kitchen" + view_path: + description: The path of the Lovelace view to show. + example: downstairs diff --git a/homeassistant/components/cert_expiry/.translations/ca.json b/homeassistant/components/cert_expiry/.translations/ca.json new file mode 100644 index 00000000000..25c0b26fafc --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada" + }, + "error": { + "certificate_fetch_failed": "No s'ha pogut obtenir el certificat des d'aquesta combinaci\u00f3 d'amfitri\u00f3 i port", + "connection_timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 amb l'amfitri\u00f3.", + "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", + "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Nom d'amfitri\u00f3 del certificat", + "name": "Nom del certificat", + "port": "Port del certificat" + }, + "title": "Configuraci\u00f3 del certificat a provar" + } + }, + "title": "Caducitat del certificat" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json new file mode 100644 index 00000000000..667ab5fa4e3 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret" + }, + "error": { + "certificate_fetch_failed": "Kan ikke hente certifikat fra denne v\u00e6rt- og portkombination", + "connection_timeout": "Timeout ved tilslutning til denne v\u00e6rt", + "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret", + "resolve_failed": "V\u00e6rten kunne ikke findes" + }, + "step": { + "user": { + "data": { + "host": "Certifikatets v\u00e6rtsnavn", + "name": "Certifikatets navn", + "port": "Certifikatets port" + }, + "title": "Definer certifikatet, der skal testes" + } + }, + "title": "Certifikat udl\u00f8b" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/de.json b/homeassistant/components/cert_expiry/.translations/de.json new file mode 100644 index 00000000000..344abe13067 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert." + }, + "error": { + "certificate_fetch_failed": "Zertifikat kann von dieser Kombination aus Host und Port nicht abgerufen werden", + "connection_timeout": "Zeit\u00fcberschreitung beim Herstellen einer Verbindung mit diesem Host", + "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert.", + "resolve_failed": "Dieser Host kann nicht aufgel\u00f6st werden" + }, + "step": { + "user": { + "data": { + "host": "Der Hostname des Zertifikats", + "name": "Der Name des Zertifikats", + "port": "Der Port des Zertifikats" + }, + "title": "Definieren Sie das zu testende Zertifikat" + } + }, + "title": "Zertifikatsablauf" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json new file mode 100644 index 00000000000..85575df6291 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "This host and port combination is already configured" + }, + "error": { + "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", + "connection_timeout": "Timeout whemn connecting to this host", + "host_port_exists": "This host and port combination is already configured", + "resolve_failed": "This host can not be resolved" + }, + "step": { + "user": { + "data": { + "host": "The hostname of the certificate", + "name": "The name of the certificate", + "port": "The port of the certificate" + }, + "title": "Define the certificate to test" + } + }, + "title": "Certificate Expiry" + } +} diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json new file mode 100644 index 00000000000..2cb0bd9af16 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + }, + "error": { + "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", + "connection_timeout": "Tiempo de espera agotado al conectar con el dispositivo.", + "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" + }, + "step": { + "user": { + "data": { + "host": "El nombre de host del certificado", + "name": "El nombre del certificado", + "port": "El puerto del certificado" + } + } + }, + "title": "Caducidad del certificado" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/hu.json b/homeassistant/components/cert_expiry/.translations/hu.json new file mode 100644 index 00000000000..584f4c2b759 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "A tan\u00fas\u00edtv\u00e1ny neve", + "port": "A tan\u00fas\u00edtv\u00e1ny portja" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json new file mode 100644 index 00000000000..9135ed3b478 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata" + }, + "error": { + "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", + "connection_timeout": "Tempo scaduto durante la connessione a questo host", + "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", + "resolve_failed": "Questo host non pu\u00f2 essere risolto" + }, + "step": { + "user": { + "data": { + "host": "L'hostname del certificato", + "name": "Il nome del certificato", + "port": "La porta del certificato" + }, + "title": "Definire il certificato da testare" + } + }, + "title": "Scadenza certificato" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/ko.json b/homeassistant/components/cert_expiry/.translations/ko.json new file mode 100644 index 00000000000..a807d32a6fb --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/ko.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", + "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\uc778\uc99d\uc11c\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984", + "name": "\uc778\uc99d\uc11c\uc758 \uc774\ub984", + "port": "\uc778\uc99d\uc11c\uc758 \ud3ec\ud2b8" + }, + "title": "\uc778\uc99d\uc11c \uc815\uc758 \ud14c\uc2a4\ud2b8 \ub300\uc0c1" + } + }, + "title": "\uc778\uc99d\uc11c \ub9cc\ub8cc" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json new file mode 100644 index 00000000000..d2fe3c76e85 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/nl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd" + }, + "error": { + "certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort", + "connection_timeout": "Timeout bij verbinding maken met deze host", + "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd", + "resolve_failed": "Deze host kon niet gevonden worden" + }, + "step": { + "user": { + "data": { + "host": "De hostnaam van het certificaat", + "name": "De naam van het certificaat", + "port": "De poort van het certificaat" + }, + "title": "Het certificaat defini\u00ebren dat moet worden getest" + } + }, + "title": "Vervaldatum certificaat" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json new file mode 100644 index 00000000000..e095cc360a0 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert" + }, + "error": { + "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", + "connection_timeout": "Timeout n\u00e5r det kobles til denne verten", + "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", + "resolve_failed": "Denne verten kan ikke l\u00f8ses" + }, + "step": { + "user": { + "data": { + "host": "Sertifikatets vertsnavn", + "name": "Sertifikatets navn", + "port": "Sertifikatets port" + }, + "title": "Definer sertifikatet som skal testes" + } + }, + "title": "Sertifikat utl\u00f8p" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json new file mode 100644 index 00000000000..162c8bf8a0a --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana" + }, + "error": { + "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", + "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z tym hostem", + "host_port_exists": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", + "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta certyfikatu", + "name": "Nazwa certyfikatu", + "port": "Port certyfikatu" + }, + "title": "Zdefiniuj certyfikat do przetestowania" + } + }, + "title": "Wa\u017cno\u015b\u0107 certyfikatu" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json new file mode 100644 index 00000000000..6a795dee13e --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430" + }, + "error": { + "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", + "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", + "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" + }, + "step": { + "user": { + "data": { + "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", + "port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + }, + "title": "C\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + } + }, + "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json new file mode 100644 index 00000000000..c088e414c73 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana" + }, + "error": { + "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", + "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem", + "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", + "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" + }, + "step": { + "user": { + "data": { + "host": "Ime gostitelja potrdila", + "name": "Ime potrdila", + "port": "Vrata potrdila" + }, + "title": "Dolo\u010dite potrdilo za testiranje" + } + }, + "title": "Veljavnost certifikata" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hant.json b/homeassistant/components/cert_expiry/.translations/zh-Hant.json new file mode 100644 index 00000000000..9af730db969 --- /dev/null +++ b/homeassistant/components/cert_expiry/.translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "certificate_fetch_failed": "\u7121\u6cd5\u81ea\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u7372\u5f97\u8a8d\u8b49", + "connection_timeout": "\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef\u903e\u6642", + "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790" + }, + "step": { + "user": { + "data": { + "host": "\u8a8d\u8b49\u4e3b\u6a5f\u7aef\u540d\u7a31", + "name": "\u8a8d\u8b49\u540d\u7a31", + "port": "\u8a8d\u8b49\u901a\u8a0a\u57e0" + }, + "title": "\u5b9a\u7fa9\u8a8d\u8b49\u9032\u884c\u6e2c\u8a66" + } + }, + "title": "\u8a8d\u8b49\u5df2\u904e\u671f" + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 78ceb60dd40..7c7efea7333 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -1 +1,17 @@ """The cert_expiry component.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + + +async def async_setup(hass, config): + """Platform setup, do nothing.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Load the saved entities.""" + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + return True diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py new file mode 100644 index 00000000000..d73762ce882 --- /dev/null +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -0,0 +1,100 @@ +"""Config flow for the Cert Expiry platform.""" +import socket +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME +from .helper import get_cert + + +@callback +def certexpiry_entries(hass: HomeAssistant): + """Return the host,port tuples for the domain.""" + return set( + (entry.data[CONF_HOST], entry.data[CONF_PORT]) + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Initialize the config flow.""" + self._errors = {} + + def _prt_in_configuration_exists(self, user_input) -> bool: + """Return True if host, port combination exists in configuration.""" + host = user_input[CONF_HOST] + port = user_input.get(CONF_PORT, DEFAULT_PORT) + if (host, port) in certexpiry_entries(self.hass): + return True + return False + + async def _test_connection(self, user_input=None): + """Test connection to the server and try to get the certtificate.""" + try: + await self.hass.async_add_executor_job( + get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT) + ) + return True + except socket.gaierror: + self._errors[CONF_HOST] = "resolve_failed" + except socket.timeout: + self._errors[CONF_HOST] = "connection_timeout" + except OSError: + self._errors[CONF_HOST] = "certificate_fetch_failed" + return False + + async def async_step_user(self, user_input=None): + """Step when user intializes a integration.""" + self._errors = {} + if user_input is not None: + # set some defaults in case we need to return to the form + if self._prt_in_configuration_exists(user_input): + self._errors[CONF_HOST] = "host_port_exists" + else: + if await self._test_connection(user_input): + host = user_input[CONF_HOST] + name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) + prt = user_input.get(CONF_PORT, DEFAULT_PORT) + return self.async_create_entry( + title=name, data={CONF_HOST: host, CONF_PORT: prt} + ) + else: + user_input = {} + user_input[CONF_NAME] = DEFAULT_NAME + user_input[CONF_HOST] = "" + user_input[CONF_PORT] = DEFAULT_PORT + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required(CONF_HOST, default=user_input[CONF_HOST]): str, + vol.Required( + CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + } + ), + errors=self._errors, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry. + + Only host was required in the yaml file all other fields are optional + """ + if self._prt_in_configuration_exists(user_input): + return self.async_abort(reason="host_port_exists") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/cert_expiry/const.py b/homeassistant/components/cert_expiry/const.py new file mode 100644 index 00000000000..4129781f2a0 --- /dev/null +++ b/homeassistant/components/cert_expiry/const.py @@ -0,0 +1,6 @@ +"""Const for Cert Expiry.""" + +DOMAIN = "cert_expiry" +DEFAULT_NAME = "SSL Certificate Expiry" +DEFAULT_PORT = 443 +TIMEOUT = 10.0 diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py new file mode 100644 index 00000000000..9c10887293a --- /dev/null +++ b/homeassistant/components/cert_expiry/helper.py @@ -0,0 +1,15 @@ +"""Helper functions for the Cert Expiry platform.""" +import socket +import ssl + +from .const import TIMEOUT + + +def get_cert(host, port): + """Get the ssl certificate for the host and port combination.""" + ctx = ssl.create_default_context() + address = (host, port) + with socket.create_connection(address, timeout=TIMEOUT) as sock: + with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: + cert = ssock.getpeercert() + return cert diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 7ef2e0b7d10..781f27afb5f 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -1,8 +1,9 @@ { - "domain": "cert_expiry", - "name": "Cert expiry", - "documentation": "https://www.home-assistant.io/components/cert_expiry", - "requirements": [], - "dependencies": [], - "codeowners": [] + "domain": "cert_expiry", + "name": "Cert expiry", + "documentation": "https://www.home-assistant.io/components/cert_expiry", + "requirements": [], + "config_flow": true, + "dependencies": [], + "codeowners": ["@cereal2nd"] } diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index b1e0d819358..b564cff7338 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, @@ -16,15 +17,13 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity +from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT +from .helper import get_cert + _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "SSL Certificate Expiry" -DEFAULT_PORT = 443 - SCAN_INTERVAL = timedelta(hours=12) -TIMEOUT = 10.0 - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -34,22 +33,22 @@ 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 certificate expiry sensor.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) + ) - def run_setup(event): - """Wait until Home Assistant is fully initialized before creating. - Delay the setup until Home Assistant is fully initialized. - """ - server_name = config.get(CONF_HOST) - server_port = config.get(CONF_PORT) - sensor_name = config.get(CONF_NAME) - - add_entities([SSLCertificate(sensor_name, server_name, server_port)], True) - - # To allow checking of the HA certificate we must first be running. - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) +async def async_setup_entry(hass, entry, async_add_entities): + """Add cert-expiry entry.""" + async_add_entities( + [SSLCertificate(entry.title, entry.data[CONF_HOST], entry.data[CONF_PORT])], + True, + ) + return True class SSLCertificate(Entity): @@ -88,15 +87,19 @@ class SSLCertificate(Entity): """Icon to use in the frontend, if any.""" return self._available + async def async_added_to_hass(self): + """Once the entity is added we should update to get the initial data loaded.""" + + def do_update(_): + """Run the update method when the start event was fired.""" + self.update() + + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + def update(self): """Fetch the certificate information.""" - ctx = ssl.create_default_context() try: - address = (self.server_name, self.server_port) - with socket.create_connection(address, timeout=TIMEOUT) as sock: - with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: - cert = ssock.getpeercert() - + cert = get_cert(self.server_name, self.server_port) except socket.gaierror: _LOGGER.error("Cannot resolve hostname: %s", self.server_name) self._available = False diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json new file mode 100644 index 00000000000..8943643e8b3 --- /dev/null +++ b/homeassistant/components/cert_expiry/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Certificate Expiry", + "step": { + "user": { + "title": "Define the certificate to test", + "data": { + "name": "The name of the certificate", + "host": "The hostname of the certificate", + "port": "The port of the certificate" + } + } + }, + "error": { + "host_port_exists": "This host and port combination is already configured", + "resolve_failed": "This host can not be resolved", + "connection_timeout": "Timeout whemn connecting to this host", + "certificate_fetch_failed": "Can not fetch certificate from this host and port combination" + }, + "abort": { + "host_port_exists": "This host and port combination is already configured" + } + } +} diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 9feac3207ad..a77f5673df7 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -52,9 +52,7 @@ class CiscoWebexTeamsNotificationService(BaseNotificationService): title = "{}{}".format(kwargs.get(ATTR_TITLE), "
") try: - self.client.messages.create( - roomId=self.room, html="{}{}".format(title, message) - ) + self.client.messages.create(roomId=self.room, html=f"{title}{message}") except ApiError as api_error: _LOGGER.error( "Could not send CiscoWebexTeams notification. " "Error: %s", api_error diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 1ec828b4a28..87fc217ac42 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -73,7 +73,7 @@ class ClicksendNotificationService(BaseNotificationService): } ) - api_url = "{}/sms/send".format(BASE_API_URL) + api_url = f"{BASE_API_URL}/sms/send" resp = requests.post( api_url, data=json.dumps(data), @@ -94,7 +94,7 @@ class ClicksendNotificationService(BaseNotificationService): def _authenticate(config): """Authenticate with ClickSend.""" - api_url = "{}/account".format(BASE_API_URL) + api_url = f"{BASE_API_URL}/account" resp = requests.get( api_url, headers=HEADERS, diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index 7c73c346a33..ba30c61e937 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -79,7 +79,7 @@ class ClicksendNotificationService(BaseNotificationService): } ] } - api_url = "{}/voice/send".format(BASE_API_URL) + api_url = f"{BASE_API_URL}/voice/send" resp = requests.post( api_url, data=json.dumps(data), @@ -100,7 +100,7 @@ class ClicksendNotificationService(BaseNotificationService): def _authenticate(config): """Authenticate with ClickSend.""" - api_url = "{}/account".format(BASE_API_URL) + api_url = f"{BASE_API_URL}/account" resp = requests.get( api_url, headers=HEADERS, diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d261c9e494c..fce530ddce5 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -157,7 +157,7 @@ def _process_cloud_exception(exc, where): err_info = _CLOUD_ERRORS.get(exc.__class__) if err_info is None: _LOGGER.exception("Unexpected error processing request for %s", where) - err_info = (502, "Unexpected error: {}".format(exc)) + err_info = (502, f"Unexpected error: {exc}") return err_info diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index a491bcc09ee..dbaa763c461 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -82,7 +82,7 @@ class CmusDevice(MediaPlayerDevice): if server: self.cmus = remote.PyCmus(server=server, password=password, port=port) - auto_name = "cmus-{}".format(server) + auto_name = f"cmus-{server}" else: self.cmus = remote.PyCmus() auto_name = "cmus-local" diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index d881482ed1a..9098a053fff 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -68,7 +68,7 @@ class CO2Sensor(Entity): lat=round(self._latitude, 2), lon=round(self._longitude, 2) ) - self._friendly_name = "CO2 intensity - {}".format(device_name) + self._friendly_name = f"CO2 intensity - {device_name}" @property def name(self): diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index c5f53ef609d..4a3e85d5e43 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -44,7 +44,7 @@ class AccountSensor(Entity): def __init__(self, coinbase_data, name, currency): """Initialize the sensor.""" self._coinbase_data = coinbase_data - self._name = "Coinbase {}".format(name) + self._name = f"Coinbase {name}" self._state = None self._unit_of_measurement = currency self._native_balance = None @@ -97,7 +97,7 @@ class ExchangeRateSensor(Entity): """Initialize the sensor.""" self._coinbase_data = coinbase_data self.currency = exchange_currency - self._name = "{} Exchange Rate".format(exchange_currency) + self._name = f"{exchange_currency} Exchange Rate" self._state = None self._unit_of_measurement = native_currency diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index 2383700f42a..68f0d77e307 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "http://{}:{}".format(host, port) + url = f"http://{host}:{port}" try: add_entities([Concord232Alarm(url, name, code, mode)], True) diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 89b6ab6af97..10643f134d7 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: _LOGGER.debug("Initializing client") - client = concord232_client.Client("http://{}:{}".format(host, port)) + client = concord232_client.Client(f"http://{host}:{port}") client.zones = client.list_zones() client.last_zone_update = datetime.datetime.now() diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5de11a032c5..6d4b465fceb 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -36,7 +36,7 @@ async def async_setup(hass, config): async def setup_panel(panel_name): """Set up a panel.""" - panel = importlib.import_module(".{}".format(panel_name), __name__) + panel = importlib.import_module(f".{panel_name}", __name__) if not panel: return @@ -44,7 +44,7 @@ async def async_setup(hass, config): success = await panel.async_setup(hass) if success: - key = "{}.{}".format(DOMAIN, panel_name) + key = f"{DOMAIN}.{panel_name}" hass.bus.async_fire(EVENT_COMPONENT_LOADED, {ATTR_COMPONENT: key}) @callback @@ -82,8 +82,8 @@ class BaseEditConfigView(HomeAssistantView): post_write_hook=None, ): """Initialize a config view.""" - self.url = "/api/config/%s/%s/{config_key}" % (component, config_type) - self.name = "api:config:%s:%s" % (component, config_type) + self.url = f"/api/config/{component}/{config_type}/{{config_key}}" + self.name = f"api:config:{component}:{config_type}" self.path = path self.key_schema = key_schema self.data_schema = data_schema @@ -126,14 +126,14 @@ class BaseEditConfigView(HomeAssistantView): try: self.key_schema(config_key) except vol.Invalid as err: - return self.json_message("Key malformed: {}".format(err), 400) + return self.json_message(f"Key malformed: {err}", 400) try: # We just validate, we don't store that data because # we don't want to store the defaults. self.data_schema(data) except vol.Invalid as err: - return self.json_message("Message malformed: {}".format(err), 400) + return self.json_message(f"Message malformed: {err}", 400) hass = request.app["hass"] path = hass.config.path(self.path) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index 99995959c23..f3b2a41e917 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -61,10 +61,10 @@ def async_request_config( Will return an ID to be used for sequent calls. """ if link_name is not None and link_url is not None: - description += "\n\n[{}]({})".format(link_name, link_url) + description += f"\n\n[{link_name}]({link_url})" if description_image is not None: - description += "\n\n![Description image]({})".format(description_image) + description += f"\n\n![Description image]({description_image})" instance = hass.data.get(_KEY_INSTANCE) diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py index 2ad31e7513b..6295125b7ca 100644 --- a/homeassistant/components/crimereports/sensor.py +++ b/homeassistant/components/crimereports/sensor.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "crimereports" -EVENT_INCIDENT = "{}_incident".format(DOMAIN) +EVENT_INCIDENT = f"{DOMAIN}_incident" SCAN_INTERVAL = timedelta(minutes=30) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 79bb050a617..f6a5133d8a9 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -333,7 +333,7 @@ class CupsData: else: for ipp_printer in self._ipp_printers: self.attributes[ipp_printer] = conn.getPrinterAttributes( - uri="ipp://{}:{}/{}".format(self._host, self._port, ipp_printer) + uri=f"ipp://{self._host}:{self._port}/{ipp_printer}" ) self.available = True diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index dbafae55187..d4660d70286 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -95,7 +95,7 @@ class CurrencylayerSensor(Entity): self.rest.update() value = self.rest.data if value is not None: - self._state = round(value["{}{}".format(self._base, self._quote)], 4) + self._state = round(value[f"{self._base}{self._quote}"], 4) class CurrencylayerData: diff --git a/homeassistant/components/daikin/.translations/ko.json b/homeassistant/components/daikin/.translations/ko.json index 2291d46800d..4b1d1bd86e5 100644 --- a/homeassistant/components/daikin/.translations/ko.json +++ b/homeassistant/components/daikin/.translations/ko.json @@ -10,10 +10,10 @@ "data": { "host": "\ud638\uc2a4\ud2b8" }, - "description": "Daikin AC \uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "Daikin AC \uad6c\uc131" + "description": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8\uc758 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8 \uad6c\uc131" } }, - "title": "Daikin AC" + "title": "\ub2e4\uc774\ud0a8 \uc5d0\uc5b4\ucee8" } } \ No newline at end of file diff --git a/homeassistant/components/daikin/sensor.py b/homeassistant/components/daikin/sensor.py index c55988b8dc1..f83566e66e8 100644 --- a/homeassistant/components/daikin/sensor.py +++ b/homeassistant/components/daikin/sensor.py @@ -58,7 +58,7 @@ class DaikinClimateSensor(Entity): @property def unique_id(self): """Return a unique ID.""" - return "{}-{}".format(self._api.mac, self._device_attribute) + return f"{self._api.mac}-{self._device_attribute}" @property def icon(self): diff --git a/homeassistant/components/daikin/switch.py b/homeassistant/components/daikin/switch.py index 6290e6fecef..4d3b0d3eade 100644 --- a/homeassistant/components/daikin/switch.py +++ b/homeassistant/components/daikin/switch.py @@ -44,7 +44,7 @@ class DaikinZoneSwitch(ToggleEntity): @property def unique_id(self): """Return a unique ID.""" - return "{}-zone{}".format(self._api.mac, self._zone_id) + return f"{self._api.mac}-zone{self._zone_id}" @property def icon(self): diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 0f33935c66c..d4e7e7ec63a 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -371,7 +371,7 @@ SENSOR_TYPES = { CONDITION_PICTURES = { "clear-day": ["/static/images/darksky/weather-sunny.svg", "mdi:weather-sunny"], - "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-sunny"], + "clear-night": ["/static/images/darksky/weather-night.svg", "mdi:weather-night"], "rain": ["/static/images/darksky/weather-pouring.svg", "mdi:weather-pouring"], "snow": ["/static/images/darksky/weather-snowy.svg", "mdi:weather-snowy"], "sleet": ["/static/images/darksky/weather-hail.svg", "mdi:weather-snowy-rainy"], @@ -553,10 +553,10 @@ class DarkSkySensor(Entity): def name(self): """Return the name of the sensor.""" if self.forecast_day is not None: - return "{} {} {}d".format(self.client_name, self._name, self.forecast_day) + return f"{self.client_name} {self._name} {self.forecast_day}d" if self.forecast_hour is not None: - return "{} {} {}h".format(self.client_name, self._name, self.forecast_hour) - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name} {self.forecast_hour}h" + return f"{self.client_name} {self._name}" @property def state(self): @@ -704,7 +704,7 @@ class DarkSkyAlertSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index 1ad4ed9aab8..5517e41d5c6 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -59,7 +59,7 @@ def setup(hass, config): statsd.event( title="Home Assistant", - text="%%% \n **{}** {} \n %%%".format(name, message), + text=f"%%% \n **{name}** {message} \n %%%", tags=[ "entity:{}".format(event.data.get("entity_id")), "domain:{}".format(event.data.get("domain")), @@ -79,8 +79,8 @@ def setup(hass, config): return states = dict(state.attributes) - metric = "{}.{}".format(prefix, state.domain) - tags = ["entity:{}".format(state.entity_id)] + metric = f"{prefix}.{state.domain}" + tags = [f"entity:{state.entity_id}"] for key, value in states.items(): if isinstance(value, (float, int)): diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 4a40561b9e3..4e661376719 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -65,7 +65,7 @@ class DdWrtDeviceScanner(DeviceScanner): self.mac2name = {} # Test the router is accessible - url = "{}://{}/Status_Wireless.live.asp".format(self.protocol, self.host) + url = f"{self.protocol}://{self.host}/Status_Wireless.live.asp" data = self.get_ddwrt_data(url) if not data: raise ConnectionError("Cannot connect to DD-Wrt router") @@ -80,7 +80,7 @@ class DdWrtDeviceScanner(DeviceScanner): """Return the name of the given device or None if we don't know.""" # If not initialised and not already scanned and not found. if device not in self.mac2name: - url = "{}://{}/Status_Lan.live.asp".format(self.protocol, self.host) + url = f"{self.protocol}://{self.host}/Status_Lan.live.asp" data = self.get_ddwrt_data(url) if not data: @@ -115,7 +115,7 @@ class DdWrtDeviceScanner(DeviceScanner): _LOGGER.info("Checking ARP") endpoint = "Wireless" if self.wireless_only else "Lan" - url = "{}://{}/Status_{}.live.asp".format(self.protocol, self.host, endpoint) + url = f"{self.protocol}://{self.host}/Status_{endpoint}.live.asp" data = self.get_ddwrt_data(url) if not data: diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index 7b69b7477f5..263730ba583 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -40,5 +40,23 @@ } }, "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Permet sensors deCONZ CLIP", + "allow_deconz_groups": "Permet grups de llums deCONZ" + }, + "description": "Configura la visibilitat dels tipus dels dispositius deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Permet sensors deCONZ CLIP", + "allow_deconz_groups": "Permet grups de llums deCONZ" + }, + "description": "Configura la visibilitat dels tipus dels dispositius deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index f79538ffeb6..1b595924106 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -40,5 +40,23 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", + "allow_deconz_groups": "Tillad deCONZ lys grupper" + }, + "description": "Konfigurer synligheden af deCONZ-enhedstyper" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", + "allow_deconz_groups": "Tillad deCONZ lys grupper" + }, + "description": "Konfigurer synligheden af deCONZ-enhedstyper" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index b7cba820daa..97e25e28965 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -40,5 +40,23 @@ } }, "title": "deCONZ Zigbee Gateway" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", + "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" + }, + "description": "Konfigurieren der Sichtbarkeit von deCONZ-Ger\u00e4tetypen" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", + "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" + }, + "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 3c6656d6ae6..ead71db8c27 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -41,8 +41,44 @@ }, "title": "deCONZ Zigbee gateway" }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "close": "Close", + "dim_down": "Dim down", + "dim_up": "Dim up", + "left": "Left", + "open": "Open", + "right": "Right", + "turn_off": "Turn off", + "turn_on": "Turn on" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_gyro_activated": "Device shaken" + } + }, "options": { "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + }, + "description": "Configure visibility of deCONZ device types" + }, "deconz_devices": { "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index ca38deb28fe..8bcf03914ce 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -38,5 +38,21 @@ } }, "title": "Pasarela Zigbee deCONZ" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + } + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Permitir sensores deCONZ CLIP", + "allow_deconz_groups": "Permitir grupos de luz deCONZ" + }, + "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index f15a2ddf265..90b85aaeba5 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione per bridge \u00e8 gi\u00e0 in corso.", "no_bridges": "Nessun bridge deCONZ rilevato", + "not_deconz_bridge": "Non \u00e8 un bridge deCONZ", "one_instance_only": "Il componente supporto solo un'istanza di deCONZ", "updated_instance": "Istanza deCONZ aggiornata con nuovo indirizzo host" }, @@ -21,12 +23,12 @@ "init": { "data": { "host": "Host", - "port": "Porta (valore di default: '80')" + "port": "Porta" }, "title": "Definisci il gateway deCONZ" }, "link": { - "description": "Sblocca il tuo gateway deCONZ per registrarlo in Home Assistant.\n\n1. Vai nelle impostazioni di sistema di deCONZ\n2. Premi il bottone \"Unlock Gateway\"", + "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"", "title": "Collega con deCONZ" }, "options": { @@ -38,5 +40,23 @@ } }, "title": "Gateway Zigbee deCONZ" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Consentire sensori CLIP deCONZ", + "allow_deconz_groups": "Consentire gruppi luce deCONZ" + }, + "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Consentire sensori CLIP deCONZ", + "allow_deconz_groups": "Consentire gruppi luce deCONZ" + }, + "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 4bf845d50e5..0ddff8557ec 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -40,5 +40,23 @@ } }, "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9", + "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9" + }, + "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9", + "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9" + }, + "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 19477bbed3f..f9f2d40488f 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -40,5 +40,23 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "DeCONZ CLIP sensoren toestaan", + "allow_deconz_groups": "DeCONZ-lichtgroepen toestaan" + }, + "description": "De zichtbaarheid van deCONZ-apparaattypen configureren" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "DeCONZ CLIP sensoren toestaan", + "allow_deconz_groups": "Sta deCONZ-lichtgroepen toe" + }, + "description": "Configureer de zichtbaarheid van deCONZ-apparaattypen" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7c674c71022..8798248224a 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -40,5 +40,16 @@ } }, "title": "deCONZ Zigbee gateway" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillat deCONZ lys grupper" + }, + "description": "Konfigurere synlighet av deCONZ enhetstyper" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index a17835f79a3..506461ea50e 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -15,9 +15,9 @@ "hassio_confirm": { "data": { "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w", - "allow_deconz_groups": "Zezw\u00f3l na importowanie grup deCONZ" + "allow_deconz_groups": "Zezwalaj na importowanie grup deCONZ" }, - "description": "Czy chcesz skonfigurowa\u0107 Home Assistant'a, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?", + "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?", "title": "Bramka deCONZ Zigbee przez dodatek Hass.io" }, "init": { @@ -28,7 +28,7 @@ "title": "Zdefiniuj bramk\u0119 deCONZ" }, "link": { - "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawie\u0144 systemu deCONZ \n 2. Naci\u015bnij przycisk \"Odblokuj bramk\u0119\"", + "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"", "title": "Po\u0142\u0105cz z deCONZ" }, "options": { @@ -40,5 +40,23 @@ } }, "title": "Brama deCONZ Zigbee" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", + "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ" + }, + "description": "Skonfiguruj widoczno\u015b\u0107 urz\u0105dze\u0144 deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", + "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ" + }, + "description": "Skonfiguruj widoczno\u015b\u0107 typ\u00f3w urz\u0105dze\u0144 deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index ea701b3f759..23e98919bb8 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -40,5 +40,23 @@ } }, "title": "deCONZ" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP", + "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 deCONZ CLIP", + "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 58ecde32a84..86210b2e6c1 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -40,5 +40,23 @@ } }, "title": "deCONZ Zigbee prehod" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "Dovoli deCONZ CLIP senzorje", + "allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di" + }, + "description": "Konfiguracija vidnosti tipov naprav deCONZ" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "Dovoli deCONZ CLIP senzorje", + "allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di" + }, + "description": "Konfiguracija vidnosti tipov naprav 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 0173c90c3b7..75dcac93dd9 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -40,5 +40,23 @@ } }, "title": "deCONZ Zigbee \u9598\u9053\u5668" + }, + "options": { + "step": { + "async_step_deconz_devices": { + "data": { + "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", + "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" + }, + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + }, + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", + "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" + }, + "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u88dd\u7f6e\u985e\u578b" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 68974d12253..56663c6b2da 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -12,15 +12,7 @@ from homeassistant.helpers import config_validation as cv # Loading the config flow file will register the flow from .config_flow import get_master_gateway -from .const import ( - CONF_ALLOW_CLIP_SENSOR, - CONF_ALLOW_DECONZ_GROUPS, - CONF_BRIDGEID, - CONF_MASTER_GATEWAY, - DEFAULT_PORT, - DOMAIN, - _LOGGER, -) +from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN, _LOGGER from .gateway import DeconzGateway CONFIG_SCHEMA = vol.Schema( @@ -86,7 +78,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = {} if not config_entry.options: - await async_populate_options(hass, config_entry) + await async_update_master_gateway(hass, config_entry) gateway = DeconzGateway(hass, config_entry) @@ -203,25 +195,25 @@ async def async_unload_entry(hass, config_entry): hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH) elif gateway.master: - await async_populate_options(hass, config_entry) + await async_update_master_gateway(hass, config_entry) new_master_gateway = next(iter(hass.data[DOMAIN].values())) - await async_populate_options(hass, new_master_gateway.config_entry) + await async_update_master_gateway(hass, new_master_gateway.config_entry) return await gateway.async_reset() -async def async_populate_options(hass, config_entry): - """Populate default options for gateway. +async def async_update_master_gateway(hass, config_entry): + """Update master gateway boolean. Called by setup_entry and unload_entry. Makes sure there is always one master available. """ master = not get_master_gateway(hass) - options = { - CONF_MASTER_GATEWAY: master, - CONF_ALLOW_CLIP_SENSOR: config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, False), - CONF_ALLOW_DECONZ_GROUPS: config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True), - } + old_options = dict(config_entry.options) + + new_options = {CONF_MASTER_GATEWAY: master} + + options = {**old_options, **new_options} hass.config_entries.async_update_entry(config_entry, options=options) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 0b5d3173812..492b16a603a 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry, DeconzEntityHandler ATTR_ORIENTATION = "orientation" ATTR_TILTANGLE = "tiltangle" @@ -24,6 +24,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ binary sensor.""" gateway = get_gateway_from_config_entry(hass, config_entry) + entity_handler = DeconzEntityHandler(gateway) + @callback def async_add_sensor(sensors): """Add binary sensor from deCONZ.""" @@ -31,17 +33,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if sensor.BINARY and not ( - not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") - ): - - entities.append(DeconzBinarySensor(sensor, gateway)) + if sensor.BINARY: + new_sensor = DeconzBinarySensor(sensor, gateway) + entity_handler.add_entity(new_sensor) + entities.append(new_sensor) async_add_entities(entities, True) gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor + hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) ) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index d20833d5a82..1844cb2c97c 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -3,6 +3,7 @@ from pydeconz.sensor import Thermostat from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, @@ -15,7 +16,7 @@ from .const import ATTR_OFFSET, ATTR_VALVE, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] +SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -37,17 +38,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if sensor.type in Thermostat.ZHATYPE and not ( - not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") - ): - + if sensor.type in Thermostat.ZHATYPE: entities.append(DeconzThermostat(sensor, gateway)) async_add_entities(entities, True) gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_climate + hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_climate ) ) @@ -68,7 +66,9 @@ class DeconzThermostat(DeconzDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ - if self._device.on: + if self._device.mode in SUPPORT_HVAC: + return self._device.mode + if self._device.state_on: return HVAC_MODE_HEAT return HVAC_MODE_OFF @@ -101,8 +101,10 @@ class DeconzThermostat(DeconzDevice, ClimateDevice): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" - if hvac_mode == HVAC_MODE_HEAT: + if hvac_mode == HVAC_MODE_AUTO: data = {"mode": "auto"} + elif hvac_mode == HVAC_MODE_HEAT: + data = {"mode": "heat"} elif hvac_mode == HVAC_MODE_OFF: data = {"mode": "off"} diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 306a4fbf839..12e2e092f67 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,6 +1,5 @@ """Config flow to configure deCONZ component.""" import asyncio -from copy import copy import async_timeout import voluptuous as vol @@ -17,6 +16,8 @@ from .const import ( CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, + DEFAULT_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, DOMAIN, ) @@ -43,8 +44,7 @@ def get_master_gateway(hass): return gateway -@config_entries.HANDLERS.register(DOMAIN) -class DeconzFlowHandler(config_entries.ConfigFlow): +class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a deCONZ config flow.""" VERSION = 1 @@ -257,7 +257,7 @@ class DeconzOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize deCONZ options flow.""" self.config_entry = config_entry - self.options = copy(config_entry.options) + self.options = dict(config_entry.options) async def async_step_init(self, user_input=None): """Manage the deCONZ options.""" @@ -278,11 +278,15 @@ class DeconzOptionsFlowHandler(config_entries.OptionsFlow): { vol.Optional( CONF_ALLOW_CLIP_SENSOR, - default=self.config_entry.options[CONF_ALLOW_CLIP_SENSOR], + default=self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ), ): bool, vol.Optional( CONF_ALLOW_DECONZ_GROUPS, - default=self.config_entry.options[CONF_ALLOW_DECONZ_GROUPS], + default=self.config_entry.options.get( + CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS + ), ): bool, } ), diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index ba6172120ec..62879a82724 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -1,13 +1,13 @@ """Constants for the deCONZ component.""" import logging -_LOGGER = logging.getLogger(".") +_LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False -DEFAULT_ALLOW_DECONZ_GROUPS = False +DEFAULT_ALLOW_DECONZ_GROUPS = True CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor" CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups" diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index be4088a5c86..b82144d37c7 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -40,7 +40,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_cover + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_cover ) ) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 389ed11e437..e6249b2304c 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -7,38 +7,14 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN as DECONZ_DOMAIN -class DeconzDevice(Entity): - """Representation of a deCONZ device.""" +class DeconzBase: + """Common base for deconz entities and events.""" def __init__(self, device, gateway): """Set up device and add update callback to get data from websocket.""" self._device = device self.gateway = gateway - self.unsub_dispatcher = None - - async def async_added_to_hass(self): - """Subscribe to device events.""" - self._device.register_async_callback(self.async_update_callback) - self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id - self.unsub_dispatcher = async_dispatcher_connect( - self.hass, self.gateway.event_reachable, self.async_update_callback - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - self._device.remove_callback(self.async_update_callback) - del self.gateway.deconz_ids[self.entity_id] - self.unsub_dispatcher() - - @callback - def async_update_callback(self, force_update=False): - """Update the device's state.""" - self.async_schedule_update_ha_state() - - @property - def name(self): - """Return the name of the device.""" - return self._device.name + self.listeners = [] @property def unique_id(self): @@ -46,30 +22,90 @@ class DeconzDevice(Entity): return self._device.uniqueid @property - def available(self): - """Return True if device is available.""" - return self.gateway.available and self._device.reachable + def serial(self): + """Return a serial number for this device.""" + if self.unique_id is None or self.unique_id.count(":") != 7: + return None - @property - def should_poll(self): - """No polling needed.""" - return False + return self.unique_id.split("-", 1)[0] @property def device_info(self): """Return a device description for device registry.""" - if self._device.uniqueid is None or self._device.uniqueid.count(":") != 7: + if self.serial is None: return None - serial = self._device.uniqueid.split("-", 1)[0] bridgeid = self.gateway.api.config.bridgeid return { - "connections": {(CONNECTION_ZIGBEE, serial)}, - "identifiers": {(DECONZ_DOMAIN, serial)}, + "connections": {(CONNECTION_ZIGBEE, self.serial)}, + "identifiers": {(DECONZ_DOMAIN, self.serial)}, "manufacturer": self._device.manufacturer, "model": self._device.modelid, "name": self._device.name, "sw_version": self._device.swversion, "via_device": (DECONZ_DOMAIN, bridgeid), } + + +class DeconzDevice(DeconzBase, Entity): + """Representation of a deCONZ device.""" + + def __init__(self, device, gateway): + """Set up device and add update callback to get data from websocket.""" + super().__init__(device, gateway) + + self.unsub_dispatcher = None + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if not self.gateway.option_allow_clip_sensor and self._device.type.startswith( + "CLIP" + ): + return False + + if ( + not self.gateway.option_allow_deconz_groups + and self._device.type == "LightGroup" + ): + return False + + return True + + async def async_added_to_hass(self): + """Subscribe to device events.""" + self._device.register_async_callback(self.async_update_callback) + self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id + self.listeners.append( + async_dispatcher_connect( + self.hass, self.gateway.signal_reachable, self.async_update_callback + ) + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect device object when removed.""" + self._device.remove_callback(self.async_update_callback) + del self.gateway.deconz_ids[self.entity_id] + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + + @callback + def async_update_callback(self, force_update=False): + """Update the device's state.""" + self.async_schedule_update_ha_state() + + @property + def available(self): + """Return True if device is available.""" + return self.gateway.available and self._device.reachable + + @property + def name(self): + """Return the name of the device.""" + return self._device.name + + @property + def should_poll(self): + """No polling needed.""" + return False diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py new file mode 100644 index 00000000000..f6c2d471bbf --- /dev/null +++ b/homeassistant/components/deconz/deconz_event.py @@ -0,0 +1,56 @@ +"""Representation of a deCONZ remote.""" +from homeassistant.const import CONF_EVENT, CONF_ID +from homeassistant.core import callback +from homeassistant.util import slugify + +from .const import _LOGGER +from .deconz_device import DeconzBase + +CONF_DECONZ_EVENT = "deconz_event" +CONF_UNIQUE_ID = "unique_id" + + +class DeconzEvent(DeconzBase): + """When you want signals instead of entities. + + Stateless sensors such as remotes are expected to generate an event + instead of a sensor entity in hass. + """ + + def __init__(self, device, gateway): + """Register callback that will be used for signals.""" + super().__init__(device, gateway) + + self._device.register_async_callback(self.async_update_callback) + + self.device_id = None + self.event_id = slugify(self._device.name) + _LOGGER.debug("deCONZ event created: %s", self.event_id) + + @callback + def async_will_remove_from_hass(self) -> None: + """Disconnect event object when removed.""" + self._device.remove_callback(self.async_update_callback) + self._device = None + + @callback + def async_update_callback(self, force_update=False): + """Fire the event if reason is that state is updated.""" + if "state" in self._device.changed_keys: + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.serial, + CONF_EVENT: self._device.state, + } + self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) + + async def async_update_device_registry(self): + """Update device registry.""" + device_registry = ( + await self.gateway.hass.helpers.device_registry.async_get_registry() + ) + + entry = device_registry.async_get_or_create( + config_entry_id=self.gateway.config_entry.entry_id, **self.device_info + ) + self.device_id = entry.id diff --git a/homeassistant/components/deconz/device_automation.py b/homeassistant/components/deconz/device_automation.py new file mode 100644 index 00000000000..28f36b8f431 --- /dev/null +++ b/homeassistant/components/deconz/device_automation.py @@ -0,0 +1,254 @@ +"""Provides device automations for deconz events.""" +import voluptuous as vol + +import homeassistant.components.automation.event as event + +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) + +from . import DOMAIN +from .config_flow import configured_gateways +from .deconz_event import CONF_DECONZ_EVENT, CONF_UNIQUE_ID +from .gateway import get_gateway_from_config_entry + +CONF_SUBTYPE = "subtype" + +CONF_SHORT_PRESS = "remote_button_short_press" +CONF_SHORT_RELEASE = "remote_button_short_release" +CONF_LONG_PRESS = "remote_button_long_press" +CONF_LONG_RELEASE = "remote_button_long_release" +CONF_DOUBLE_PRESS = "remote_button_double_press" +CONF_TRIPLE_PRESS = "remote_button_triple_press" +CONF_QUADRUPLE_PRESS = "remote_button_quadruple_press" +CONF_QUINTUPLE_PRESS = "remote_button_quintuple_press" +CONF_ROTATED = "remote_button_rotated" +CONF_SHAKE = "remote_gyro_activated" + +CONF_TURN_ON = "turn_on" +CONF_TURN_OFF = "turn_off" +CONF_DIM_UP = "dim_up" +CONF_DIM_DOWN = "dim_down" +CONF_LEFT = "left" +CONF_RIGHT = "right" +CONF_OPEN = "open" +CONF_CLOSE = "close" +CONF_BOTH_BUTTONS = "both_buttons" +CONF_BUTTON_1 = "button_1" +CONF_BUTTON_2 = "button_2" +CONF_BUTTON_3 = "button_3" +CONF_BUTTON_4 = "button_4" + +HUE_DIMMER_REMOTE_MODEL = "RWL021" +HUE_DIMMER_REMOTE = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1000, + (CONF_SHORT_RELEASE, CONF_TURN_ON): 1002, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, + (CONF_SHORT_PRESS, CONF_DIM_UP): 2000, + (CONF_SHORT_RELEASE, CONF_DIM_UP): 2002, + (CONF_LONG_PRESS, CONF_DIM_UP): 2001, + (CONF_LONG_RELEASE, CONF_DIM_UP): 2003, + (CONF_SHORT_PRESS, CONF_DIM_DOWN): 3000, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): 3002, + (CONF_LONG_PRESS, CONF_DIM_DOWN): 3001, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): 3003, + (CONF_SHORT_PRESS, CONF_TURN_OFF): 4000, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): 4002, + (CONF_LONG_PRESS, CONF_TURN_OFF): 4001, + (CONF_LONG_RELEASE, CONF_TURN_OFF): 4003, +} + +HUE_TAP_REMOTE_MODEL = "ZGPSWITCH" +HUE_TAP_REMOTE = { + (CONF_SHORT_PRESS, CONF_BUTTON_1): 34, + (CONF_SHORT_PRESS, CONF_BUTTON_2): 16, + (CONF_SHORT_PRESS, CONF_BUTTON_3): 17, + (CONF_SHORT_PRESS, CONF_BUTTON_4): 18, +} + +TRADFRI_ON_OFF_SWITCH_MODEL = "TRADFRI on/off switch" +TRADFRI_ON_OFF_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, + (CONF_SHORT_PRESS, CONF_TURN_OFF): 2002, + (CONF_LONG_PRESS, CONF_TURN_OFF): 2001, + (CONF_LONG_RELEASE, CONF_TURN_OFF): 2003, +} + +TRADFRI_OPEN_CLOSE_REMOTE_MODEL = "TRADFRI open/close remote" +TRADFRI_OPEN_CLOSE_REMOTE = { + (CONF_SHORT_PRESS, CONF_OPEN): 1002, + (CONF_LONG_PRESS, CONF_OPEN): 1003, + (CONF_SHORT_PRESS, CONF_CLOSE): 2002, + (CONF_LONG_PRESS, CONF_CLOSE): 2003, +} + +TRADFRI_REMOTE_MODEL = "TRADFRI remote control" +TRADFRI_REMOTE = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_SHORT_PRESS, CONF_DIM_UP): 2002, + (CONF_LONG_PRESS, CONF_DIM_UP): 2001, + (CONF_LONG_RELEASE, CONF_DIM_UP): 2003, + (CONF_SHORT_PRESS, CONF_DIM_DOWN): 3002, + (CONF_LONG_PRESS, CONF_DIM_DOWN): 3001, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): 3003, + (CONF_SHORT_PRESS, CONF_LEFT): 4002, + (CONF_LONG_PRESS, CONF_LEFT): 4001, + (CONF_LONG_RELEASE, CONF_LEFT): 4003, + (CONF_SHORT_PRESS, CONF_RIGHT): 5002, + (CONF_LONG_PRESS, CONF_RIGHT): 5001, + (CONF_LONG_RELEASE, CONF_RIGHT): 5003, +} + +TRADFRI_WIRELESS_DIMMER_MODEL = "TRADFRI wireless dimmer" +TRADFRI_WIRELESS_DIMMER = { + (CONF_ROTATED, CONF_LEFT): 3002, + (CONF_ROTATED, CONF_RIGHT): 2002, +} + +AQARA_DOUBLE_WALL_SWITCH_MODEL = "lumi.remote.b286acn01" +AQARA_DOUBLE_WALL_SWITCH = { + (CONF_SHORT_PRESS, CONF_LEFT): 1002, + (CONF_LONG_PRESS, CONF_LEFT): 1001, + (CONF_DOUBLE_PRESS, CONF_LEFT): 1004, + (CONF_SHORT_PRESS, CONF_RIGHT): 2002, + (CONF_LONG_PRESS, CONF_RIGHT): 2001, + (CONF_DOUBLE_PRESS, CONF_RIGHT): 2004, + (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): 3002, + (CONF_LONG_PRESS, CONF_BOTH_BUTTONS): 3001, + (CONF_DOUBLE_PRESS, CONF_BOTH_BUTTONS): 3004, +} + +AQARA_MINI_SWITCH_MODEL = "lumi.remote.b1acn01" +AQARA_MINI_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, +} + +AQARA_ROUND_SWITCH_MODEL = "lumi.sensor_switch" +AQARA_ROUND_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1000, + (CONF_SHORT_RELEASE, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, + (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): 1006, + (CONF_QUINTUPLE_PRESS, CONF_TURN_ON): 1010, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, +} + +AQARA_SQUARE_SWITCH_MODEL = "lumi.sensor_switch.aq3" +AQARA_SQUARE_SWITCH = { + (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, + (CONF_LONG_PRESS, CONF_TURN_ON): 1001, + (CONF_LONG_RELEASE, CONF_TURN_ON): 1003, + (CONF_SHAKE, ""): 1007, +} + +REMOTES = { + HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, + HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, + TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, + TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, + TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, + TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, + AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, + AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, + AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, + AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, +} + +TRIGGER_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): DOMAIN, + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_TYPE): str, + vol.Required(CONF_SUBTYPE): str, + } + ) +) + + +def _get_deconz_event_from_device_id(hass, device_id): + """Resolve deconz event from device id.""" + deconz_config_entries = configured_gateways(hass) + for config_entry in deconz_config_entries.values(): + + gateway = get_gateway_from_config_entry(hass, config_entry) + for deconz_event in gateway.events: + + if device_id == deconz_event.device_id: + return deconz_event + + return None + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + if device.model not in REMOTES and trigger not in REMOTES[device.model]: + raise InvalidDeviceAutomationConfig + + trigger = REMOTES[device.model][trigger] + + deconz_event = _get_deconz_event_from_device_id(hass, device.id) + if deconz_event is None: + raise InvalidDeviceAutomationConfig + + event_id = deconz_event.serial + + state_config = { + event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, + event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, + } + + return await event.async_trigger(hass, state_config, action, automation_info) + + +async def async_get_triggers(hass, device_id): + """List device triggers. + + Make sure device is a supported remote model. + Retrieve the deconz event object matching device entry. + Generate device trigger list. + """ + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(device_id) + + if device.model not in REMOTES: + return + + triggers = [] + for trigger, subtype in REMOTES[device.model].keys(): + triggers.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 0ed3ffd2a56..35cf63fc3d2 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -6,15 +6,18 @@ from pydeconz import DeconzSession, errors from pydeconz.sensor import Switch from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID -from homeassistant.core import EventOrigin, callback +from homeassistant.const import CONF_HOST +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.util import slugify +from homeassistant.helpers.entity_registry import ( + async_get_registry, + DISABLED_CONFIG_ENTRY, +) from .const import ( _LOGGER, @@ -22,11 +25,14 @@ from .const import ( CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, CONF_MASTER_GATEWAY, + DEFAULT_ALLOW_CLIP_SENSOR, + DEFAULT_ALLOW_DECONZ_GROUPS, DOMAIN, NEW_DEVICE, NEW_SENSOR, SUPPORTED_PLATFORMS, ) +from .deconz_event import DeconzEvent from .errors import AuthenticationRequired, CannotConnect @@ -61,14 +67,18 @@ class DeconzGateway: return self.config_entry.options[CONF_MASTER_GATEWAY] @property - def allow_clip_sensor(self) -> bool: + def option_allow_clip_sensor(self) -> bool: """Allow loading clip sensor from gateway.""" - return self.config_entry.options.get(CONF_ALLOW_CLIP_SENSOR, True) + return self.config_entry.options.get( + CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR + ) @property - def allow_deconz_groups(self) -> bool: + def option_allow_deconz_groups(self) -> bool: """Allow loading deCONZ groups from gateway.""" - return self.config_entry.options.get(CONF_ALLOW_DECONZ_GROUPS, True) + return self.config_entry.options.get( + CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS + ) async def async_update_device_registry(self): """Update device registry.""" @@ -111,7 +121,7 @@ class DeconzGateway: self.listeners.append( async_dispatcher_connect( - hass, self.async_event_new_device(NEW_SENSOR), self.async_add_remote + hass, self.async_signal_new_device(NEW_SENSOR), self.async_add_remote ) ) @@ -119,35 +129,50 @@ class DeconzGateway: self.api.start() - self.config_entry.add_update_listener(self.async_new_address_callback) + self.config_entry.add_update_listener(self.async_new_address) + self.config_entry.add_update_listener(self.async_options_updated) return True @staticmethod - async def async_new_address_callback(hass, entry): + async def async_new_address(hass, entry): """Handle signals of gateway getting new address. This is a static method because a class method (bound method), can not be used with weak references. """ - gateway = hass.data[DOMAIN][entry.data[CONF_BRIDGEID]] - gateway.api.close() - gateway.api.host = entry.data[CONF_HOST] - gateway.api.start() + gateway = get_gateway_from_config_entry(hass, entry) + if gateway.api.host != entry.data[CONF_HOST]: + gateway.api.close() + gateway.api.host = entry.data[CONF_HOST] + gateway.api.start() @property - def event_reachable(self): + def signal_reachable(self): """Gateway specific event to signal a change in connection status.""" - return "deconz_reachable_{}".format(self.bridgeid) + return f"deconz-reachable-{self.bridgeid}" @callback def async_connection_status_callback(self, available): """Handle signals of gateway connection status.""" self.available = available - async_dispatcher_send(self.hass, self.event_reachable, True) + async_dispatcher_send(self.hass, self.signal_reachable, True) + + @property + def signal_options_update(self): + """Event specific per deCONZ entry to signal new options.""" + return f"deconz-options-{self.bridgeid}" + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + gateway = get_gateway_from_config_entry(hass, entry) + + registry = await async_get_registry(hass) + async_dispatcher_send(hass, gateway.signal_options_update, registry) @callback - def async_event_new_device(self, device_type): + def async_signal_new_device(self, device_type): """Gateway specific event to signal new device.""" return NEW_DEVICE[device_type].format(self.bridgeid) @@ -157,7 +182,7 @@ class DeconzGateway: if not isinstance(device, list): device = [device] async_dispatcher_send( - self.hass, self.async_event_new_device(device_type), device + self.hass, self.async_signal_new_device(device_type), device ) @callback @@ -165,9 +190,11 @@ class DeconzGateway: """Set up remote from deCONZ.""" for sensor in sensors: if sensor.type in Switch.ZHATYPE and not ( - not self.allow_clip_sensor and sensor.type.startswith("CLIP") + not self.option_allow_clip_sensor and sensor.type.startswith("CLIP") ): - self.events.append(DeconzEvent(self.hass, sensor)) + event = DeconzEvent(sensor, self) + self.hass.async_create_task(event.async_update_device_registry()) + self.events.append(event) @callback def shutdown(self, event): @@ -183,6 +210,7 @@ class DeconzGateway: Will cancel any scheduled setup retry and will unload the config entry. """ + self.api.async_connection_status_callback = None self.api.close() for component in SUPPORTED_PLATFORMS: @@ -229,31 +257,36 @@ async def get_gateway( raise CannotConnect -class DeconzEvent: - """When you want signals instead of entities. +class DeconzEntityHandler: + """Platform entity handler to help with updating disabled by.""" - Stateless sensors such as remotes are expected to generate an event - instead of a sensor entity in hass. - """ + def __init__(self, gateway): + """Create an entity handler.""" + self.gateway = gateway + self._entities = [] - def __init__(self, hass, device): - """Register callback that will be used for signals.""" - self._hass = hass - self._device = device - self._device.register_async_callback(self.async_update_callback) - self._event = "deconz_{}".format(CONF_EVENT) - self._id = slugify(self._device.name) - _LOGGER.debug("deCONZ event created: %s", self._id) + gateway.listeners.append( + async_dispatcher_connect( + gateway.hass, gateway.signal_options_update, self.update_entity_registry + ) + ) @callback - def async_will_remove_from_hass(self) -> None: - """Disconnect event object when removed.""" - self._device.remove_callback(self.async_update_callback) - self._device = None + def add_entity(self, entity): + """Add a new entity to handler.""" + self._entities.append(entity) @callback - def async_update_callback(self, force_update=False): - """Fire the event if reason is that state is updated.""" - if "state" in self._device.changed_keys: - data = {CONF_ID: self._id, CONF_EVENT: self._device.state} - self._hass.bus.async_fire(self._event, data, EventOrigin.remote) + def update_entity_registry(self, entity_registry): + """Update entity registry disabled by status.""" + for entity in self._entities: + + if entity.entity_registry_enabled_default != entity.enabled: + disabled_by = None + + if entity.enabled: + disabled_by = DISABLED_CONFIG_ENTRY + + entity_registry.async_update_entity( + entity.registry_entry.entity_id, disabled_by=disabled_by + ) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index b68aa6f0779..ec1dfd2bcb1 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -29,7 +29,7 @@ from .const import ( SWITCH_TYPES, ) from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry, DeconzEntityHandler async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -41,6 +41,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ lights and groups from a config entry.""" gateway = get_gateway_from_config_entry(hass, config_entry) + entity_handler = DeconzEntityHandler(gateway) + @callback def async_add_light(lights): """Add light from deCONZ.""" @@ -54,7 +56,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_light + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_light ) ) @@ -64,14 +66,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for group in groups: - if group.lights and gateway.allow_deconz_groups: - entities.append(DeconzGroup(group, gateway)) + if group.lights: + new_group = DeconzGroup(group, gateway) + entity_handler.add_entity(new_group) + entities.append(new_group) async_add_entities(entities, True) gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_GROUP), async_add_group + hass, gateway.async_signal_new_device(NEW_GROUP), async_add_group ) ) diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py index ede60e3ef45..8d27d386da2 100644 --- a/homeassistant/components/deconz/scene.py +++ b/homeassistant/components/deconz/scene.py @@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SCENE), async_add_scene + hass, gateway.async_signal_new_device(NEW_SCENE), async_add_scene ) ) @@ -49,6 +49,7 @@ class DeconzScene(Scene): async def async_will_remove_from_hass(self) -> None: """Disconnect scene object when removed.""" + del self.gateway.deconz_ids[self.entity_id] self._scene = None async def async_activate(self): diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index dad3c25cc38..d84a47c6aaf 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -13,7 +13,7 @@ from homeassistant.util import slugify from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice -from .gateway import get_gateway_from_config_entry +from .gateway import get_gateway_from_config_entry, DeconzEntityHandler ATTR_CURRENT = "current" ATTR_POWER = "power" @@ -30,6 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the deCONZ sensors.""" gateway = get_gateway_from_config_entry(hass, config_entry) + entity_handler = DeconzEntityHandler(gateway) + @callback def async_add_sensor(sensors): """Add sensors from deCONZ.""" @@ -37,22 +39,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): for sensor in sensors: - if not sensor.BINARY and not ( - not gateway.allow_clip_sensor and sensor.type.startswith("CLIP") - ): + if not sensor.BINARY: if sensor.type in Switch.ZHATYPE: if sensor.battery: entities.append(DeconzBattery(sensor, gateway)) else: - entities.append(DeconzSensor(sensor, gateway)) + new_sensor = DeconzSensor(sensor, gateway) + entity_handler.add_entity(new_sensor) + entities.append(new_sensor) async_add_entities(entities, True) gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor + hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor ) ) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 7081f816e6a..00aa463349c 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -51,5 +51,34 @@ } } } + }, + "device_automation": { + "trigger_type": { + "remote_button_short_press": "\"{subtype}\" button pressed", + "remote_button_short_release": "\"{subtype}\" button released", + "remote_button_long_press": "\"{subtype}\" button continuously pressed", + "remote_button_long_release": "\"{subtype}\" button released after long press", + "remote_button_double_press": "\"{subtype}\" button double clicked", + "remote_button_triple_press": "\"{subtype}\" button triple clicked", + "remote_button_quadruple_press": "\"{subtype}\" button quadruple clicked", + "remote_button_quintuple_press": "\"{subtype}\" button quintuple clicked", + "remote_button_rotated": "Button rotated \"{subtype}\"", + "remote_gyro_activated": "Device shaken" + }, + "trigger_subtype": { + "turn_on": "Turn on", + "turn_off": "Turn off", + "dim_up": "Dim up", + "dim_down": "Dim down", + "left": "Left", + "right": "Right", + "open": "Open", + "close": "Close", + "both_buttons": "Both buttons", + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button" + } } } diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 7ce40789802..b1fd4b10f46 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_event_new_device(NEW_LIGHT), async_add_switch + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_switch ) ) diff --git a/homeassistant/components/deluge/sensor.py b/homeassistant/components/deluge/sensor.py index 8b42b6175ce..098484cf7ae 100644 --- a/homeassistant/components/deluge/sensor.py +++ b/homeassistant/components/deluge/sensor.py @@ -84,7 +84,7 @@ class DelugeSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/demo/camera.py b/homeassistant/components/demo/camera.py index 7ac5fc17c69..0cd77b6112e 100644 --- a/homeassistant/components/demo/camera.py +++ b/homeassistant/components/demo/camera.py @@ -28,7 +28,7 @@ class DemoCamera(Camera): self._images_index = (self._images_index + 1) % 4 image_path = os.path.join( - os.path.dirname(__file__), "demo_{}.jpg".format(self._images_index) + os.path.dirname(__file__), f"demo_{self._images_index}.jpg" ) _LOGGER.debug("Loading camera_image: %s", image_path) with open(image_path, "rb") as file: diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index e3f69be3020..fb64f8015c0 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -417,7 +417,7 @@ class DemoTVShowPlayer(AbstractDemoPlayer): @property def media_title(self): """Return the title of current playing media.""" - return "Chapter {}".format(self._cur_episode) + return f"Chapter {self._cur_episode}" @property def media_series_title(self): diff --git a/homeassistant/components/demo/vacuum.py b/homeassistant/components/demo/vacuum.py index ffd3e768b11..2ba704d3925 100644 --- a/homeassistant/components/demo/vacuum.py +++ b/homeassistant/components/demo/vacuum.py @@ -244,7 +244,7 @@ class DemoVacuum(VacuumDevice): if self.supported_features & SUPPORT_SEND_COMMAND == 0: return - self._status = "Executing {}({})".format(command, params) + self._status = f"Executing {command}({params})" self._state = True self.schedule_update_ha_state() diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 5e40dbb89da..34699d666ad 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denonavr", "documentation": "https://www.home-assistant.io/components/denonavr", "requirements": [ - "denonavr==0.7.9" + "denonavr==0.7.10" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index db094bb9b12..fbe0efa15ac 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -47,7 +47,7 @@ class DeutscheBahnSensor(Entity): def __init__(self, start, goal, offset, only_direct): """Initialize the sensor.""" - self._name = "{} to {}".format(start, goal) + self._name = f"{start} to {goal}" self.data = SchieneData(start, goal, offset, only_direct) self._state = None diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index b1f319b0a6a..9508dd9c849 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,12 +1,16 @@ """Helpers for device automations.""" import asyncio import logging +from typing import Callable, cast import voluptuous as vol from homeassistant.components import websocket_api -from homeassistant.core import split_entity_id +from homeassistant.const import CONF_DOMAIN +from homeassistant.core import split_entity_id, HomeAssistant +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import async_get_integration, IntegrationNotFound DOMAIN = "device_automation" @@ -16,14 +20,34 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass, config): """Set up device automation.""" + hass.components.websocket_api.async_register_command( + websocket_device_automation_list_actions + ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_list_conditions + ) hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) return True -async def _async_get_device_automation_triggers(hass, domain, device_id): - """List device triggers.""" +async def async_device_condition_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True +) -> Callable[..., bool]: + """Wrap action method with state based condition.""" + if config_validation: + config = cv.DEVICE_CONDITION_SCHEMA(config) + integration = await async_get_integration(hass, config[CONF_DOMAIN]) + platform = integration.get_platform("device_automation") + return cast( + Callable[..., bool], + platform.async_condition_from_config(config, config_validation), # type: ignore + ) + + +async def _async_get_device_automations_from_domain(hass, domain, fname, device_id): + """List device automations.""" integration = None try: integration = await async_get_integration(hass, domain) @@ -37,19 +61,19 @@ async def _async_get_device_automation_triggers(hass, domain, device_id): # The domain does not have device automations return None - if hasattr(platform, "async_get_triggers"): - return await platform.async_get_triggers(hass, device_id) + if hasattr(platform, fname): + return await getattr(platform, fname)(hass, device_id) -async def async_get_device_automation_triggers(hass, device_id): - """List device triggers.""" +async def _async_get_device_automations(hass, fname, device_id): + """List device automations.""" device_registry, entity_registry = await asyncio.gather( hass.helpers.device_registry.async_get_registry(), hass.helpers.entity_registry.async_get_registry(), ) domains = set() - triggers = [] + automations = [] device = device_registry.async_get(device_id) for entry_id in device.config_entries: config_entry = hass.config_entries.async_get_entry(entry_id) @@ -59,28 +83,60 @@ async def async_get_device_automation_triggers(hass, device_id): for entity in entities: domains.add(split_entity_id(entity.entity_id)[0]) - device_triggers = await asyncio.gather( + device_automations = await asyncio.gather( *( - _async_get_device_automation_triggers(hass, domain, device_id) + _async_get_device_automations_from_domain(hass, domain, fname, device_id) for domain in domains ) ) - for device_trigger in device_triggers: - if device_trigger is not None: - triggers.extend(device_trigger) + for device_automation in device_automations: + if device_automation is not None: + automations.extend(device_automation) - return triggers + return automations @websocket_api.async_response @websocket_api.websocket_command( { - vol.Required("type"): "device_automation/list_triggers", + vol.Required("type"): "device_automation/action/list", + vol.Required("device_id"): str, + } +) +async def websocket_device_automation_list_actions(hass, connection, msg): + """Handle request for device actions.""" + device_id = msg["device_id"] + actions = await _async_get_device_automations(hass, "async_get_actions", device_id) + connection.send_result(msg["id"], actions) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/condition/list", + vol.Required("device_id"): str, + } +) +async def websocket_device_automation_list_conditions(hass, connection, msg): + """Handle request for device conditions.""" + device_id = msg["device_id"] + conditions = await _async_get_device_automations( + hass, "async_get_conditions", device_id + ) + connection.send_result(msg["id"], conditions) + + +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/trigger/list", vol.Required("device_id"): str, } ) async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] - triggers = await async_get_device_automation_triggers(hass, device_id) - connection.send_result(msg["id"], {"triggers": triggers}) + triggers = await _async_get_device_automations( + hass, "async_get_triggers", device_id + ) + connection.send_result(msg["id"], triggers) diff --git a/homeassistant/components/device_automation/const.py b/homeassistant/components/device_automation/const.py new file mode 100644 index 00000000000..40bfc4ca0a1 --- /dev/null +++ b/homeassistant/components/device_automation/const.py @@ -0,0 +1,8 @@ +"""Constants for device automations.""" +CONF_IS_OFF = "is_off" +CONF_IS_ON = "is_on" +CONF_TOGGLE = "toggle" +CONF_TURN_OFF = "turn_off" +CONF_TURN_ON = "turn_on" +CONF_TURNED_OFF = "turned_off" +CONF_TURNED_ON = "turned_on" diff --git a/homeassistant/components/device_automation/exceptions.py b/homeassistant/components/device_automation/exceptions.py new file mode 100644 index 00000000000..2f7c0df0187 --- /dev/null +++ b/homeassistant/components/device_automation/exceptions.py @@ -0,0 +1,6 @@ +"""Device automation exceptions.""" +from homeassistant.exceptions import HomeAssistantError + + +class InvalidDeviceAutomationConfig(HomeAssistantError): + """When device automation config is invalid.""" diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py new file mode 100644 index 00000000000..1593e70771a --- /dev/null +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -0,0 +1,186 @@ +"""Device automation helpers for toggle entity.""" +import voluptuous as vol + +import homeassistant.components.automation.state as state +from homeassistant.components.device_automation.const import ( + CONF_IS_OFF, + CONF_IS_ON, + CONF_TOGGLE, + CONF_TURN_OFF, + CONF_TURN_ON, + CONF_TURNED_OFF, + CONF_TURNED_ON, +) +from homeassistant.core import split_entity_id +from homeassistant.const import ( + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import condition, config_validation as cv, service + +ENTITY_ACTIONS = [ + { + # Turn entity off + CONF_TYPE: CONF_TURN_OFF + }, + { + # Turn entity on + CONF_TYPE: CONF_TURN_ON + }, + { + # Toggle entity + CONF_TYPE: CONF_TOGGLE + }, +] + +ENTITY_CONDITIONS = [ + { + # True when entity is turned off + CONF_CONDITION: "device", + CONF_TYPE: CONF_IS_OFF, + }, + { + # True when entity is turned on + CONF_CONDITION: "device", + CONF_TYPE: CONF_IS_ON, + }, +] + +ENTITY_TRIGGERS = [ + { + # Trigger when entity is turned off + CONF_PLATFORM: "device", + CONF_TYPE: CONF_TURNED_OFF, + }, + { + # Trigger when entity is turned on + CONF_PLATFORM: "device", + CONF_TYPE: CONF_TURNED_ON, + }, +] + +ACTION_SCHEMA = vol.Schema( + { + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_TOGGLE, CONF_TURN_OFF, CONF_TURN_ON]), + } +) + +CONDITION_SCHEMA = vol.Schema( + { + vol.Required(CONF_CONDITION): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), + } +) + +TRIGGER_SCHEMA = vol.Schema( + { + vol.Required(CONF_PLATFORM): "device", + vol.Required(CONF_DEVICE_ID): str, + vol.Required(CONF_DOMAIN): str, + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In([CONF_TURNED_OFF, CONF_TURNED_ON]), + } +) + + +def _is_domain(entity, domain): + return split_entity_id(entity.entity_id)[0] == domain + + +async def async_call_action_from_config(hass, config, variables, context, domain): + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + action_type = config[CONF_TYPE] + if action_type == CONF_TURN_ON: + action = "turn_on" + elif action_type == CONF_TURN_OFF: + action = "turn_off" + else: + action = "toggle" + + service_action = { + service.CONF_SERVICE: "{}.{}".format(domain, action), + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + } + + await service.async_call_from_config( + hass, service_action, blocking=True, variables=variables, context=context + ) + + +def async_condition_from_config(config, config_validation): + """Evaluate state based on configuration.""" + condition_type = config[CONF_TYPE] + if condition_type == CONF_IS_ON: + stat = "on" + else: + stat = "off" + state_config = { + condition.CONF_CONDITION: "state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + condition.CONF_STATE: stat, + } + + return condition.state_from_config(state_config, config_validation) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + trigger_type = config[CONF_TYPE] + if trigger_type == CONF_TURNED_ON: + from_state = "off" + to_state = "on" + else: + from_state = "on" + to_state = "off" + state_config = { + state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + + return await state.async_trigger(hass, state_config, action, automation_info) + + +async def _async_get_automations(hass, device_id, automation_templates, domain): + """List device automations.""" + automations = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entities = async_entries_for_device(entity_registry, device_id) + domain_entities = [x for x in entities if _is_domain(x, domain)] + for entity in domain_entities: + for automation in automation_templates: + automation = dict(automation) + automation.update( + device_id=device_id, entity_id=entity.entity_id, domain=domain + ) + automations.append(automation) + + return automations + + +async def async_get_actions(hass, device_id, domain): + """List device actions.""" + return await _async_get_automations(hass, device_id, ENTITY_ACTIONS, domain) + + +async def async_get_conditions(hass, device_id, domain): + """List device conditions.""" + return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) + + +async def async_get_triggers(hass, device_id, domain): + """List device triggers.""" + return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 1b71b44369d..9a058cfacc1 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -63,12 +63,14 @@ async def async_setup(hass, config): device_tracker = hass.components.device_tracker group = hass.components.group light = hass.components.light + person = hass.components.person conf = config[DOMAIN] disable_turn_off = conf.get(CONF_DISABLE_TURN_OFF) light_group = conf.get(CONF_LIGHT_GROUP, light.ENTITY_ID_ALL_LIGHTS) light_profile = conf.get(CONF_LIGHT_PROFILE) device_group = conf.get(CONF_DEVICE_GROUP, device_tracker.ENTITY_ID_ALL_DEVICES) device_entity_ids = group.get_entity_ids(device_group, device_tracker.DOMAIN) + device_entity_ids.extend(group.get_entity_ids(device_group, person.DOMAIN)) if not device_entity_ids: logger.error("No devices found to track") diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index abe5a1d500c..40ab85bc1e5 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -6,7 +6,8 @@ "dependencies": [ "device_tracker", "group", - "light" + "light", + "person" ], "codeowners": [] } diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 460f1198409..9e53c2e0cea 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -18,7 +18,7 @@ from .const import ATTR_SOURCE_TYPE, DOMAIN, LOGGER async def async_setup_entry(hass, entry): """Set up an entry.""" - component = hass.data.get(DOMAIN) # type: Optional[EntityComponent] + component: Optional[EntityComponent] = hass.data.get(DOMAIN) if component is None: component = hass.data[DOMAIN] = EntityComponent(LOGGER, DOMAIN, hass) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 67e35df00a1..5c186cc12a1 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -327,15 +327,15 @@ class DeviceTracker: class Device(RestoreEntity): """Represent a tracked device.""" - host_name = None # type: str - location_name = None # type: str - gps = None # type: GPSType - gps_accuracy = 0 # type: int - last_seen = None # type: dt_util.dt.datetime - consider_home = None # type: dt_util.dt.timedelta - battery = None # type: int - attributes = None # type: dict - icon = None # type: str + host_name: str = None + location_name: str = None + gps: GPSType = None + gps_accuracy: int = 0 + last_seen: dt_util.dt.datetime = None + consider_home: dt_util.dt.timedelta = None + battery: int = None + attributes: dict = None + icon: str = None # Track if the last update of this device was HOME. last_update_home = False @@ -532,7 +532,7 @@ class Device(RestoreEntity): class DeviceScanner: """Device scanner object.""" - hass = None # type: HomeAssistantType + hass: HomeAssistantType = None def scan_devices(self) -> List[str]: """Scan for devices.""" diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index e6edb5f63ac..6c9f05dead7 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -147,7 +147,7 @@ def async_setup_scanner_platform( scanner.hass = hass # Initial scan of each mac we also tell about host name for config - seen = set() # type: Any + seen: Any = set() async def async_device_tracker_scan(now: dt_util.dt.datetime): """Handle interval matches.""" diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 6ea5e7a46a2..aadb6b2d4cb 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -115,7 +115,7 @@ class DHTSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index d80385d0f54..9983ccc93fa 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -88,7 +88,7 @@ class DINRelay(SwitchDevice): @property def name(self): """Return the display name of this relay.""" - return "{}_{}".format(self._controller_name, self._name) + return f"{self._controller_name}_{self._name}" @property def is_on(self): diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 5f1fd335d45..827e05a424b 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -36,6 +36,7 @@ SERVICE_KONNECTED = "konnected" SERVICE_MOBILE_APP = "hass_mobile_app" SERVICE_NETGEAR = "netgear_router" SERVICE_OCTOPRINT = "octoprint" +SERVICE_PLEX = "plex_mediaserver" SERVICE_ROKU = "roku" SERVICE_SABNZBD = "sabnzbd" SERVICE_SAMSUNG_PRINTER = "samsung_printer" @@ -68,7 +69,7 @@ SERVICE_HANDLERS = { SERVICE_FREEBOX: ("freebox", None), SERVICE_YEELIGHT: ("yeelight", None), "panasonic_viera": ("media_player", "panasonic_viera"), - "plex_mediaserver": ("media_player", "plex"), + SERVICE_PLEX: ("plex", None), "yamaha": ("media_player", "yamaha"), "logitech_mediaserver": ("media_player", "squeezebox"), "directv": ("media_player", "directv"), diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 4e7b11767be..bf05d5c7f63 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -3,7 +3,7 @@ "name": "Dlna dmr", "documentation": "https://www.home-assistant.io/components/dlna_dmr", "requirements": [ - "async-upnp-client==0.14.10" + "async-upnp-client==0.14.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 3afa9c58e66..ff0bbd71194 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -20,7 +20,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "doorbird" -API_URL = "/api/{}".format(DOMAIN) +API_URL = f"/api/{DOMAIN}" CONF_CUSTOM_URL = "hass_url_override" CONF_EVENTS = "events" @@ -195,17 +195,15 @@ class ConfiguredDoorBird: return slugify(self._name) def _get_event_name(self, event): - return "{}_{}".format(self.slug, event) + return f"{self.slug}_{event}" def _register_event(self, hass_url, event): """Add a schedule entry in the device for a sensor.""" - url = "{}{}/{}?token={}".format(hass_url, API_URL, event, self._token) + url = f"{hass_url}{API_URL}/{event}?token={self._token}" # Register HA URL as webhook if not already, then get the ID if not self.webhook_is_registered(url): - self.device.change_favorite( - "http", "Home Assistant ({})".format(event), url - ) + self.device.change_favorite("http", f"Home Assistant ({event})", url) fav_id = self.get_webhook_id(url) @@ -288,9 +286,9 @@ class DoorBirdRequestView(HomeAssistantView): if event == "clear": hass.bus.async_fire(RESET_DEVICE_FAVORITES, {"token": token}) - message = "HTTP Favorites cleared for {}".format(device.slug) + message = f"HTTP Favorites cleared for {device.slug}" return web.Response(status=200, text=message) - hass.bus.async_fire("{}_{}".format(DOMAIN, event), event_data) + hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) return web.Response(status=200, text="OK") diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index a907099cba4..643e006dfef 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -45,9 +45,9 @@ class DoorBirdSwitch(SwitchDevice): def name(self): """Return the name of the switch.""" if self._relay == IR_RELAY: - return "{} IR".format(self._doorstation.name) + return f"{self._doorstation.name} IR" - return "{} Relay {}".format(self._doorstation.name, self._relay) + return f"{self._doorstation.name} Relay {self._relay}" @property def icon(self): diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 0fe589f2765..9c725d9b3a2 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -81,7 +81,7 @@ def setup(hass, config): "downloading '%s' failed, status_code=%d", url, req.status_code ) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), + f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", {"url": url, "filename": filename}, ) @@ -126,7 +126,7 @@ def setup(hass, config): while os.path.isfile(final_path): tries += 1 - final_path = "{}_{}.{}".format(path, tries, ext) + final_path = f"{path}_{tries}.{ext}" _LOGGER.debug("%s -> %s", url, final_path) @@ -136,14 +136,14 @@ def setup(hass, config): _LOGGER.debug("Downloading of %s done", url) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_COMPLETED_EVENT), + f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}", {"url": url, "filename": filename}, ) except requests.exceptions.ConnectionError: _LOGGER.exception("ConnectionError occurred for %s", url) hass.bus.fire( - "{}_{}".format(DOMAIN, DOWNLOAD_FAILED_EVENT), + f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", {"url": url, "filename": filename}, ) diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 7d677580177..171d17faff9 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -1,13 +1,17 @@ """Integrate with DuckDNS.""" -from datetime import timedelta import logging +from asyncio import iscoroutinefunction +from datetime import timedelta import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN +from homeassistant.core import callback, CALLBACK_TYPE from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -42,25 +46,28 @@ async def async_setup(hass, config): token = config[DOMAIN][CONF_ACCESS_TOKEN] session = async_get_clientsession(hass) - result = await _update_duckdns(session, domain, token) - - if not result: - return False - - async def update_domain_interval(now): + async def update_domain_interval(_now): """Update the DuckDNS entry.""" - await _update_duckdns(session, domain, token) + return await _update_duckdns(session, domain, token) + + intervals = ( + INTERVAL, + timedelta(minutes=1), + timedelta(minutes=5), + timedelta(minutes=15), + timedelta(minutes=30), + ) + async_track_time_interval_backoff(hass, update_domain_interval, intervals) async def update_domain_service(call): """Update the DuckDNS entry.""" await _update_duckdns(session, domain, token, txt=call.data[ATTR_TXT]) - async_track_time_interval(hass, update_domain_interval, INTERVAL) hass.services.async_register( DOMAIN, SERVICE_SET_TXT, update_domain_service, schema=SERVICE_TXT_SCHEMA ) - return result + return True _SENTINEL = object() @@ -89,3 +96,37 @@ async def _update_duckdns(session, domain, token, *, txt=_SENTINEL, clear=False) return False return True + + +@callback +@bind_hass +def async_track_time_interval_backoff(hass, action, intervals) -> CALLBACK_TYPE: + """Add a listener that fires repetitively at every timedelta interval.""" + if not iscoroutinefunction: + _LOGGER.error("action needs to be a coroutine and return True/False") + return + + if not isinstance(intervals, (list, tuple)): + intervals = (intervals,) + remove = None + failed = 0 + + async def interval_listener(now): + """Handle elapsed intervals with backoff.""" + nonlocal failed, remove + try: + failed += 1 + if await action(now): + failed = 0 + finally: + delay = intervals[failed] if failed < len(intervals) else intervals[-1] + remove = async_track_point_in_utc_time(hass, interval_listener, now + delay) + + hass.async_run_job(interval_listener, dt_util.utcnow()) + + def remove_listener(): + """Remove interval listener.""" + if remove: + remove() # pylint: disable=not-callable + + return remove_listener diff --git a/homeassistant/components/duke_energy/sensor.py b/homeassistant/components/duke_energy/sensor.py index b8a9bec5db8..998809decc0 100644 --- a/homeassistant/components/duke_energy/sensor.py +++ b/homeassistant/components/duke_energy/sensor.py @@ -44,7 +44,7 @@ class DukeEnergyMeter(Entity): @property def name(self): """Return the name.""" - return "duke_energy_{}".format(self.duke_meter.id) + return f"duke_energy_{self.duke_meter.id}" @property def unique_id(self): diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index a019a5c7b3a..4d7ad04e382 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -92,7 +92,7 @@ class DwdWeatherWarningsSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._var_name) + return f"{self._name} {self._var_name}" @property def icon(self): @@ -140,23 +140,23 @@ class DwdWeatherWarningsSensor(Entity): for event in self._api.data[prefix + "_warnings"]: i = i + 1 - data["warning_{}_name".format(i)] = event["event"] - data["warning_{}_level".format(i)] = event["level"] - data["warning_{}_type".format(i)] = event["type"] + data[f"warning_{i}_name"] = event["event"] + data[f"warning_{i}_level"] = event["level"] + data[f"warning_{i}_type"] = event["type"] if event["headline"]: - data["warning_{}_headline".format(i)] = event["headline"] + data[f"warning_{i}_headline"] = event["headline"] if event["description"]: - data["warning_{}_description".format(i)] = event["description"] + data[f"warning_{i}_description"] = event["description"] if event["instruction"]: - data["warning_{}_instruction".format(i)] = event["instruction"] + data[f"warning_{i}_instruction"] = event["instruction"] if event["start"] is not None: - data["warning_{}_start".format(i)] = dt_util.as_local( + data[f"warning_{i}_start"] = dt_util.as_local( dt_util.utc_from_timestamp(event["start"] / 1000) ) if event["end"] is not None: - data["warning_{}_end".format(i)] = dt_util.as_local( + data[f"warning_{i}_end"] = dt_util.as_local( dt_util.utc_from_timestamp(event["end"] / 1000) ) @@ -212,7 +212,7 @@ class DwdWeatherWarningsAPI: "Found %d %s global DWD warnings", len(json_obj[myvalue]), mykey ) - data["{}_warning_level".format(mykey)] = 0 + data[f"{mykey}_warning_level"] = 0 my_warnings = [] if self.region_id is not None: @@ -234,13 +234,13 @@ class DwdWeatherWarningsAPI: break # Get max warning level - maxlevel = data["{}_warning_level".format(mykey)] + maxlevel = data[f"{mykey}_warning_level"] for event in my_warnings: if event["level"] >= maxlevel: - data["{}_warning_level".format(mykey)] = event["level"] + data[f"{mykey}_warning_level"] = event["level"] - data["{}_warning_count".format(mykey)] = len(my_warnings) - data["{}_warnings".format(mykey)] = my_warnings + data[f"{mykey}_warning_count"] = len(my_warnings) + data[f"{mykey}_warnings"] = my_warnings _LOGGER.debug("Found %d %s local DWD warnings", len(my_warnings), mykey) diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index f89823b143f..1eb2b79c073 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -101,7 +101,7 @@ class DysonSensor(Entity): @property def unique_id(self): """Return the sensor's unique id.""" - return "{}-{}".format(self._device.serial, self._sensor_type) + return f"{self._device.serial}-{self._sensor_type}" class DysonFilterLifeSensor(DysonSensor): @@ -110,7 +110,7 @@ class DysonFilterLifeSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Filter Life sensor.""" super().__init__(device, "filter_life") - self._name = "{} Filter Life".format(self._device.name) + self._name = f"{self._device.name} Filter Life" @property def state(self): @@ -126,7 +126,7 @@ class DysonDustSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Dust sensor.""" super().__init__(device, "dust") - self._name = "{} Dust".format(self._device.name) + self._name = f"{self._device.name} Dust" @property def state(self): @@ -142,7 +142,7 @@ class DysonHumiditySensor(DysonSensor): def __init__(self, device): """Create a new Dyson Humidity sensor.""" super().__init__(device, "humidity") - self._name = "{} Humidity".format(self._device.name) + self._name = f"{self._device.name} Humidity" @property def state(self): @@ -160,7 +160,7 @@ class DysonTemperatureSensor(DysonSensor): def __init__(self, device, unit): """Create a new Dyson Temperature sensor.""" super().__init__(device, "temperature") - self._name = "{} Temperature".format(self._device.name) + self._name = f"{self._device.name} Temperature" self._unit = unit @property @@ -187,7 +187,7 @@ class DysonAirQualitySensor(DysonSensor): def __init__(self, device): """Create a new Dyson Air Quality sensor.""" super().__init__(device, "air_quality") - self._name = "{} AQI".format(self._device.name) + self._name = f"{self._device.name} AQI" @property def state(self): diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index 1482ab34c68..95c5513ecaf 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -26,10 +26,10 @@ from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) -GIGABITS = "Gb" # type: str -PRICE = "CAD" # type: str -DAYS = "days" # type: str -PERCENT = "%" # type: str +GIGABITS = "Gb" +PRICE = "CAD" +DAYS = "days" +PERCENT = "%" DEFAULT_NAME = "EBox" @@ -106,7 +106,7 @@ class EBoxSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 37b7d2dd060..ac156e040d7 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -44,7 +44,7 @@ class EbusdSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._client_name, self._name) + return f"{self._client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 0680ef67f82..b09e06bd822 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -123,7 +123,7 @@ class EcobeeWeather(WeatherEntity): if self.weather: station = self.weather.get("weatherStation", "UNKNOWN") time = self.weather.get("timestamp", "UNKNOWN") - return "Ecobee weather provided by {} at {}".format(station, time) + return f"Ecobee weather provided by {station} at {time}" return None @property diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 53c89097a59..43c3b67457a 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -99,7 +99,7 @@ class EfergySensor(Entity): """Initialize the sensor.""" self.sid = sid if sid: - self._name = "efergy_{}".format(sid) + self._name = f"efergy_{sid}" else: self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type @@ -109,7 +109,7 @@ class EfergySensor(Entity): self.period = period self.currency = currency if self.type == "cost": - self._unit_of_measurement = "{}/{}".format(self.currency, self.period) + self._unit_of_measurement = f"{self.currency}/{self.period}" else: self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] @@ -132,7 +132,7 @@ class EfergySensor(Entity): """Get the Efergy monitor data from the web service.""" try: if self.type == "instant_readings": - url_string = "{}getInstant?token={}".format(_RESOURCE, self.app_token) + url_string = f"{_RESOURCE}getInstant?token={self.app_token}" response = requests.get(url_string, timeout=10) self._state = response.json()["reading"] elif self.type == "amount": @@ -142,7 +142,7 @@ class EfergySensor(Entity): response = requests.get(url_string, timeout=10) self._state = response.json()["sum"] elif self.type == "budget": - url_string = "{}getBudget?token={}".format(_RESOURCE, self.app_token) + url_string = f"{_RESOURCE}getBudget?token={self.app_token}" response = requests.get(url_string, timeout=10) self._state = response.json()["status"] elif self.type == "cost": diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index e17ea8f065d..9e11f522dd5 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -110,7 +110,7 @@ def setup(hass, config): server = egardiaserver.EgardiaServer("", rs_port) bound = server.bind() if not bound: - raise IOError( + raise OSError( "Binding error occurred while " + "starting EgardiaServer." ) hass.data[EGARDIA_SERVER] = server @@ -123,7 +123,7 @@ def setup(hass, config): # listen to home assistant stop event hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event) - except IOError: + except OSError: _LOGGER.error("Binding error occurred while starting EgardiaServer") return False diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 2479ea5440f..923c3f7d309 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -141,8 +141,8 @@ async def async_setup(hass, config): for user in eight.users: obj = eight.users[user] for sensor in SENSORS: - sensors.append("{}_{}".format(obj.side, sensor)) - binary_sensors.append("{}_presence".format(obj.side)) + sensors.append(f"{obj.side}_{sensor}") + binary_sensors.append(f"{obj.side}_presence") sensors.append("room_temp") else: # No users, cannot continue diff --git a/homeassistant/components/eight_sleep/binary_sensor.py b/homeassistant/components/eight_sleep/binary_sensor.py index 7d7ebecafee..7b801578ccd 100644 --- a/homeassistant/components/eight_sleep/binary_sensor.py +++ b/homeassistant/components/eight_sleep/binary_sensor.py @@ -34,7 +34,7 @@ class EightHeatSensor(EightSleepHeatEntity, BinarySensorDevice): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._side = self._sensor.split("_")[0] diff --git a/homeassistant/components/eight_sleep/sensor.py b/homeassistant/components/eight_sleep/sensor.py index afc06986ea6..d3d54fd58ca 100644 --- a/homeassistant/components/eight_sleep/sensor.py +++ b/homeassistant/components/eight_sleep/sensor.py @@ -68,7 +68,7 @@ class EightHeatSensor(EightSleepHeatEntity): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._side = self._sensor.split("_")[0] @@ -122,7 +122,7 @@ class EightUserSensor(EightSleepUserEntity): self._sensor = sensor self._sensor_root = self._sensor.split("_", 1)[1] self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._attr = None self._units = units @@ -261,7 +261,7 @@ class EightRoomSensor(EightSleepUserEntity): self._sensor = sensor self._mapped_name = NAME_MAP.get(self._sensor, self._sensor) - self._name = "{} {}".format(name, self._mapped_name) + self._name = f"{name} {self._mapped_name}" self._state = None self._attr = None self._units = units diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index e26749e6f6b..d15399df67b 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -146,7 +146,7 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: def _included(ranges, set_to, values): for rng in ranges: if not rng[0] <= rng[1] <= len(values): - raise vol.Invalid("Invalid range {}".format(rng)) + raise vol.Invalid(f"Invalid range {rng}") values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1) for index, conf in enumerate(hass_config[DOMAIN]): @@ -250,7 +250,7 @@ class ElkEntity(Entity): # we could have used elkm1__foo_bar for the latter, but that # would have been a breaking change if self._prefix != "": - uid_start = "elkm1m_{prefix}".format(prefix=self._prefix) + uid_start = f"elkm1m_{self._prefix}" else: uid_start = "elkm1" self._unique_id = "{uid_start}_{name}".format( @@ -260,7 +260,7 @@ class ElkEntity(Entity): @property def name(self): """Name of the element.""" - return "{p}{n}".format(p=self._prefix, n=self._element.name) + return f"{self._prefix}{self._element.name}" @property def unique_id(self): diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 275d94efa66..927ed53115e 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -59,7 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _dispatch(signal, entity_ids, *args): for entity_id in entity_ids: - async_dispatcher_send(hass, "{}_{}".format(signal, entity_id), *args) + async_dispatcher_send(hass, f"{signal}_{entity_id}", *args) def _arm_service(service): entity_ids = service.data.get(ATTR_ENTITY_ID, []) @@ -117,13 +117,11 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): for keypad in self._elk.keypads: keypad.add_callback(self._watch_keypad) async_dispatcher_connect( - self.hass, - "{}_{}".format(SIGNAL_ARM_ENTITY, self.entity_id), - self._arm_service, + self.hass, f"{SIGNAL_ARM_ENTITY}_{self.entity_id}", self._arm_service ) async_dispatcher_connect( self.hass, - "{}_{}".format(SIGNAL_DISPLAY_MESSAGE, self.entity_id), + f"{SIGNAL_DISPLAY_MESSAGE}_{self.entity_id}", self._display_message, ) diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 409dd8ec472..d8a98a96585 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -209,8 +209,7 @@ class EmbyDevice(MediaPlayerDevice): def name(self): """Return the name of the device.""" return ( - "Emby - {} - {}".format(self.device.client, self.device.name) - or DEVICE_DEFAULT_NAME + f"Emby - {self.device.client} - {self.device.name}" or DEVICE_DEFAULT_NAME ) @property diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 8d79b771fb9..5f9d31697b8 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -135,7 +135,7 @@ class EmonCmsSensor(Entity): id_for_name = "" if str(sensorid) == "1" else sensorid # Use the feed name assigned in EmonCMS or fall back to the feed ID feed_name = elem.get("name") or "Feed {}".format(elem["id"]) - self._name = "EmonCMS{} {}".format(id_for_name, feed_name) + self._name = f"EmonCMS{id_for_name} {feed_name}" else: self._name = name self._identifier = get_id( @@ -225,7 +225,7 @@ class EmonCmsData: def __init__(self, hass, url, apikey, interval): """Initialize the data object.""" self._apikey = apikey - self._url = "{}/feed/list.json".format(url) + self._url = f"{url}/feed/list.json" self._interval = interval self._hass = hass self.data = None diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 779a25872f9..3b30a29960b 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -47,7 +47,7 @@ def setup(hass, config): def send_data(url, apikey, node, payload): """Send payload data to Emoncms.""" try: - fullurl = "{}/input/post.json".format(url) + fullurl = f"{url}/input/post.json" data = {"apikey": apikey, "data": payload} parameters = {"node": node} req = requests.post( @@ -83,7 +83,7 @@ def setup(hass, config): if payload_dict: payload = "{%s}" % ",".join( - "{}:{}".format(key, val) for key, val in payload_dict.items() + f"{key}:{val}" for key, val in payload_dict.items() ) send_data( diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index fc00746fc7f..5d08af6c5ee 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -197,11 +197,19 @@ class HueOneLightStateView(HomeAssistantView): return self.json_message("only local IPs allowed", HTTP_BAD_REQUEST) hass = request.app["hass"] - entity_id = self.config.number_to_entity_id(entity_id) - entity = hass.states.get(entity_id) + hass_entity_id = self.config.number_to_entity_id(entity_id) + + if hass_entity_id is None: + _LOGGER.error( + "Unknown entity number: %s not found in emulated_hue_ids.json", + entity_id, + ) + return web.Response(text="Entity not found", status=404) + + entity = hass.states.get(hass_entity_id) if entity is None: - _LOGGER.error("Entity not found: %s", entity_id) + _LOGGER.error("Entity not found: %s", hass_entity_id) return web.Response(text="Entity not found", status=404) if not self.config.is_entity_exposed(entity): @@ -590,5 +598,5 @@ def entity_to_json(config, entity, state): def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" - success_key = "/lights/{}/state/{}".format(entity_id, attr) + success_key = f"/lights/{entity_id}/state/{attr}" return {"success": {success_key: value}} diff --git a/homeassistant/components/emulated_roku/.translations/it.json b/homeassistant/components/emulated_roku/.translations/it.json index cba89add799..8f39309264a 100644 --- a/homeassistant/components/emulated_roku/.translations/it.json +++ b/homeassistant/components/emulated_roku/.translations/it.json @@ -6,8 +6,12 @@ "step": { "user": { "data": { + "advertise_ip": "Pubblicizza IP", + "advertise_port": "Pubblicizza porta", "host_ip": "Indirizzo IP dell'host", - "name": "Nome" + "listen_port": "Porta di ascolto", + "name": "Nome", + "upnp_bind_multicast": "Associa multicast (Vero / Falso)" }, "title": "Definisci la configurazione del server" } diff --git a/homeassistant/components/emulated_roku/.translations/no.json b/homeassistant/components/emulated_roku/.translations/no.json index e83497599ca..b41da3ccde3 100644 --- a/homeassistant/components/emulated_roku/.translations/no.json +++ b/homeassistant/components/emulated_roku/.translations/no.json @@ -11,7 +11,7 @@ "host_ip": "Vert IP", "listen_port": "Lytte port", "name": "Navn", - "upnp_bind_multicast": "Bind multicast (True/False)" + "upnp_bind_multicast": "Bind multicast (Sant/Usant)" }, "title": "Definer serverkonfigurasjon" } diff --git a/homeassistant/components/entur_public_transport/sensor.py b/homeassistant/components/entur_public_transport/sensor.py index 46ba62ba3fa..0f8324ded9e 100644 --- a/homeassistant/components/entur_public_transport/sensor.py +++ b/homeassistant/components/entur_public_transport/sensor.py @@ -121,7 +121,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: given_name = "{} {}".format(name, data.get_stop_info(place).name) except KeyError: - given_name = "{} {}".format(name, place) + given_name = f"{name} {place}" entities.append( EnturPublicTransportSensor(proxy, given_name, place, show_on_map) diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index ebb6b0cd51f..a4fad083d2a 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -20,7 +20,6 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS -from homeassistant.util import Throttle import homeassistant.util.dt as dt import homeassistant.helpers.config_validation as cv @@ -30,8 +29,6 @@ CONF_FORECAST = "forecast" CONF_ATTRIBUTION = "Data provided by Environment Canada" CONF_STATION = "station" -MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(minutes=10) - def validate_station(station): """Check that the station ID is well-formed.""" @@ -171,7 +168,6 @@ class ECWeather(WeatherEntity): """Return the forecast array.""" return get_forecast(self.ec_data, self.forecast_type) - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Environment Canada.""" self.ec_data.update() diff --git a/homeassistant/components/esphome/.translations/it.json b/homeassistant/components/esphome/.translations/it.json index b9088c2eadc..bb77e87f6a1 100644 --- a/homeassistant/components/esphome/.translations/it.json +++ b/homeassistant/components/esphome/.translations/it.json @@ -18,7 +18,7 @@ "title": "Inserisci la password" }, "discovery_confirm": { - "description": "Vuoi aggiungere il nodo ESPHome ` {name} ` a Home Assistant?", + "description": "Vuoi aggiungere il nodo ESPHome `{name}` a Home Assistant?", "title": "Trovato nodo ESPHome" }, "user": { diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json index c8e6012ea94..9394b5af543 100644 --- a/homeassistant/components/esphome/.translations/pl.json +++ b/homeassistant/components/esphome/.translations/pl.json @@ -26,7 +26,7 @@ "host": "Host", "port": "Port" }, - "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia swojego [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.", + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia [ESPHome](https://esphomelib.com/) w\u0119z\u0142a.", "title": "ESPHome" } }, diff --git a/homeassistant/components/esphome/.translations/zh-Hant.json b/homeassistant/components/esphome/.translations/zh-Hant.json index 74d0b925fb2..0386fd8c468 100644 --- a/homeassistant/components/esphome/.translations/zh-Hant.json +++ b/homeassistant/components/esphome/.translations/zh-Hant.json @@ -18,7 +18,7 @@ "title": "\u8f38\u5165\u5bc6\u78bc" }, "discovery_confirm": { - "description": "\u662f\u5426\u8981\u5c07 ESPHome \u7bc0\u9ede\u300c{name}\u300d\u65b0\u589e\u81f3 Home Assistant\uff1f", + "description": "\u662f\u5426\u8981\u5c07 ESPHome \u7bc0\u9ede `{name}` \u65b0\u589e\u81f3 Home Assistant\uff1f", "title": "\u767c\u73fe\u5230 ESPHome \u7bc0\u9ede" }, "user": { diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8780d2b67ae..bc06aba94ea 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -80,7 +80,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool host, port, password, - client_info="Home Assistant {}".format(const.__version__), + client_info=f"Home Assistant {const.__version__}", ) # Store client in per-config-entry hass.data @@ -203,7 +203,7 @@ async def _setup_auto_reconnect_logic( # When removing/disconnecting manually return - data = hass.data[DOMAIN][entry.entry_id] # type: RuntimeEntryData + data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] for disconnect_cb in data.disconnect_callbacks: disconnect_cb() data.disconnect_callbacks = [] @@ -254,7 +254,7 @@ async def _async_setup_device_registry( """Set up device registry feature for a particular config entry.""" sw_version = device_info.esphome_version if device_info.compilation_time: - sw_version += " ({})".format(device_info.compilation_time) + sw_version += f" ({device_info.compilation_time})" device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -269,7 +269,7 @@ async def _async_setup_device_registry( async def _register_service( hass: HomeAssistantType, entry_data: RuntimeEntryData, service: UserService ): - service_name = "{}_{}".format(entry_data.device_info.name, service.name) + service_name = f"{entry_data.device_info.name}_{service.name}" schema = {} for arg in service.args: schema[vol.Required(arg.name)] = { @@ -315,7 +315,7 @@ async def _setup_services( entry_data.services = {serv.key: serv for serv in services} for service in to_unregister: - service_name = "{}_{}".format(entry_data.device_info.name, service.name) + service_name = f"{entry_data.device_info.name}_{service.name}" hass.services.async_remove(DOMAIN, service_name) for service in to_register: @@ -326,7 +326,7 @@ async def _cleanup_instance( hass: HomeAssistantType, entry: ConfigEntry ) -> RuntimeEntryData: """Cleanup the esphome client if it exists.""" - data = hass.data[DATA_KEY].pop(entry.entry_id) # type: RuntimeEntryData + data: RuntimeEntryData = hass.data[DATA_KEY].pop(entry.entry_id) if data.reconnect_task is not None: data.reconnect_task.cancel() for disconnect_cb in data.disconnect_callbacks: @@ -363,7 +363,7 @@ async def platform_async_setup_entry( This method is in charge of receiving, distributing and storing info and state updates. """ - entry_data = hass.data[DOMAIN][entry.entry_id] # type: RuntimeEntryData + entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] entry_data.info[component_key] = {} entry_data.state[component_key] = {} @@ -468,7 +468,7 @@ class EsphomeEntity(Entity): self._entry_id = entry_id self._component_key = component_key self._key = key - self._remove_callbacks = [] # type: List[Callable[[], None]] + self._remove_callbacks: List[Callable[[], None]] = [] async def async_added_to_hass(self) -> None: """Register callbacks.""" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 35389d055d6..9680ed46acd 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -19,9 +19,9 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize flow.""" - self._host = None # type: Optional[str] - self._port = None # type: Optional[int] - self._password = None # type: Optional[str] + self._host: Optional[str] = None + self._port: Optional[int] = None + self._password: Optional[str] = None async def async_step_user( self, user_input: Optional[ConfigType] = None, error: Optional[str] = None @@ -94,9 +94,7 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): already_configured = True elif entry.entry_id in self.hass.data.get(DATA_KEY, {}): # Does a config entry with this name already exist? - data = self.hass.data[DATA_KEY][ - entry.entry_id - ] # type: RuntimeEntryData + data: RuntimeEntryData = self.hass.data[DATA_KEY][entry.entry_id] # Node names are unique in the network if data.device_info is not None: already_configured = data.device_info.name == node_name diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py index 83d3164e3ff..b106d9d2ae6 100644 --- a/homeassistant/components/essent/sensor.py +++ b/homeassistant/components/essent/sensor.py @@ -95,7 +95,7 @@ class EssentMeter(Entity): @property def name(self): """Return the name of the sensor.""" - return "Essent {} ({})".format(self._type, self._tariff) + return f"Essent {self._type} ({self._tariff})" @property def state(self): diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 21629360ac7..506617e4c60 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -87,7 +87,7 @@ class EverLightsLight(Light): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}-{}".format(self._mac, self._channel) + return f"{self._mac}-{self._channel}" @property def available(self) -> bool: @@ -102,7 +102,7 @@ class EverLightsLight(Light): @property def is_on(self): """Return true if device is on.""" - return self._status["ch{}Active".format(self._channel)] == 1 + return self._status[f"ch{self._channel}Active"] == 1 @property def brightness(self): diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 05308782362..ba7a72024ed 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -2,14 +2,13 @@ Such systems include evohome (multi-zone), and Round Thermostat (single zone). """ -import asyncio from datetime import datetime, timedelta import logging from typing import Any, Dict, Optional, Tuple -import requests.exceptions +import aiohttp.client_exceptions import voluptuous as vol -import evohomeclient2 +import evohomeasync2 from homeassistant.const import ( CONF_ACCESS_TOKEN, @@ -21,17 +20,10 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import ( - async_track_point_in_utc_time, - track_time_interval, -) from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime, utcnow @@ -81,55 +73,60 @@ def _handle_exception(err) -> bool: try: raise err - except evohomeclient2.AuthenticationError: + except evohomeasync2.AuthenticationError: _LOGGER.error( "Failed to (re)authenticate with the vendor's server. " + "Check your network and the vendor's service status page. " "Check that your username and password are correct. " "Message is: %s", err, ) return False - except requests.exceptions.ConnectionError: + except aiohttp.ClientConnectionError: # this appears to be common with Honeywell's servers _LOGGER.warning( "Unable to connect with the vendor's server. " - "Check your network and the vendor's status page." + "Check your network and the vendor's service status page. " "Message is: %s", err, ) return False - except requests.exceptions.HTTPError: - if err.response.status_code == HTTP_SERVICE_UNAVAILABLE: + except aiohttp.ClientResponseError: + if err.status == HTTP_SERVICE_UNAVAILABLE: _LOGGER.warning( - "Vendor says their server is currently unavailable. " - "Check the vendor's status page." + "The vendor says their server is currently unavailable. " + "Check the vendor's service status page." ) return False - if err.response.status_code == HTTP_TOO_MANY_REQUESTS: + if err.status == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning( "The vendor's API rate limit has been exceeded. " - "Consider increasing the %s.", + "If this message persists, consider increasing the %s.", CONF_SCAN_INTERVAL, ) return False - raise # we don't expect/handle any other HTTPErrors + raise # we don't expect/handle any other ClientResponseError -def setup(hass: HomeAssistantType, hass_config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell evohome system.""" - broker = EvoBroker(hass, hass_config[DOMAIN]) - if not broker.init_client(): + broker = EvoBroker(hass, config[DOMAIN]) + if not await broker.init_client(): return False - load_platform(hass, "climate", DOMAIN, {}, hass_config) + hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config)) if broker.tcs.hotwater: - load_platform(hass, "water_heater", DOMAIN, {}, hass_config) + hass.async_create_task( + async_load_platform(hass, "water_heater", DOMAIN, {}, config) + ) - track_time_interval(hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL]) + hass.helpers.event.async_track_time_interval( + broker.update, config[DOMAIN][CONF_SCAN_INTERVAL] + ) return True @@ -141,8 +138,7 @@ class EvoBroker: """Initialize the evohome client and data structure.""" self.hass = hass self.params = params - - self.config = self.status = self.timers = {} + self.config = {} self.client = self.tcs = None self._app_storage = {} @@ -150,32 +146,31 @@ class EvoBroker: hass.data[DOMAIN] = {} hass.data[DOMAIN]["broker"] = self - def init_client(self) -> bool: + async def init_client(self) -> bool: """Initialse the evohome data broker. Return True if this is successful, otherwise return False. """ - refresh_token, access_token, access_token_expires = asyncio.run_coroutine_threadsafe( - self._load_auth_tokens(), self.hass.loop - ).result() + refresh_token, access_token, access_token_expires = ( + await self._load_auth_tokens() + ) - # evohomeclient2 uses naive/local datetimes + # evohomeasync2 uses naive/local datetimes if access_token_expires is not None: access_token_expires = _utc_to_local_dt(access_token_expires) - try: - client = self.client = evohomeclient2.EvohomeClient( - self.params[CONF_USERNAME], - self.params[CONF_PASSWORD], - refresh_token=refresh_token, - access_token=access_token, - access_token_expires=access_token_expires, - ) + client = self.client = evohomeasync2.EvohomeClient( + self.params[CONF_USERNAME], + self.params[CONF_PASSWORD], + refresh_token=refresh_token, + access_token=access_token, + access_token_expires=access_token_expires, + session=async_get_clientsession(self.hass), + ) - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: + try: + await client.login() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: if not _handle_exception(err): return False @@ -200,17 +195,14 @@ class EvoBroker: return False self.tcs = ( - client.locations[loc_idx] # noqa: E501; pylint: disable=protected-access + client.locations[loc_idx] # pylint: disable=protected-access ._gateways[0] ._control_systems[0] ) _LOGGER.debug("Config = %s", self.config) - if _LOGGER.isEnabledFor(logging.DEBUG): - # don't do an I/O unless required - _LOGGER.debug( - "Status = %s", client.locations[loc_idx].status()[GWS][0][TCS][0] - ) + if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required + await self.update() # includes: _LOGGER.debug("Status = %s"... return True @@ -237,7 +229,7 @@ class EvoBroker: return (None, None, None) # account switched: so tokens wont be valid async def _save_auth_tokens(self, *args) -> None: - # evohomeclient2 uses naive/local datetimes + # evohomeasync2 uses naive/local datetimes access_token_expires = _local_dt_to_utc(self.client.access_token_expires) self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] @@ -248,13 +240,12 @@ class EvoBroker: store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save(self._app_storage) - async_track_point_in_utc_time( - self.hass, + self.hass.helpers.event.async_track_point_in_utc_time( self._save_auth_tokens, access_token_expires + self.params[CONF_SCAN_INTERVAL], ) - def update(self, *args, **kwargs) -> None: + async def update(self, *args, **kwargs) -> None: """Get the latest state data of the entire evohome Location. This includes state data for the Controller and all its child devices, @@ -264,19 +255,16 @@ class EvoBroker: loc_idx = self.params[CONF_LOCATION_IDX] try: - status = self.client.locations[loc_idx].status()[GWS][0][TCS][0] - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: + status = await self.client.locations[loc_idx].status() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: _handle_exception(err) else: - self.timers["statusUpdated"] = utcnow() - - _LOGGER.debug("Status = %s", status) - # inform the evohome devices that state data has been updated - async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) + self.hass.helpers.dispatcher.async_dispatcher_send( + DOMAIN, {"signal": "refresh"} + ) + + _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) class EvoDevice(Entity): @@ -289,6 +277,7 @@ class EvoDevice(Entity): def __init__(self, evo_broker, evo_device) -> None: """Initialize the evohome entity.""" self._evo_device = evo_device + self._evo_broker = evo_broker self._evo_tcs = evo_broker.tcs self._name = self._icon = self._precision = None @@ -341,7 +330,7 @@ class EvoDevice(Entity): switchpoint = day["Switchpoints"][idx] dt_naive = datetime.strptime( - "{}T{}".format(sp_date, switchpoint["TimeOfDay"]), "%Y-%m-%dT%H:%M:%S" + f"{sp_date}T{switchpoint['TimeOfDay']}", "%Y-%m-%dT%H:%M:%S" ) spt["from"] = _local_dt_to_utc(dt_naive).isoformat() @@ -387,7 +376,7 @@ class EvoDevice(Entity): async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + self.hass.helpers.dispatcher.async_dispatcher_connect(DOMAIN, self._refresh) @property def precision(self) -> float: @@ -399,14 +388,27 @@ class EvoDevice(Entity): """Return the temperature unit to use in the frontend UI.""" return TEMP_CELSIUS - def _update_schedule(self) -> None: + async def _call_client_api(self, api_function) -> None: + try: + await api_function + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) + + self.hass.helpers.event.async_call_later( + 2, self._evo_broker.update() + ) # call update() in 2 seconds + + async def _update_schedule(self) -> None: """Get the latest state data.""" if ( not self._schedule.get("DailySchedules") or parse_datetime(self.setpoints["next"]["from"]) < utcnow() ): - self._schedule = self._evo_device.schedule() + try: + self._schedule = await self._evo_device.schedule() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) - def update(self) -> None: + async def async_update(self) -> None: """Get the latest state data.""" - self._update_schedule() + await self._update_schedule() diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index d1b9d5f54c7..0264f76f38f 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -3,9 +3,6 @@ from datetime import datetime import logging from typing import Any, Dict, Optional, List -import requests.exceptions -import evohomeclient2 - from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, @@ -25,7 +22,7 @@ from homeassistant.const import PRECISION_TENTHS from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, _handle_exception, EvoDevice +from . import CONF_LOCATION_IDX, EvoDevice from .const import ( DOMAIN, EVO_RESET, @@ -65,10 +62,13 @@ EVO_PRESET_TO_HA = { HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items()} -def setup_platform( - hass: HomeAssistantType, hass_config: ConfigType, add_entities, discovery_info=None +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ) -> None: """Create the evohome Controller, and its Zones, if any.""" + if discovery_info is None: + return + broker = hass.data[DOMAIN]["broker"] loc_idx = broker.params[CONF_LOCATION_IDX] @@ -91,7 +91,7 @@ def setup_platform( zone.name, ) - add_entities([EvoThermostat(broker, zone)], update_before_add=True) + async_add_entities([EvoThermostat(broker, zone)], update_before_add=True) return controller = EvoController(broker, broker.tcs) @@ -107,7 +107,7 @@ def setup_platform( ) zones.append(EvoZone(broker, zone)) - add_entities([controller] + zones, update_before_add=True) + async_add_entities([controller] + zones, update_before_add=True) class EvoClimateDevice(EvoDevice, ClimateDevice): @@ -119,22 +119,18 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): self._preset_modes = None - def _set_temperature( + async def _set_temperature( self, temperature: float, until: Optional[datetime] = None ) -> None: """Set a new target temperature for the Zone. until == None means indefinitely (i.e. PermanentOverride) """ - try: + await self._call_client_api( self._evo_device.set_temperature(temperature, until) - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: - _handle_exception(err) + ) - def _set_zone_mode(self, op_mode: str) -> None: + async def _set_zone_mode(self, op_mode: str) -> None: """Set a Zone to one of its native EVO_* operating modes. Zones inherit their _effective_ operating mode from the Controller. @@ -153,35 +149,24 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): (by default) 5C, and 'Away', Zones to (by default) 12C. """ if op_mode == EVO_FOLLOW: - try: - self._evo_device.cancel_temp_override() - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: - _handle_exception(err) + await self._call_client_api(self._evo_device.cancel_temp_override()) return temperature = self._evo_device.setpointStatus["targetHeatTemperature"] until = None # EVO_PERMOVER if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: - self._update_schedule() + await self._update_schedule() if self._schedule["DailySchedules"]: until = parse_datetime(self.setpoints["next"]["from"]) - self._set_temperature(temperature, until=until) + await self._set_temperature(temperature, until=until) - def _set_tcs_mode(self, op_mode: str) -> None: + async def _set_tcs_mode(self, op_mode: str) -> None: """Set the Controller to any of its native EVO_* operating modes.""" - try: - # noqa: E501; pylint: disable=protected-access - self._evo_tcs._set_status(op_mode) - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: - _handle_exception(err) + await self._call_client_api( + self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access + ) @property def hvac_modes(self) -> List[str]: @@ -216,6 +201,11 @@ class EvoZone(EvoClimateDevice): self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._evo_device.temperatureStatus["isAvailable"] + @property def hvac_mode(self) -> str: """Return the current operating mode of the evohome Zone.""" @@ -276,28 +266,28 @@ class EvoZone(EvoClimateDevice): """ return self._evo_device.setpointCapabilities["maxHeatSetpoint"] - def set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs) -> None: """Set a new target temperature.""" until = kwargs.get("until") if until: until = parse_datetime(until) - self._set_temperature(kwargs["temperature"], until) + await self._set_temperature(kwargs["temperature"], until) - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode for the Zone.""" if hvac_mode == HVAC_MODE_OFF: - self._set_temperature(self.min_temp, until=None) + await self._set_temperature(self.min_temp, until=None) else: # HVAC_MODE_HEAT - self._set_zone_mode(EVO_FOLLOW) + await self._set_zone_mode(EVO_FOLLOW) - def set_preset_mode(self, preset_mode: Optional[str]) -> None: + async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to following the schedule. """ - self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) class EvoController(EvoClimateDevice): @@ -344,25 +334,25 @@ class EvoController(EvoClimateDevice): """Return the current preset mode, e.g., home, away, temp.""" return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus["mode"]) - def set_temperature(self, **kwargs) -> None: + async def async_set_temperature(self, **kwargs) -> None: """Do nothing. The evohome Controller doesn't have a target temperature. """ return - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode for the Controller.""" - self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) - def set_preset_mode(self, preset_mode: Optional[str]) -> None: + async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to 'Auto' mode. """ - self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) + await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) - def update(self) -> None: + async def async_update(self) -> None: """Get the latest state data.""" return @@ -409,16 +399,16 @@ class EvoThermostat(EvoZone): return super().preset_mode - def set_hvac_mode(self, hvac_mode: str) -> None: + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode.""" - self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + await self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) - def set_preset_mode(self, preset_mode: Optional[str]) -> None: + async def async_set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to following the schedule. """ if preset_mode in list(HA_PRESET_TO_TCS): - self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) + await self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) else: - self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + await self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 078d4ace776..32a57cf20b1 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,7 +3,7 @@ "name": "Evohome", "documentation": "https://www.home-assistant.io/components/evohome", "requirements": [ - "evohomeclient==0.3.3" + "evohome-async==0.3.3b4" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 6309f07a000..1b37bc3b2b5 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -2,9 +2,6 @@ import logging from typing import List -import requests.exceptions -import evohomeclient2 - from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, WaterHeaterDevice, @@ -12,7 +9,7 @@ from homeassistant.components.water_heater import ( from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.util.dt import parse_datetime -from . import _handle_exception, EvoDevice +from . import EvoDevice from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER _LOGGER = logging.getLogger(__name__) @@ -23,8 +20,13 @@ EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items()} HA_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER} -def setup_platform(hass, hass_config, add_entities, discovery_info=None) -> None: +async def async_setup_platform( + hass, config, async_add_entities, discovery_info=None +) -> None: """Create the DHW controller.""" + if discovery_info is None: + return + broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( @@ -33,7 +35,7 @@ def setup_platform(hass, hass_config, add_entities, discovery_info=None) -> None evo_dhw = EvoDHW(broker, broker.tcs.hotwater) - add_entities([evo_dhw], update_before_add=True) + async_add_entities([evo_dhw], update_before_add=True) class EvoDHW(EvoDevice, WaterHeaterDevice): @@ -58,6 +60,11 @@ class EvoDHW(EvoDevice, WaterHeaterDevice): self._supported_features = SUPPORT_OPERATION_MODE self._operation_list = list(HA_OPMODE_TO_DHW) + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._evo_device.temperatureStatus.get("isAvailable", False) + @property def current_operation(self) -> str: """Return the current operating mode (On, or Off).""" @@ -73,7 +80,7 @@ class EvoDHW(EvoDevice, WaterHeaterDevice): """Return the current temperature.""" return self._evo_device.temperatureStatus["temperature"] - def set_operation_mode(self, operation_mode: str) -> None: + async def async_set_operation_mode(self, operation_mode: str) -> None: """Set new operation mode for a DHW controller.""" op_mode = HA_OPMODE_TO_DHW[operation_mode] @@ -81,17 +88,13 @@ class EvoDHW(EvoDevice, WaterHeaterDevice): until = None # EVO_FOLLOW, EVO_PERMOVER if op_mode == EVO_TEMPOVER and self._schedule["DailySchedules"]: - self._update_schedule() + await self._update_schedule() if self._schedule["DailySchedules"]: until = parse_datetime(self.setpoints["next"]["from"]) until = until.strftime(EVO_STRFTIME) data = {"Mode": op_mode, "State": state, "UntilTime": until} - try: + await self._call_client_api( self._evo_device._set_dhw(data) # pylint: disable=protected-access - except ( - requests.exceptions.RequestException, - evohomeclient2.AuthenticationError, - ) as err: - _handle_exception(err) + ) diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index a1e686bcbd0..228cae2f19d 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -168,7 +168,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config[CONF_PORT] username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - url_health = "http://{}:{}/healthz".format(ip_address, port) + url_health = f"http://{ip_address}:{port}/healthz" hostname = check_box_health(url_health, username, password) if hostname is None: return @@ -214,8 +214,8 @@ class FaceClassifyEntity(ImageProcessingFaceEntity): ): """Init with the API key and model id.""" super().__init__() - self._url_check = "http://{}:{}/{}/check".format(ip_address, port, CLASSIFIER) - self._url_teach = "http://{}:{}/{}/teach".format(ip_address, port, CLASSIFIER) + self._url_check = f"http://{ip_address}:{port}/{CLASSIFIER}/check" + self._url_teach = f"http://{ip_address}:{port}/{CLASSIFIER}/teach" self._username = username self._password = password self._hostname = hostname @@ -224,7 +224,7 @@ class FaceClassifyEntity(ImageProcessingFaceEntity): self._name = name else: camera_name = split_entity_id(camera_entity)[1] - self._name = "{} {}".format(CLASSIFIER, camera_name) + self._name = f"{CLASSIFIER} {camera_name}" self._matched = {} def process_image(self, image): diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index d5e3d6064ea..2dc528b2cff 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -57,7 +57,7 @@ class BanSensor(Entity): def __init__(self, name, jail, log_parser): """Initialize the sensor.""" - self._name = "{} {}".format(name, jail) + self._name = f"{name} {jail}" self.jail = jail self.ban_dict = {STATE_CURRENT_BANS: [], STATE_ALL_BANS: []} self.last_ban = None diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 50d698f7336..82f4d37938c 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -55,23 +55,21 @@ PROP_TO_ATTR = { "speed_list": ATTR_SPEED_LIST, "oscillating": ATTR_OSCILLATING, "current_direction": ATTR_DIRECTION, -} # type: dict +} FAN_SET_SPEED_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( {vol.Required(ATTR_SPEED): cv.string} -) # type: dict +) -FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {vol.Optional(ATTR_SPEED): cv.string} -) # type: dict +FAN_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({vol.Optional(ATTR_SPEED): cv.string}) FAN_OSCILLATE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( {vol.Required(ATTR_OSCILLATING): cv.boolean} -) # type: dict +) FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( {vol.Optional(ATTR_DIRECTION): cv.string} -) # type: dict +) @bind_hass @@ -198,7 +196,7 @@ class FanEntity(ToggleEntity): @property def state_attributes(self) -> dict: """Return optional state attributes.""" - data = {} # type: dict + data = {} for prop, attr in PROP_TO_ATTR.items(): if not hasattr(self, prop): diff --git a/homeassistant/components/fastdotcom/__init__.py b/homeassistant/components/fastdotcom/__init__.py index a40c2597222..b070eef0310 100644 --- a/homeassistant/components/fastdotcom/__init__.py +++ b/homeassistant/components/fastdotcom/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval DOMAIN = "fastdotcom" -DATA_UPDATED = "{}_data_updated".format(DOMAIN) +DATA_UPDATED = f"{DOMAIN}_data_updated" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index cdd76a56e16..44ec95f8213 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -44,7 +44,7 @@ def setup(hass, config): urls = config.get(DOMAIN)[CONF_URLS] scan_interval = config.get(DOMAIN).get(CONF_SCAN_INTERVAL) max_entries = config.get(DOMAIN).get(CONF_MAX_ENTRIES) - data_file = hass.config.path("{}.pickle".format(DOMAIN)) + data_file = hass.config.path(f"{DOMAIN}.pickle") storage = StoredData(data_file) feeds = [ FeedManager(url, scan_interval, max_entries, hass, storage) for url in urls diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index d47c2b0c2d2..f500b386643 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -247,11 +247,11 @@ class FibaroController: else: room_name = self._room_map[device.roomID].name device.room_name = room_name - device.friendly_name = "{} {}".format(room_name, device.name) + device.friendly_name = f"{room_name} {device.name}" device.ha_id = "scene_{}_{}_{}".format( slugify(room_name), slugify(device.name), device.id ) - device.unique_id_str = "{}.scene.{}".format(self.hub_serial, device.id) + device.unique_id_str = f"{self.hub_serial}.scene.{device.id}" self._scene_map[device.id] = device self.fibaro_devices["scene"].append(device) @@ -287,7 +287,7 @@ class FibaroController: device.mapped_type = None dtype = device.mapped_type if dtype: - device.unique_id_str = "{}.{}".format(self.hub_serial, device.id) + device.unique_id_str = f"{self.hub_serial}.{device.id}" self._device_map[device.id] = device if dtype != "climate": self.fibaro_devices[dtype].append(device) @@ -414,7 +414,7 @@ class FibaroDevice(Entity): green = int(max(0, min(255, green))) blue = int(max(0, min(255, blue))) white = int(max(0, min(255, white))) - color_str = "{},{},{},{}".format(red, green, blue, white) + color_str = f"{red},{green},{blue},{white}" self.fibaro_device.properties.color = color_str self.action("setColor", str(red), str(green), str(blue), str(white)) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index ed399fac209..71be289e27b 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -115,7 +115,7 @@ class FibaroThermostat(FibaroDevice, ClimateDevice): self._op_mode_device = None self._fan_mode_device = None self._support_flags = 0 - self.entity_id = "climate.{}".format(self.ha_id) + self.entity_id = f"climate.{self.ha_id}" self._hvac_support = [] self._preset_support = [] self._fan_support = [] diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index e556903638c..8814a2406c5 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -25,10 +25,10 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -KILOBITS = "Kb" # type: str -PRICE = "CAD" # type: str -MESSAGES = "messages" # type: str -MINUTES = "minutes" # type: str +KILOBITS = "Kb" +PRICE = "CAD" +MESSAGES = "messages" +MINUTES = "minutes" DEFAULT_NAME = "Fido" @@ -108,7 +108,7 @@ class FidoSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {} {}".format(self.client_name, self._number, self._name) + return f"{self.client_name} {self._number} {self._name}" @property def state(self): diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index f4d31a5fd6f..b190bf5d121 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -57,5 +57,5 @@ class FileNotificationService(BaseNotificationService): if self.add_timestamp: text = "{} {}\n".format(dt_util.utcnow().isoformat(), message) else: - text = "{}\n".format(message) + text = f"{message}\n" file.write(text) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 2a8798d3729..81c4623c53f 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -89,7 +89,7 @@ FILTER_LOWPASS_SCHEMA = FILTER_SCHEMA.extend( } ) -FILTER_RANGE_SCHEMA = vol.Schema( +FILTER_RANGE_SCHEMA = FILTER_SCHEMA.extend( { vol.Required(CONF_FILTER_NAME): FILTER_NAME_RANGE, vol.Optional(CONF_FILTER_LOWER_BOUND): vol.Coerce(float), @@ -332,7 +332,7 @@ class FilterState: def __repr__(self): """Return timestamp and state as the representation of FilterState.""" - return "{} : {}".format(self.timestamp, self.state) + return f"{self.timestamp} : {self.state}" class Filter: @@ -406,6 +406,7 @@ class RangeFilter(Filter): def __init__( self, entity, + precision: Optional[int] = DEFAULT_PRECISION, lower_bound: Optional[float] = None, upper_bound: Optional[float] = None, ): @@ -414,7 +415,7 @@ class RangeFilter(Filter): :param upper_bound: band upper bound :param lower_bound: band lower bound """ - super().__init__(FILTER_NAME_RANGE, entity=entity) + super().__init__(FILTER_NAME_RANGE, precision=precision, entity=entity) self._lower_bound = lower_bound self._upper_bound = upper_bound self._stats_internal = Counter() diff --git a/homeassistant/components/fints/sensor.py b/homeassistant/components/fints/sensor.py index 7a1760ea3d5..376ea2c0f9d 100644 --- a/homeassistant/components/fints/sensor.py +++ b/homeassistant/components/fints/sensor.py @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): account_name = account_config.get(account.iban) if not account_name: - account_name = "{} - {}".format(fints_name, account.iban) + account_name = f"{fints_name} - {account.iban}" accounts.append(FinTsAccount(client, account, account_name)) _LOGGER.debug("Creating account %s for bank %s", account.iban, fints_name) @@ -90,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): account_name = holdings_config.get(account.accountnumber) if not account_name: - account_name = "{} - {}".format(fints_name, account.accountnumber) + account_name = f"{fints_name} - {account.accountnumber}" accounts.append(FinTsHoldingsAccount(client, account, account_name)) _LOGGER.debug( "Creating holdings %s for bank %s", account.accountnumber, fints_name @@ -162,11 +162,11 @@ class FinTsAccount(Entity): def __init__(self, client: FinTsClient, account, name: str) -> None: """Initialize a FinTs balance account.""" - self._client = client # type: FinTsClient + self._client = client self._account = account - self._name = name # type: str - self._balance = None # type: float - self._currency = None # type: str + self._name = name + self._balance: float = None + self._currency: str = None @property def should_poll(self) -> bool: @@ -222,11 +222,11 @@ class FinTsHoldingsAccount(Entity): def __init__(self, client: FinTsClient, account, name: str) -> None: """Initialize a FinTs holdings account.""" - self._client = client # type: FinTsClient - self._name = name # type: str + self._client = client + self._name = name self._account = account self._holdings = [] - self._total = None # type: float + self._total: float = None @property def should_poll(self) -> bool: @@ -265,11 +265,11 @@ class FinTsHoldingsAccount(Entity): if self._client.name: attributes[ATTR_BANK] = self._client.name for holding in self._holdings: - total_name = "{} total".format(holding.name) + total_name = f"{holding.name} total" attributes[total_name] = holding.total_value - pieces_name = "{} pieces".format(holding.name) + pieces_name = f"{holding.name} pieces" attributes[pieces_name] = holding.pieces - price_name = "{} price".format(holding.name) + price_name = f"{holding.name} price" attributes[price_name] = holding.market_value return attributes diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 830914ce113..534477d88cf 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -167,7 +167,7 @@ def request_app_setup(hass, config, add_entities, config_path, discovery_info=No else: setup_platform(hass, config, add_entities, discovery_info) - start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_CALLBACK_PATH) + start_url = f"{hass.config.api.base_url}{FITBIT_AUTH_CALLBACK_PATH}" description = """Please create a Fitbit developer app at https://dev.fitbit.com/apps/new. @@ -204,9 +204,9 @@ def request_oauth_completion(hass): def fitbit_configuration_callback(callback_data): """Handle configuration updates.""" - start_url = "{}{}".format(hass.config.api.base_url, FITBIT_AUTH_START) + start_url = f"{hass.config.api.base_url}{FITBIT_AUTH_START}" - description = "Please authorize Fitbit by visiting {}".format(start_url) + description = f"Please authorize Fitbit by visiting {start_url}" _CONFIGURING["fitbit"] = configurator.request_config( "Fitbit", @@ -498,7 +498,7 @@ class FitbitSensor(Entity): hours -= 12 elif hours == 0: hours = 12 - self._state = "{}:{:02d} {}".format(hours, minutes, setting) + self._state = f"{hours}:{minutes:02d} {setting}" else: self._state = raw_state else: diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index 07abd097c87..ba52d3b4beb 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -20,7 +20,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_ACCESS_TOKEN): cv.st async def get_service(hass, config, discovery_info=None): """Get the Flock notification service.""" access_token = config.get(CONF_ACCESS_TOKEN) - url = "{}{}".format(_RESOURCE, access_token) + url = f"{_RESOURCE}{access_token}" session = async_get_clientsession(hass) return FlockNotificationService(url, session) diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 97453c41af0..0df61fd24e1 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -143,7 +143,7 @@ class FluNearYouSensor(Entity): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return "{0},{1}_{2}".format(self.fny.latitude, self.fny.longitude, self._kind) + return f"{self.fny.latitude},{self.fny.longitude}_{self._kind}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index 8ec3541d188..8d3cf6de27d 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -117,7 +117,7 @@ class FoobotSensor(Entity): @property def unique_id(self): """Return the unique id of this entity.""" - return "{}_{}".format(self._uuid, self.type) + return f"{self._uuid}_{self.type}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py index 22a44a11133..dcb700d6636 100644 --- a/homeassistant/components/fritzdect/switch.py +++ b/homeassistant/components/fritzdect/switch.py @@ -95,7 +95,7 @@ class FritzDectSwitch(SwitchDevice): attrs[ATTR_CURRENT_CONSUMPTION_UNIT] = "{}".format( ATTR_CURRENT_CONSUMPTION_UNIT_VALUE ) - attrs[ATTR_TOTAL_CONSUMPTION] = "{:.3f}".format(self.data.total_consumption) + attrs[ATTR_TOTAL_CONSUMPTION] = f"{self.data.total_consumption:.3f}" attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = "{}".format( ATTR_TOTAL_CONSUMPTION_UNIT_VALUE ) @@ -104,7 +104,7 @@ class FritzDectSwitch(SwitchDevice): attrs[ATTR_TEMPERATURE] = "{}".format( self.units.temperature(self.data.temperature, TEMP_CELSIUS) ) - attrs[ATTR_TEMPERATURE_UNIT] = "{}".format(self.units.temperature_unit) + attrs[ATTR_TEMPERATURE_UNIT] = f"{self.units.temperature_unit}" return attrs @property diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index d8790b746be..7298ce8c1d0 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -274,9 +274,7 @@ async def async_setup(hass, config): ("frontend_latest", True), ("frontend_es5", True), ): - hass.http.register_static_path( - "/{}".format(path), str(root_path / path), should_cache - ) + hass.http.register_static_path(f"/{path}", str(root_path / path), should_cache) hass.http.register_static_path( "/auth/authorize", str(root_path / "authorize.html"), False @@ -294,9 +292,7 @@ async def async_setup(hass, config): # To smooth transition to new urls, add redirects to new urls of dev tools # Added June 27, 2019. Can be removed in 2021. for panel in ("event", "info", "service", "state", "template", "mqtt"): - hass.http.register_redirect( - "/dev-{}".format(panel), "/developer-tools/{}".format(panel) - ) + hass.http.register_redirect(f"/dev-{panel}", f"/developer-tools/{panel}") async_register_built_in_panel( hass, diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 1366269061e..978127c6342 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190828.1" + "home-assistant-frontend==20190918.1" ], "dependencies": [ "api", @@ -14,7 +14,5 @@ "system_log", "websocket_api" ], - "codeowners": [ - "@home-assistant/frontend" - ] + "codeowners": ["@home-assistant/frontend"] } diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index 2e52b49c5f4..d487c39db6b 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -177,7 +177,7 @@ class GaradgetCover(CoverDevice): "username": self._username, "password": self._password, } - url = "{}/oauth/token".format(self.particle_url) + url = f"{self.particle_url}/oauth/token" ret = requests.post(url, auth=("particle", "particle"), data=args, timeout=10) try: @@ -187,7 +187,7 @@ class GaradgetCover(CoverDevice): def remove_token(self): """Remove authorization token from API.""" - url = "{}/v1/access_tokens/{}".format(self.particle_url, self.access_token) + url = f"{self.particle_url}/v1/access_tokens/{self.access_token}" ret = requests.delete(url, auth=(self._username, self._password), timeout=10) return ret.text @@ -266,6 +266,6 @@ class GaradgetCover(CoverDevice): params = {"access_token": self.access_token} if arg: params["command"] = arg - url = "{}/v1/devices/{}/{}".format(self.particle_url, self.device_id, func) + url = f"{self.particle_url}/v1/devices/{self.device_id}/{func}" ret = requests.post(url, data=params, timeout=10) return ret.json() diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index 1cc8cd3f406..105a03bf757 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -29,9 +29,9 @@ class GeniusBinarySensor(GeniusEntity, BinarySensorDevice): self._device = device if device.type[:21] == "Dual Channel Receiver": - self._name = "Dual Channel Receiver {}".format(device.id) + self._name = f"Dual Channel Receiver {device.id}" else: - self._name = "{} {}".format(device.type, device.id) + self._name = f"{device.type} {device.id}" @property def is_on(self) -> bool: diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 12f7c266840..f2110ffb2f0 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/components/geniushub", "requirements": [ - "geniushub-client==0.6.5" + "geniushub-client==0.6.13" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 5e39be1620a..82db3d4224e 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -34,7 +34,7 @@ class GeniusBattery(GeniusEntity): super().__init__() self._device = device - self._name = "{} {}".format(device.type, device.id) + self._name = f"{device.type} {device.id}" @property def icon(self) -> str: @@ -59,7 +59,7 @@ class GeniusBattery(GeniusEntity): icon = "mdi:battery" if battery_level <= 95: - icon += "-{}".format(int(round(battery_level / 10 - 0.01)) * 10) + icon += f"-{int(round(battery_level / 10 - 0.01)) * 10}" return icon @@ -112,7 +112,7 @@ class GeniusIssue(GeniusEntity): @property def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - return {"{}_list".format(self._level): self._issues} + return {f"{self._level}_list": self._issues} async def async_update(self) -> Awaitable[None]: """Process the sensor's state data.""" diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index a4d13bdef9d..9f336668142 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -157,7 +157,7 @@ class GeoRssServiceSensor(Entity): # And now compute the attributes from the filtered events. matrix = {} for entry in feed_entries: - matrix[entry.title] = "{:.0f}km".format(entry.distance_to_home) + matrix[entry.title] = f"{entry.distance_to_home:.0f}km" self._state_attributes = matrix elif status == georss_client.UPDATE_OK_NO_DATA: _LOGGER.debug("Update successful, but no data received from %s", self._feed) diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 6835103968a..9d8e0b29f5d 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -50,7 +50,7 @@ BEACON_DEV_PREFIX = "beacon" LOCATION_ENTRY = "1" LOCATION_EXIT = "0" -TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" def _address(value: str) -> str: @@ -131,7 +131,7 @@ def _set_location(hass, data, location_name): data, ) - return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + return web.Response(text=f"Setting location for {device}", status=HTTP_OK) async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/geonetnz_quakes/.translations/ca.json b/homeassistant/components/geonetnz_quakes/.translations/ca.json new file mode 100644 index 00000000000..57ce2b4ee81 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Ubicaci\u00f3 ja registrada" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radi" + }, + "title": "Introdueix els detalls del filtre." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/da.json b/homeassistant/components/geonetnz_quakes/.translations/da.json new file mode 100644 index 00000000000..0d0e927bc4b --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/da.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Placering allerede registreret" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Udfyld dine filteroplysninger." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/de.json b/homeassistant/components/geonetnz_quakes/.translations/de.json new file mode 100644 index 00000000000..7c6fd08af96 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Standort bereits registriert" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "F\u00fcllen Sie Ihre Filterdaten aus." + } + }, + "title": "GeoNet NZ Erdbeben" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/es.json b/homeassistant/components/geonetnz_quakes/.translations/es.json new file mode 100644 index 00000000000..41404822dd8 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "identifier_exists": "Ubicaci\u00f3n ya registrada" + }, + "step": { + "user": { + "data": { + "radius": "Radio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/hu.json b/homeassistant/components/geonetnz_quakes/.translations/hu.json new file mode 100644 index 00000000000..42de5a13142 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "radius": "Sug\u00e1r" + }, + "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/it.json b/homeassistant/components/geonetnz_quakes/.translations/it.json new file mode 100644 index 00000000000..2a019aa39d9 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" + }, + "step": { + "user": { + "data": { + "mmi": "Intensit\u00e0 in Scala Mercalli Modificata", + "radius": "Raggio" + }, + "title": "Inserisci i tuoi dettagli del filtro." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ko.json b/homeassistant/components/geonetnz_quakes/.translations/ko.json new file mode 100644 index 00000000000..26caa2ebe54 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\ubc18\uacbd" + }, + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/nl.json b/homeassistant/components/geonetnz_quakes/.translations/nl.json new file mode 100644 index 00000000000..d6af28240eb --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Locatie al geregistreerd" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Straal" + }, + "title": "Vul uw filtergegevens in." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/no.json b/homeassistant/components/geonetnz_quakes/.translations/no.json new file mode 100644 index 00000000000..40b695d6f51 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Beliggenhet allerede er registrert" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Fyll ut filterdetaljene." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json new file mode 100644 index 00000000000..427c753f6c1 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokalizacja ju\u017c zarejestrowana" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Promie\u0144" + }, + "title": "Wype\u0142nij szczeg\u00f3\u0142y dotycz\u0105ce filtra." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json new file mode 100644 index 00000000000..7d6583bc1d5 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441" + }, + "title": "GeoNet" + } + }, + "title": "\u0417\u0435\u043c\u043b\u0435\u0442\u0440\u044f\u0441\u0435\u043d\u0438\u044f \u0432 \u041d\u043e\u0432\u043e\u0439 \u0417\u0435\u043b\u0430\u043d\u0434\u0438\u0438 (GeoNet)" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/sl.json b/homeassistant/components/geonetnz_quakes/.translations/sl.json new file mode 100644 index 00000000000..bdd05d33953 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokacija je \u017ee registrirana" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radij" + }, + "title": "Izpolnite podrobnosti filtra." + } + }, + "title": "GeoNet NZ Potresi" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json new file mode 100644 index 00000000000..59b4abf259a --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\u534a\u5f91" + }, + "title": "\u586b\u5beb\u904e\u6ffe\u5668\u8cc7\u8a0a\u3002" + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index e786b413029..069c9ab7daa 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -1,27 +1,47 @@ """The GeoNet NZ Quakes integration.""" -import voluptuous as vol +import asyncio +import logging +from datetime import timedelta +import voluptuous as vol +from aio_geojson_geonetnz_quakes import GeonetnzQuakesFeedManager + +from homeassistant.core import callback +from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS, CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM, + LENGTH_MILES, ) -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval from .config_flow import configured_instances from .const import ( + PLATFORMS, CONF_MINIMUM_MAGNITUDE, CONF_MMI, + DEFAULT_FILTER_TIME_INTERVAL, DEFAULT_MINIMUM_MAGNITUDE, DEFAULT_MMI, DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, + SIGNAL_DELETE_ENTITY, + SIGNAL_NEW_GEOLOCATION, + SIGNAL_STATUS, + SIGNAL_UPDATE_ENTITY, ) +_LOGGER = logging.getLogger(__name__) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -81,13 +101,20 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up the GeoNet NZ Quakes component as config entry.""" - hass.data[DOMAIN] = {} - hass.data[DOMAIN][FEED] = {} - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "geo_location") - ) + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + if FEED not in hass.data[DOMAIN]: + hass.data[DOMAIN][FEED] = {} + radius = config_entry.data[CONF_RADIUS] + unit_system = config_entry.data[CONF_UNIT_SYSTEM] + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + # Create feed entity manager for all platforms. + manager = GeonetnzQuakesFeedEntityManager(hass, config_entry, radius, unit_system) + hass.data[DOMAIN][FEED][config_entry.entry_id] = manager + _LOGGER.debug("Feed entity manager added for %s", config_entry.entry_id) + await manager.async_init() return True @@ -95,7 +122,114 @@ async def async_unload_entry(hass, config_entry): """Unload an GeoNet NZ Quakes component config entry.""" manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) await manager.async_stop() - - await hass.config_entries.async_forward_entry_unload(config_entry, "geo_location") - + await asyncio.wait( + [ + hass.config_entries.async_forward_entry_unload(config_entry, domain) + for domain in PLATFORMS + ] + ) return True + + +class GeonetnzQuakesFeedEntityManager: + """Feed Entity Manager for GeoNet NZ Quakes feed.""" + + def __init__(self, hass, config_entry, radius_in_km, unit_system): + """Initialize the Feed Entity Manager.""" + self._hass = hass + self._config_entry = config_entry + coordinates = ( + config_entry.data[CONF_LATITUDE], + config_entry.data[CONF_LONGITUDE], + ) + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GeonetnzQuakesFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + mmi=config_entry.data[CONF_MMI], + filter_radius=radius_in_km, + filter_minimum_magnitude=config_entry.data[CONF_MINIMUM_MAGNITUDE], + filter_time=DEFAULT_FILTER_TIME_INTERVAL, + status_callback=self._status_update, + ) + self._config_entry_id = config_entry.entry_id + self._scan_interval = timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]) + self._unit_system = unit_system + self._track_time_remove_callback = None + self._status_info = None + self.listeners = [] + + async def async_init(self): + """Schedule initial and regular updates based on configured time interval.""" + + for domain in PLATFORMS: + self._hass.async_create_task( + self._hass.config_entries.async_forward_entry_setup( + self._config_entry, domain + ) + ) + + async def update(event_time): + """Update.""" + await self.async_update() + + # Trigger updates at regular intervals. + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval + ) + + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + @callback + def async_event_new_entity(self): + """Return manager specific event to signal new entity.""" + return SIGNAL_NEW_GEOLOCATION.format(self._config_entry_id) + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + def status_info(self): + """Return latest status update info received.""" + return self._status_info + + async def _generate_entity(self, external_id): + """Generate new entity.""" + async_dispatcher_send( + self._hass, + self.async_event_new_entity(), + self, + external_id, + self._unit_system, + ) + + async def _update_entity(self, external_id): + """Update entity.""" + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + + async def _remove_entity(self, external_id): + """Remove entity.""" + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + + async def _status_update(self, status_info): + """Propagate status update.""" + _LOGGER.debug("Status update received: %s", status_info) + self._status_info = status_info + async_dispatcher_send(self._hass, SIGNAL_STATUS.format(self._config_entry_id)) diff --git a/homeassistant/components/geonetnz_quakes/const.py b/homeassistant/components/geonetnz_quakes/const.py index d06e85ee2cb..d564d407f7c 100644 --- a/homeassistant/components/geonetnz_quakes/const.py +++ b/homeassistant/components/geonetnz_quakes/const.py @@ -3,12 +3,21 @@ from datetime import timedelta DOMAIN = "geonetnz_quakes" +PLATFORMS = ("sensor", "geo_location") + CONF_MINIMUM_MAGNITUDE = "minimum_magnitude" CONF_MMI = "mmi" FEED = "feed" +DEFAULT_FILTER_TIME_INTERVAL = timedelta(days=7) DEFAULT_MINIMUM_MAGNITUDE = 0.0 DEFAULT_MMI = 3 DEFAULT_RADIUS = 50.0 DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) + +SIGNAL_DELETE_ENTITY = "geonetnz_quakes_delete_{}" +SIGNAL_UPDATE_ENTITY = "geonetnz_quakes_update_{}" +SIGNAL_STATUS = "geonetnz_quakes_status_{}" + +SIGNAL_NEW_GEOLOCATION = "geonetnz_quakes_new_geolocation_{}" diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 9d4be94e3aa..1ee7c287c61 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -1,33 +1,20 @@ """Geolocation support for GeoNet NZ Quakes Feeds.""" -from datetime import timedelta import logging from typing import Optional -from aio_geojson_geonetnz_quakes import GeonetnzQuakesFeedManager - from homeassistant.components.geo_location import GeolocationEvent from homeassistant.const import ( ATTR_ATTRIBUTION, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_RADIUS, - CONF_SCAN_INTERVAL, - CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, ATTR_TIME, ) from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from .const import CONF_MINIMUM_MAGNITUDE, CONF_MMI, DOMAIN, FEED +from .const import DOMAIN, FEED, SIGNAL_DELETE_ENTITY, SIGNAL_UPDATE_ENTITY _LOGGER = logging.getLogger(__name__) @@ -39,111 +26,27 @@ ATTR_MMI = "mmi" ATTR_PUBLICATION_DATE = "publication_date" ATTR_QUALITY = "quality" -DEFAULT_FILTER_TIME_INTERVAL = timedelta(days=7) - -SIGNAL_DELETE_ENTITY = "geonetnz_quakes_delete_{}" -SIGNAL_UPDATE_ENTITY = "geonetnz_quakes_update_{}" - SOURCE = "geonetnz_quakes" async def async_setup_entry(hass, entry, async_add_entities): """Set up the GeoNet NZ Quakes Feed platform.""" - radius = entry.data[CONF_RADIUS] - unit_system = entry.data[CONF_UNIT_SYSTEM] - if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: - radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) - manager = GeonetnzQuakesFeedEntityManager( - hass, - async_add_entities, - entry.data[CONF_SCAN_INTERVAL], - entry.data[CONF_LATITUDE], - entry.data[CONF_LONGITUDE], - entry.data[CONF_MMI], - radius, - unit_system, - entry.data[CONF_MINIMUM_MAGNITUDE], + manager = hass.data[DOMAIN][FEED][entry.entry_id] + + @callback + def async_add_geolocation(feed_manager, external_id, unit_system): + """Add gelocation entity from feed.""" + new_entity = GeonetnzQuakesEvent(feed_manager, external_id, unit_system) + _LOGGER.debug("Adding geolocation %s", new_entity) + async_add_entities([new_entity], True) + + manager.listeners.append( + async_dispatcher_connect( + hass, manager.async_event_new_entity(), async_add_geolocation + ) ) - hass.data[DOMAIN][FEED][entry.entry_id] = manager - await manager.async_init() - - -class GeonetnzQuakesFeedEntityManager: - """Feed Entity Manager for GeoNet NZ Quakes feed.""" - - def __init__( - self, - hass, - async_add_entities, - scan_interval, - latitude, - longitude, - mmi, - radius_in_km, - unit_system, - minimum_magnitude, - ): - """Initialize the Feed Entity Manager.""" - self._hass = hass - coordinates = (latitude, longitude) - websession = aiohttp_client.async_get_clientsession(hass) - self._feed_manager = GeonetnzQuakesFeedManager( - websession, - self._generate_entity, - self._update_entity, - self._remove_entity, - coordinates, - mmi=mmi, - filter_radius=radius_in_km, - filter_minimum_magnitude=minimum_magnitude, - filter_time=DEFAULT_FILTER_TIME_INTERVAL, - ) - self._async_add_entities = async_add_entities - self._scan_interval = timedelta(seconds=scan_interval) - self._unit_system = unit_system - self._track_time_remove_callback = None - - async def async_init(self): - """Schedule regular updates based on configured time interval.""" - - async def update(event_time): - """Update.""" - await self.async_update() - - await self.async_update() - self._track_time_remove_callback = async_track_time_interval( - self._hass, update, self._scan_interval - ) - _LOGGER.debug("Feed entity manager initialized") - - async def async_update(self): - """Refresh data.""" - await self._feed_manager.update() - _LOGGER.debug("Feed entity manager updated") - - async def async_stop(self): - """Stop this feed entity manager from refreshing.""" - if self._track_time_remove_callback: - self._track_time_remove_callback() - _LOGGER.debug("Feed entity manager stopped") - - def get_entry(self, external_id): - """Get feed entry by external id.""" - return self._feed_manager.feed_entries.get(external_id) - - async def _generate_entity(self, external_id): - """Generate new entity.""" - new_entity = GeonetnzQuakesEvent(self, external_id, self._unit_system) - # Add new entities to HA. - self._async_add_entities([new_entity], True) - - async def _update_entity(self, external_id): - """Update entity.""" - async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) - - async def _remove_entity(self, external_id): - """Remove entity.""" - async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + hass.async_create_task(manager.async_update()) + _LOGGER.debug("Geolocation setup done") class GeonetnzQuakesEvent(GeolocationEvent): diff --git a/homeassistant/components/geonetnz_quakes/sensor.py b/homeassistant/components/geonetnz_quakes/sensor.py new file mode 100644 index 00000000000..e0be94d1b26 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/sensor.py @@ -0,0 +1,139 @@ +"""Feed Entity Manager Sensor support for GeoNet NZ Quakes Feeds.""" +import logging +from typing import Optional + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.util import dt + +from .const import DOMAIN, FEED, SIGNAL_STATUS + +_LOGGER = logging.getLogger(__name__) + +ATTR_STATUS = "status" +ATTR_LAST_UPDATE = "last_update" +ATTR_LAST_UPDATE_SUCCESSFUL = "last_update_successful" +ATTR_LAST_TIMESTAMP = "last_timestamp" +ATTR_CREATED = "created" +ATTR_UPDATED = "updated" +ATTR_REMOVED = "removed" + +DEFAULT_ICON = "mdi:pulse" +DEFAULT_UNIT_OF_MEASUREMENT = "quakes" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GeoNet NZ Quakes Feed platform.""" + manager = hass.data[DOMAIN][FEED][entry.entry_id] + sensor = GeonetnzQuakesSensor(entry.entry_id, entry.title, manager) + async_add_entities([sensor]) + _LOGGER.debug("Sensor setup done") + + +class GeonetnzQuakesSensor(Entity): + """This is a status sensor for the GeoNet NZ Quakes integration.""" + + def __init__(self, config_entry_id, config_title, manager): + """Initialize entity.""" + self._config_entry_id = config_entry_id + self._config_title = config_title + self._manager = manager + self._status = None + self._last_update = None + self._last_update_successful = None + self._last_timestamp = None + self._total = None + self._created = None + self._updated = None + self._removed = None + self._remove_signal_status = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_status = async_dispatcher_connect( + self.hass, + SIGNAL_STATUS.format(self._config_entry_id), + self._update_status_callback, + ) + _LOGGER.debug("Waiting for updates %s", self._config_entry_id) + # First update is manual because of how the feed entity manager is updated. + await self.async_update() + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + if self._remove_signal_status: + self._remove_signal_status() + + @callback + def _update_status_callback(self): + """Call status update method.""" + _LOGGER.debug("Received status update for %s", self._config_entry_id) + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GeoNet NZ Quakes status sensor.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._config_entry_id) + if self._manager: + status_info = self._manager.status_info() + if status_info: + self._update_from_status_info(status_info) + + def _update_from_status_info(self, status_info): + """Update the internal state from the provided information.""" + self._status = status_info.status + self._last_update = ( + dt.as_utc(status_info.last_update) if status_info.last_update else None + ) + self._last_update_successful = ( + dt.as_utc(status_info.last_update_successful) + if status_info.last_update_successful + else None + ) + self._last_timestamp = status_info.last_timestamp + self._total = status_info.total + self._created = status_info.created + self._updated = status_info.updated + self._removed = status_info.removed + + @property + def state(self): + """Return the state of the sensor.""" + return self._total + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return f"GeoNet NZ Quakes ({self._config_title})" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return DEFAULT_ICON + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return DEFAULT_UNIT_OF_MEASUREMENT + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_STATUS, self._status), + (ATTR_LAST_UPDATE, self._last_update), + (ATTR_LAST_UPDATE_SUCCESSFUL, self._last_update_successful), + (ATTR_LAST_TIMESTAMP, self._last_timestamp), + (ATTR_CREATED, self._created), + (ATTR_UPDATED, self._updated), + (ATTR_REMOVED, self._removed), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 3d630378e42..1385d5e59a7 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -207,6 +207,8 @@ class GlancesSensor(Entity): "soc_thermal 1", "soc-thermal 1", "aml_thermal", + "Core 0", + "Core 1", ]: self._state = sensor["value"] elif self.type == "docker_active": diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 41901a71704..62aa2212bb1 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -59,10 +59,10 @@ SERVICE_ADD_EVENT = "add_event" DATA_INDEX = "google_calendars" -YAML_DEVICES = "{}_calendars.yaml".format(DOMAIN) +YAML_DEVICES = f"{DOMAIN}_calendars.yaml" SCOPES = "https://www.googleapis.com/auth/calendar" -TOKEN_FILE = ".{}.token".format(DOMAIN) +TOKEN_FILE = f".{DOMAIN}.token" CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/google_assistant/error.py b/homeassistant/components/google_assistant/error.py index a2ff72511c7..82c256067eb 100644 --- a/homeassistant/components/google_assistant/error.py +++ b/homeassistant/components/google_assistant/error.py @@ -26,9 +26,7 @@ class ChallengeNeeded(SmartHomeError): def __init__(self, challenge_type): """Initialize challenge needed error.""" - super().__init__( - ERR_CHALLENGE_NEEDED, "Challenge needed: {}".format(challenge_type) - ) + super().__init__(ERR_CHALLENGE_NEEDED, f"Challenge needed: {challenge_type}") self.challenge_type = challenge_type def to_response(self): diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 066ed0057ac..daaf790a0c1 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -212,7 +212,7 @@ class GoogleEntity: if not executed: raise SmartHomeError( ERR_FUNCTION_NOT_SUPPORTED, - "Unable to execute {} for {}".format(command, self.state.entity_id), + f"Unable to execute {command} for {self.state.entity_id}", ) @callback diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 24502462512..d68650fb638 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -93,7 +93,7 @@ class GoogleAssistantView(HomeAssistantView): async def post(self, request: Request) -> Response: """Handle Google Assistant requests.""" - message = await request.json() # type: dict + message: dict = await request.json() result = await async_handle_message( request.app["hass"], self.config, request["hass_user"].id, message ) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 2cb440f9181..6ab6d937b51 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -24,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) async def async_handle_message(hass, config, user_id, message): """Handle incoming API messages.""" - request_id = message.get("requestId") # type: str + request_id: str = message.get("requestId") data = RequestData(config, user_id, request_id) @@ -38,7 +38,7 @@ async def async_handle_message(hass, config, user_id, message): async def _process(hass, data, message): """Process a message.""" - inputs = message.get("inputs") # type: list + inputs: list = message.get("inputs") if len(inputs) != 1: return { diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 2b5550860ee..75f370e502e 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -67,10 +67,7 @@ class GoogleMapsScanner: except InvalidCookies: _LOGGER.error( - "You have specified invalid login credentials. " - "Please make sure you have saved your credentials" - " in the following file: %s", - credfile, + "The cookie file provided does not provide a valid session. Please create another one and try again." ) self.success_init = False diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index 5f31f533a38..ec48e5252a8 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -3,7 +3,7 @@ "name": "Google maps", "documentation": "https://www.home-assistant.io/components/google_maps", "requirements": [ - "locationsharinglib==4.0.2" + "locationsharinglib==4.1.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index a910e91b164..1d4ed8d84f8 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -88,7 +88,7 @@ class GoogleWifiSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{}_{}".format(self._name, self._var_name) + return f"{self._name}_{self._var_name}" @property def icon(self): @@ -125,7 +125,7 @@ class GoogleWifiAPI: def __init__(self, host, conditions): """Initialize the data object.""" uri = "http://" - resource = "{}{}{}".format(uri, host, ENDPOINT) + resource = f"{uri}{host}{ENDPOINT}" self._request = requests.Request("GET", resource).prepare() self.raw_data = None self.conditions = conditions diff --git a/homeassistant/components/gpmdp/media_player.py b/homeassistant/components/gpmdp/media_player.py index 90522a84ce5..e6df8b0fe8b 100644 --- a/homeassistant/components/gpmdp/media_player.py +++ b/homeassistant/components/gpmdp/media_player.py @@ -142,7 +142,7 @@ def setup_gpmdp(hass, config, code, add_entities): name = config.get(CONF_NAME) host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "ws://{}:{}".format(host, port) + url = f"ws://{host}:{port}" if not code: request_configuration(hass, config, url, add_entities) diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 839adec2f5b..3ac09457d81 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -29,7 +29,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" DEFAULT_ACCURACY = 200 @@ -90,7 +90,7 @@ async def handle_webhook(hass, webhook_id, request): attrs, ) - return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + return web.Response(text=f"Setting location for {device}", status=HTTP_OK) async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index f0d29d923c8..87d8134ccbf 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -75,19 +75,19 @@ class LightGroup(light.Light): def __init__(self, name: str, entity_ids: List[str]) -> None: """Initialize a light group.""" - self._name = name # type: str - self._entity_ids = entity_ids # type: List[str] - self._is_on = False # type: bool - self._available = False # type: bool - self._brightness = None # type: Optional[int] - self._hs_color = None # type: Optional[Tuple[float, float]] - self._color_temp = None # type: Optional[int] - self._min_mireds = 154 # type: Optional[int] - self._max_mireds = 500 # type: Optional[int] - self._white_value = None # type: Optional[int] - self._effect_list = None # type: Optional[List[str]] - self._effect = None # type: Optional[str] - self._supported_features = 0 # type: int + self._name = name + self._entity_ids = entity_ids + self._is_on = False + self._available = False + self._brightness: Optional[int] = None + self._hs_color: Optional[Tuple[float, float]] = None + self._color_temp: Optional[int] = None + self._min_mireds: Optional[int] = 154 + self._max_mireds: Optional[int] = 500 + self._white_value: Optional[int] = None + self._effect_list: Optional[List[str]] = None + self._effect: Optional[str] = None + self._supported_features: int = 0 self._async_unsub_state_changed = None async def async_added_to_hass(self) -> None: diff --git a/homeassistant/components/growatt_server/__init__.py b/homeassistant/components/growatt_server/__init__.py new file mode 100644 index 00000000000..14205e8d9ba --- /dev/null +++ b/homeassistant/components/growatt_server/__init__.py @@ -0,0 +1 @@ +"""The Growatt server PV inverter sensor integration.""" diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json new file mode 100644 index 00000000000..a6a1d2b8aeb --- /dev/null +++ b/homeassistant/components/growatt_server/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "growatt_server", + "name": "Growatt Server", + "documentation": "https://www.home-assistant.io/components/growatt_server/", + "requirements": [ + "growattServer==0.0.1" + ], + "dependencies": [], + "codeowners": [ + "@indykoning" + ] +} diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py new file mode 100644 index 00000000000..3b7109222a4 --- /dev/null +++ b/homeassistant/components/growatt_server/sensor.py @@ -0,0 +1,189 @@ +"""Read status of growatt inverters.""" +import re +import json +import logging +import datetime + +import growattServer +import voluptuous as vol + +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD + +_LOGGER = logging.getLogger(__name__) + +CONF_PLANT_ID = "plant_id" +DEFAULT_PLANT_ID = "0" +DEFAULT_NAME = "Growatt" +SCAN_INTERVAL = datetime.timedelta(minutes=5) + +TOTAL_SENSOR_TYPES = { + "total_money_today": ("Total money today", "€", "plantMoneyText", None), + "total_money_total": ("Money lifetime", "€", "totalMoneyText", None), + "total_energy_today": ("Energy Today", "kWh", "todayEnergy", "power"), + "total_output_power": ("Output Power", "W", "invTodayPpv", "power"), + "total_energy_output": ("Lifetime energy output", "kWh", "totalEnergy", "power"), + "total_maximum_output": ("Maximum power", "W", "nominalPower", "power"), +} + +INVERTER_SENSOR_TYPES = { + "inverter_energy_today": ("Energy today", "kWh", "e_today", "power"), + "inverter_energy_total": ("Lifetime energy output", "kWh", "e_total", "power"), + "inverter_voltage_input_1": ("Input 1 voltage", "V", "vpv1", None), + "inverter_amperage_input_1": ("Input 1 Amperage", "A", "ipv1", None), + "inverter_wattage_input_1": ("Input 1 Wattage", "W", "ppv1", "power"), + "inverter_voltage_input_2": ("Input 2 voltage", "V", "vpv2", None), + "inverter_amperage_input_2": ("Input 2 Amperage", "A", "ipv2", None), + "inverter_wattage_input_2": ("Input 2 Wattage", "W", "ppv2", "power"), + "inverter_voltage_input_3": ("Input 3 voltage", "V", "vpv3", None), + "inverter_amperage_input_3": ("Input 3 Amperage", "A", "ipv3", None), + "inverter_wattage_input_3": ("Input 3 Wattage", "W", "ppv3", "power"), + "inverter_internal_wattage": ("Internal wattage", "W", "ppv", "power"), + "inverter_reactive_voltage": ("Reactive voltage", "V", "vacr", None), + "inverter_inverter_reactive_amperage": ("Reactive amperage", "A", "iacr", None), + "inverter_frequency": ("AC frequency", "Hz", "fac", None), + "inverter_current_wattage": ("Output power", "W", "pac", "power"), + "inverter_current_reactive_wattage": ("Reactive wattage", "W", "pacr", "power"), +} + +SENSOR_TYPES = {**TOTAL_SENSOR_TYPES, **INVERTER_SENSOR_TYPES} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PLANT_ID, default=DEFAULT_PLANT_ID): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Growatt sensor.""" + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + plant_id = config[CONF_PLANT_ID] + name = config[CONF_NAME] + + api = growattServer.GrowattApi() + + # Log in to api and fetch first plant if no plant id is defined. + login_response = api.login(username, password) + if not login_response["success"] and login_response["errCode"] == "102": + _LOGGER.error("Username or Password may be incorrect!") + return + user_id = login_response["userId"] + if plant_id == DEFAULT_PLANT_ID: + plant_info = api.plant_list(user_id) + plant_id = plant_info["data"][0]["plantId"] + + # Get a list of inverters for specified plant to add sensors for. + inverters = api.inverter_list(plant_id) + entities = [] + probe = GrowattData(api, username, password, plant_id, True) + for sensor in TOTAL_SENSOR_TYPES: + entities.append( + GrowattInverter(probe, f"{name} Total", sensor, f"{plant_id}-{sensor}") + ) + + # Add sensors for each inverter in the specified plant. + for inverter in inverters: + probe = GrowattData(api, username, password, inverter["deviceSn"], False) + for sensor in INVERTER_SENSOR_TYPES: + entities.append( + GrowattInverter( + probe, + f"{inverter['deviceAilas']}", + sensor, + f"{inverter['deviceSn']}-{sensor}", + ) + ) + + add_entities(entities, True) + + +class GrowattInverter(Entity): + """Representation of a Growatt Sensor.""" + + def __init__(self, probe, name, sensor, unique_id): + """Initialize a PVOutput sensor.""" + self.sensor = sensor + self.probe = probe + self._name = name + self._state = None + self._unique_id = unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {SENSOR_TYPES[self.sensor][0]}" + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return self._unique_id + + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:solar-power" + + @property + def state(self): + """Return the state of the sensor.""" + return self.probe.get_data(SENSOR_TYPES[self.sensor][2]) + + @property + def device_class(self): + """Return the device class of the sensor.""" + return SENSOR_TYPES[self.sensor][3] + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return SENSOR_TYPES[self.sensor][1] + + def update(self): + """Get the latest data from the Growat API and updates the state.""" + self.probe.update() + + +class GrowattData: + """The class for handling data retrieval.""" + + def __init__(self, api, username, password, inverter_id, is_total=False): + """Initialize the probe.""" + + self.is_total = is_total + self.api = api + self.inverter_id = inverter_id + self.data = {} + self.username = username + self.password = password + + @Throttle(SCAN_INTERVAL) + def update(self): + """Update probe data.""" + self.api.login(self.username, self.password) + _LOGGER.debug("Updating data for %s", self.inverter_id) + try: + if self.is_total: + total_info = self.api.plant_info(self.inverter_id) + del total_info["deviceList"] + # PlantMoneyText comes in as "3.1/€" remove anything that isn't part of the number + total_info["plantMoneyText"] = re.sub( + r"[^\d.,]", "", total_info["plantMoneyText"] + ) + self.data = total_info + else: + inverter_info = self.api.inverter_detail(self.inverter_id) + self.data = inverter_info["data"] + except json.decoder.JSONDecodeError: + _LOGGER.error("Unable to fetch data from Growatt server") + + def get_data(self, variable): + """Get the data.""" + return self.data.get(variable) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index b5c1000681d..086545f0c76 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -122,7 +122,7 @@ def get_next_departure( include_tomorrow: bool = False, ) -> dict: """Get the next departure for the given schedule.""" - now = datetime.datetime.now() + offset + now = dt_util.now().replace(tzinfo=None) + offset now_date = now.strftime(dt_util.DATE_STR_FORMAT) yesterday = now - datetime.timedelta(days=1) yesterday_date = yesterday.strftime(dt_util.DATE_STR_FORMAT) @@ -139,9 +139,9 @@ def get_next_departure( if include_tomorrow: limit = int(limit / 2 * 3) tomorrow_name = tomorrow.strftime("%A").lower() - tomorrow_select = "calendar.{} AS tomorrow,".format(tomorrow_name) - tomorrow_where = "OR calendar.{} = 1".format(tomorrow_name) - tomorrow_order = "calendar.{} DESC,".format(tomorrow_name) + tomorrow_select = f"calendar.{tomorrow_name} AS tomorrow," + tomorrow_where = f"OR calendar.{tomorrow_name} = 1" + tomorrow_order = f"calendar.{tomorrow_name} DESC," sql_query = """ SELECT trip.trip_id, trip.route_id, @@ -256,7 +256,7 @@ def get_next_departure( _LOGGER.debug("Timetable: %s", sorted(timetable.keys())) - item = {} # type: dict + item = {} for key in sorted(timetable.keys()): if dt_util.parse_datetime(key) > now: item = timetable[key] @@ -357,7 +357,7 @@ def setup_platform( (gtfs_root, _) = os.path.splitext(data) - sqlite_file = "{}.sqlite?check_same_thread=False".format(gtfs_root) + sqlite_file = f"{gtfs_root}.sqlite?check_same_thread=False" joined_path = os.path.join(gtfs_dir, sqlite_file) gtfs = pygtfs.Schedule(joined_path) @@ -393,11 +393,11 @@ class GTFSDepartureSensor(Entity): self._available = False self._icon = ICON self._name = "" - self._state = None # type: Optional[str] - self._attributes = {} # type: dict + self._state: Optional[str] = None + self._attributes = {} self._agency = None - self._departure = {} # type: dict + self._departure = {} self._destination = None self._origin = None self._route = None @@ -673,7 +673,7 @@ class GTFSDepartureSensor(Entity): continue key = attr if prefix and not key.startswith(prefix): - key = "{} {}".format(prefix, key) + key = f"{prefix} {key}" key = slugify(key) self._attributes[key] = val diff --git a/homeassistant/components/gtt/sensor.py b/homeassistant/components/gtt/sensor.py index 43f13c94620..cd66a670696 100644 --- a/homeassistant/components/gtt/sensor.py +++ b/homeassistant/components/gtt/sensor.py @@ -38,7 +38,7 @@ class GttSensor(Entity): """Initialize the Gtt sensor.""" self.data = GttData(stop, bus_name) self._state = None - self._name = "Stop {}".format(stop) + self._name = f"Stop {stop}" @property def name(self): diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index e70d0eb696a..1fa4ad63b36 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -66,7 +66,7 @@ class HabitipySensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{0}_{1}_{2}".format(habitica.DOMAIN, self._name, self._sensor_name) + return f"{habitica.DOMAIN}_{self._name}_{self._sensor_name}" @property def state(self): diff --git a/homeassistant/components/hangouts/.translations/it.json b/homeassistant/components/hangouts/.translations/it.json index ad8dafd17ec..ff0a8238d49 100644 --- a/homeassistant/components/hangouts/.translations/it.json +++ b/homeassistant/components/hangouts/.translations/it.json @@ -14,14 +14,16 @@ "data": { "2fa": "2FA Pin" }, + "description": "Vuoto", "title": "Autenticazione a due fattori" }, "user": { "data": { "authorization_code": "Codice di autorizzazione (necessario per l'autenticazione manuale)", - "email": "Indirizzo email", + "email": "Indirizzo E-mail", "password": "Password" }, + "description": "Vuoto", "title": "Accesso a Google Hangouts" } }, diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json index e045f3359d1..3b1c755b358 100644 --- a/homeassistant/components/hangouts/.translations/ko.json +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Google Hangouts \uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "\uad6c\uae00 \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -24,9 +24,9 @@ "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", - "title": "Google Hangouts \ub85c\uadf8\uc778" + "title": "\uad6c\uae00 \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" } }, - "title": "Google Hangouts" + "title": "\uad6c\uae00 \ud589\uc544\uc6c3" } } \ No newline at end of file diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index d444e852cca..9fc3e2fa58e 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -293,7 +293,7 @@ class HangoutsBot: if self.hass.config.is_allowed_path(uri): try: image_file = open(uri, "rb") - except IOError as error: + except OSError as error: _LOGGER.error( "Image file I/O error(%s): %s", error.errno, error.strerror ) @@ -323,7 +323,7 @@ class HangoutsBot: } self.hass.states.async_set( - "{}.conversations".format(DOMAIN), + f"{DOMAIN}.conversations", len(self._conversation_list.get_all()), attributes=conversations, ) diff --git a/homeassistant/components/hangouts/intents.py b/homeassistant/components/hangouts/intents.py index a26da7a4872..5e4c6ff206b 100644 --- a/homeassistant/components/hangouts/intents.py +++ b/homeassistant/components/hangouts/intents.py @@ -25,7 +25,7 @@ class HelpIntent(intent.IntentHandler): help_text = "I understand the following sentences:" for intent_data in intents.values(): for sentence in intent_data["sentences"]: - help_text += "\n'{}'".format(sentence) + help_text += f"\n'{sentence}'" response.async_set_speech(help_text) return response diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 801c20b5c2b..6603728e037 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -271,7 +271,7 @@ async def async_setup(hass, config): hass.components.persistent_notification.async_create( "Config error. See dev-info panel for details.", "Config validating", - "{0}.check_config".format(HASS_DOMAIN), + f"{HASS_DOMAIN}.check_config", ) return diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 10f21556fb3..5213443614c 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -80,7 +80,7 @@ class HassIO: This method return a coroutine. """ - return self.send_command("/addons/{}/info".format(addon), method="get") + return self.send_command(f"/addons/{addon}/info", method="get") @_api_data def get_ingress_panels(self): @@ -120,7 +120,7 @@ class HassIO: This method return a coroutine. """ - return self.send_command("/discovery/{}".format(uuid), method="get") + return self.send_command(f"/discovery/{uuid}", method="get") @_api_bool async def update_hass_api(self, http_config, refresh_token): @@ -156,7 +156,7 @@ class HassIO: with async_timeout.timeout(timeout): request = await self.websession.request( method, - "http://{}{}".format(self._ip, command), + f"http://{self._ip}{command}", json=payload, headers={X_HASSIO: os.environ.get("HASSIO_TOKEN", "")}, ) diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index f42aaca4438..3b1b8374510 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -75,7 +75,7 @@ class HassIOView(HomeAssistantView): method = getattr(self._websession, request.method.lower()) client = await method( - "http://{}/{}".format(self._host, path), + f"http://{self._host}/{path}", data=data, headers=headers, timeout=read_timeout, diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 84e2b096362..4ecb9a8419f 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -42,7 +42,7 @@ class HassIOIngress(HomeAssistantView): def _create_url(self, token: str, path: str) -> str: """Create URL to service.""" - return "http://{}/ingress/{}/{}".format(self._host, token, path) + return f"http://{self._host}/ingress/{token}/{path}" async def _handle( self, request: web.Request, token: str, path: str @@ -91,7 +91,7 @@ class HassIOIngress(HomeAssistantView): # Support GET query if request.query_string: - url = "{}?{}".format(url, request.query_string) + url = f"{url}?{request.query_string}" # Start proxy async with self._websession.ws_connect( @@ -175,15 +175,15 @@ def _init_header( headers[X_HASSIO] = os.environ.get("HASSIO_TOKEN", "") # Ingress information - headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(token) + headers[X_INGRESS_PATH] = f"/api/hassio_ingress/{token}" # Set X-Forwarded-For forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) connected_ip = ip_address(request.transport.get_extra_info("peername")[0]) if forward_for: - forward_for = "{}, {!s}".format(forward_for, connected_ip) + forward_for = f"{forward_for}, {connected_ip!s}" else: - forward_for = "{!s}".format(connected_ip) + forward_for = f"{connected_ip!s}" headers[hdrs.X_FORWARDED_FOR] = forward_for # Set X-Forwarded-Host diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index ec43d9444a2..7fa3f422300 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -61,7 +61,7 @@ class HaveIBeenPwnedSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "Breaches {}".format(self._email) + return f"Breaches {self._email}" @property def unit_of_measurement(self): @@ -151,7 +151,7 @@ class HaveIBeenPwnedData: def update(self, **kwargs): """Get the latest data for current email from REST service.""" try: - url = "{}{}?truncateResponse=false".format(URL, self._email) + url = f"{URL}{self._email}?truncateResponse=false" header = {USER_AGENT: HA_USER_AGENT, "hibp-api-key": self._api_key} _LOGGER.debug("Checking for breaches for email: %s", self._email) req = requests.get(url, headers=header, allow_redirects=True, timeout=5) diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index fa3b5fd256c..d0dd5018dca 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -67,7 +67,7 @@ class HddTempSensor(Entity): """Initialize a HDDTemp sensor.""" self.hddtemp = hddtemp self.disk = disk - self._name = "{} {}".format(name, disk) + self._name = f"{name} {disk}" self._state = None self._details = None self._unit = None diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 969925182fd..d1637f96d95 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -264,7 +264,7 @@ def setup(hass: HomeAssistant, base_config): if isinstance(data[ATTR_ATT], (list,)): att = data[ATTR_ATT] else: - att = reduce(lambda x, y: "%s:%x" % (x, y), data[ATTR_ATT]) + att = reduce(lambda x, y: f"{x}:{y:x}", data[ATTR_ATT]) else: att = "" command = CecCommand(cmd, dst, src, att) @@ -312,7 +312,7 @@ def setup(hass: HomeAssistant, base_config): def _new_device(device): """Handle new devices which are detected by HDMI network.""" - key = "{}.{}".format(DOMAIN, device.name) + key = f"{DOMAIN}.{device.name}" hass.data[key] = device ent_platform = base_config[DOMAIN][CONF_TYPES].get(key, platform) discovery.load_platform( @@ -399,7 +399,7 @@ class CecDevice(Entity): def name(self): """Return the name of the device.""" return ( - "%s %s" % (self.vendor_name, self._device.osd_name) + f"{self.vendor_name} {self._device.osd_name}" if ( self._device.osd_name is not None and self.vendor_name is not None diff --git a/homeassistant/components/heos/.translations/ca.json b/homeassistant/components/heos/.translations/ca.json index 05d95116b10..60bd780547c 100644 --- a/homeassistant/components/heos/.translations/ca.json +++ b/homeassistant/components/heos/.translations/ca.json @@ -4,7 +4,7 @@ "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 de Heos tot i que aquesta ja pot controlar tots els dispositius de la xarxa." }, "error": { - "connection_failure": "No es pot connectar amb l'amfitri\u00f3 especificat." + "connection_failure": "No s'ha pogut connectar amb l'amfitri\u00f3 especificat." }, "step": { "user": { diff --git a/homeassistant/components/heos/.translations/it.json b/homeassistant/components/heos/.translations/it.json index 20a4060add4..824f7c3fb50 100644 --- a/homeassistant/components/heos/.translations/it.json +++ b/homeassistant/components/heos/.translations/it.json @@ -16,6 +16,6 @@ "title": "Connetti a Heos" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/pl.json b/homeassistant/components/heos/.translations/pl.json index 9b5f9844ddc..d427acc3a98 100644 --- a/homeassistant/components/heos/.translations/pl.json +++ b/homeassistant/components/heos/.translations/pl.json @@ -12,7 +12,7 @@ "access_token": "Host", "host": "Host" }, - "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP urz\u0105dzenia Heos (preferowane po\u0142\u0105czenie kablowe, nie WiFi).", + "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP urz\u0105dzenia Heos (najlepiej pod\u0142\u0105czonego przewodowo do sieci).", "title": "Po\u0142\u0105cz si\u0119 z Heos" } }, diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 20ed7930a4f..f7e1ce5bc58 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -87,9 +87,8 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): favorites = await controller.get_favorites() else: _LOGGER.warning( - "%s is not logged in to a HEOS account and will be unable " - "to retrieve HEOS favorites: Use the 'heos.sign_in' service " - "to sign-in to a HEOS account", + "%s is not logged in to a HEOS account and will be unable to retrieve " + "HEOS favorites: Use the 'heos.sign_in' service to sign-in to a HEOS account", host, ) inputs = await controller.get_input_sources() @@ -312,7 +311,7 @@ class SourceManager: if retry_attempts < self.max_retry_attempts: retry_attempts += 1 _LOGGER.debug( - "Error retrieving sources and will " "retry: %s", error + "Error retrieving sources and will retry: %s", error ) await asyncio.sleep(self.retry_delay) else: diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 1d56478ba3a..4380cb4d8ba 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -10,7 +10,7 @@ from .const import DATA_DISCOVERED_HOSTS, DOMAIN def format_title(host: str) -> str: """Format the title for config entries.""" - return "Controller ({})".format(host) + return f"Controller ({host})" @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 40f6113a80d..10ea28ca16c 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -183,7 +183,7 @@ class HeosMediaPlayer(MediaPlayerDevice): None, ) if index is None: - raise ValueError("Invalid quick select '{}'".format(media_id)) + raise ValueError(f"Invalid quick select '{media_id}'") await self._player.play_quick_select(index) return @@ -191,7 +191,7 @@ class HeosMediaPlayer(MediaPlayerDevice): playlists = await self._player.heos.get_playlists() playlist = next((p for p in playlists if p.name == media_id), None) if not playlist: - raise ValueError("Invalid playlist '{}'".format(media_id)) + raise ValueError(f"Invalid playlist '{media_id}'") add_queue_option = ( heos_const.ADD_QUEUE_ADD_TO_END if kwargs.get(ATTR_MEDIA_ENQUEUE) @@ -215,11 +215,11 @@ class HeosMediaPlayer(MediaPlayerDevice): None, ) if index is None: - raise ValueError("Invalid favorite '{}'".format(media_id)) + raise ValueError(f"Invalid favorite '{media_id}'") await self._player.play_favorite(index) return - raise ValueError("Unsupported media type '{}'".format(media_type)) + raise ValueError(f"Unsupported media type '{media_type}'") @log_command_error("select source") async def async_select_source(self, source): diff --git a/homeassistant/components/hikvision/binary_sensor.py b/homeassistant/components/hikvision/binary_sensor.py index a9ab242c2fd..b898f5d860c 100644 --- a/homeassistant/components/hikvision/binary_sensor.py +++ b/homeassistant/components/hikvision/binary_sensor.py @@ -93,7 +93,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: protocol = "http" - url = "{}://{}".format(protocol, host) + url = f"{protocol}://{host}" data = HikvisionData(hass, url, port, name, username, password) @@ -196,11 +196,11 @@ class HikvisionBinarySensor(BinarySensorDevice): self._channel = channel if self._cam.type == "NVR": - self._name = "{} {} {}".format(self._cam.name, sensor, channel) + self._name = f"{self._cam.name} {sensor} {channel}" else: - self._name = "{} {}".format(self._cam.name, sensor) + self._name = f"{self._cam.name} {sensor}" - self._id = "{}.{}.{}".format(self._cam.cam_id, sensor, channel) + self._id = f"{self._cam.cam_id}.{sensor}.{channel}" if delay is None: self._delay = 0 diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index a2285da4e80..65607d0f8bf 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -154,20 +154,21 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) from sqlalchemy import and_, func with session_scope(hass=hass) as session: + query = session.query(States) + if entity_ids and len(entity_ids) == 1: # Use an entirely different (and extremely fast) query if we only # have a single entity id - most_recent_state_ids = ( - session.query(States.state_id.label("max_state_id")) - .filter( - (States.last_updated < utc_point_in_time) - & (States.entity_id.in_(entity_ids)) + query = ( + query.filter( + States.last_updated >= run.start, + States.last_updated < utc_point_in_time, + States.entity_id.in_(entity_ids), ) .order_by(States.last_updated.desc()) + .limit(1) ) - most_recent_state_ids = most_recent_state_ids.limit(1) - else: # We have more than one entity to look at (most commonly we want # all entities,) so we need to do a search on all states since the @@ -203,19 +204,15 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) most_recent_state_ids = most_recent_state_ids.group_by(States.entity_id) - most_recent_state_ids = most_recent_state_ids.subquery() + most_recent_state_ids = most_recent_state_ids.subquery() - query = ( - session.query(States) - .join( + query = query.join( most_recent_state_ids, States.state_id == most_recent_state_ids.c.max_state_id, - ) - .filter((~States.domain.in_(IGNORE_DOMAINS))) - ) + ).filter(~States.domain.in_(IGNORE_DOMAINS)) - if filters: - query = filters.apply(query, entity_ids) + if filters: + query = filters.apply(query, entity_ids) return [ state diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index e3e8975c125..2f3526d45b6 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -44,8 +44,8 @@ class HitronCODADeviceScanner(DeviceScanner): """Initialize the scanner.""" self.last_results = [] host = config[CONF_HOST] - self._url = "http://{}/data/getConnectInfo.asp".format(host) - self._loginurl = "http://{}/goform/login".format(host) + self._url = f"http://{host}/data/getConnectInfo.asp" + self._loginurl = f"http://{host}/goform/login" self._username = config.get(CONF_USERNAME) self._password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index 80aaaf86463..50c8277302f 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -26,8 +26,8 @@ class HiveBinarySensorEntity(BinarySensorDevice): self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self.session.entities.append(self) @property @@ -42,7 +42,7 @@ class HiveBinarySensorEntity(BinarySensorDevice): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index d4a1c915518..861957e6ef0 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -54,8 +54,8 @@ class HiveClimateEntity(ClimateDevice): self.thermostat_node_id = hivedevice["Thermostat_NodeID"] self.session = hivesession self.attributes = {} - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" @property def unique_id(self): @@ -74,7 +74,7 @@ class HiveClimateEntity(ClimateDevice): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property @@ -82,7 +82,7 @@ class HiveClimateEntity(ClimateDevice): """Return the name of the Climate device.""" friendly_name = "Heating" if self.node_name is not None: - friendly_name = "{} {}".format(self.node_name, friendly_name) + friendly_name = f"{self.node_name} {friendly_name}" return friendly_name @property diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 5892e304379..a85c3a43992 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -33,8 +33,8 @@ class HiveDeviceLight(Light): self.light_device_type = hivedevice["Hive_Light_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self.session.entities.append(self) @property @@ -49,7 +49,7 @@ class HiveDeviceLight(Light): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index dd3343633d8..c43fe461a8e 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -37,8 +37,8 @@ class HiveSensorEntity(Entity): self.device_type = hivedevice["HA_DeviceType"] self.node_device_type = hivedevice["Hive_DeviceType"] self.session = hivesession - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self.session.entities.append(self) @property @@ -53,7 +53,7 @@ class HiveSensorEntity(Entity): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 4644ccaec00..75efdfe3e5d 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -23,8 +23,8 @@ class HiveDevicePlug(SwitchDevice): self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession self.attributes = {} - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self.session.entities.append(self) @property @@ -39,7 +39,7 @@ class HiveDevicePlug(SwitchDevice): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index f186d804d34..1b009582c1a 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -42,8 +42,8 @@ class HiveWaterHeater(WaterHeaterDevice): self.node_name = hivedevice["Hive_NodeName"] self.device_type = hivedevice["HA_DeviceType"] self.session = hivesession - self.data_updatesource = "{}.{}".format(self.device_type, self.node_id) - self._unique_id = "{}-{}".format(self.node_id, self.device_type) + self.data_updatesource = f"{self.device_type}.{self.node_id}" + self._unique_id = f"{self.node_id}-{self.device_type}" self._unit_of_measurement = TEMP_CELSIUS @property @@ -63,7 +63,7 @@ class HiveWaterHeater(WaterHeaterDevice): def handle_update(self, updatesource): """Handle the new update request.""" - if "{}.{}".format(self.device_type, self.node_id) not in updatesource: + if f"{self.device_type}.{self.node_id}" not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 2bd0a62cebb..02e53d1de10 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -110,7 +110,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: hass.components.persistent_notification.async_create( "Config error. See dev-info panel for details.", "Config validating", - "{0}.check_config".format(ha.DOMAIN), + f"{ha.DOMAIN}.check_config", ) return diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index fce81d0adf7..8e1b07fbbff 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -127,9 +127,7 @@ class Light(HomeAccessory): self.set_state(0) # Turn off light return params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} - self.call_service( - DOMAIN, SERVICE_TURN_ON, params, "brightness at {}%".format(value) - ) + self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"brightness at {value}%") def set_color_temperature(self, value): """Set color temperature if call came from HomeKit.""" @@ -137,7 +135,7 @@ class Light(HomeAccessory): self._flag[CHAR_COLOR_TEMPERATURE] = True params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} self.call_service( - DOMAIN, SERVICE_TURN_ON, params, "color temperature at {}".format(value) + DOMAIN, SERVICE_TURN_ON, params, f"color temperature at {value}" ) def set_saturation(self, value): @@ -167,9 +165,7 @@ class Light(HomeAccessory): {CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True} ) params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} - self.call_service( - DOMAIN, SERVICE_TURN_ON, params, "set color at {}".format(color) - ) + self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"set color at {color}") def update_state(self, new_state): """Update light after state change.""" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index e00912d340e..63eb688a0c1 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -209,7 +209,7 @@ class Thermostat(HomeAccessory): DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, params, - "cooling threshold {}{}".format(temperature, self._unit), + f"cooling threshold {temperature}{self._unit}", ) @debounce @@ -230,7 +230,7 @@ class Thermostat(HomeAccessory): DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, params, - "heating threshold {}{}".format(temperature, self._unit), + f"heating threshold {temperature}{self._unit}", ) @debounce @@ -244,7 +244,7 @@ class Thermostat(HomeAccessory): DOMAIN_CLIMATE, SERVICE_SET_TEMPERATURE_THERMOSTAT, params, - "{}{}".format(temperature, self._unit), + f"{temperature}{self._unit}", ) def update_state(self, new_state): @@ -378,7 +378,7 @@ class WaterHeater(HomeAccessory): DOMAIN_WATER_HEATER, SERVICE_SET_TEMPERATURE_WATER_HEATER, params, - "{}{}".format(temperature, self._unit), + f"{temperature}{self._unit}", ) def update_state(self, new_state): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 3b5f3c81436..d60c94d420d 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -116,9 +116,7 @@ def validate_entity_config(values): params = MEDIA_PLAYER_SCHEMA(feature) key = params.pop(CONF_FEATURE) if key in feature_list: - raise vol.Invalid( - "A feature can be added only once for {}".format(entity) - ) + raise vol.Invalid(f"A feature can be added only once for {entity}") feature_list[key] = params config[CONF_FEATURE_LIST] = feature_list diff --git a/homeassistant/components/homekit_controller/.translations/it.json b/homeassistant/components/homekit_controller/.translations/it.json index a1d460d12dc..7ed026a529c 100644 --- a/homeassistant/components/homekit_controller/.translations/it.json +++ b/homeassistant/components/homekit_controller/.translations/it.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "accessory_not_found_error": "Impossibile aggiungere l'abbinamento in quanto non \u00e8 pi\u00f9 possibile trovare il dispositivo.", "already_configured": "L'accessorio \u00e8 gi\u00e0 configurato con questo controller.", "already_in_progress": "Il flusso di configurazione per il dispositivo \u00e8 gi\u00e0 in corso.", "already_paired": "Questo accessorio \u00e8 gi\u00e0 associato a un altro dispositivo. Si prega di resettare l'accessorio e riprovare.", @@ -10,6 +11,10 @@ }, "error": { "authentication_error": "Codice HomeKit errato. Per favore, controllate e riprovate.", + "busy_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto \u00e8 gi\u00e0 associato a un altro controller.", + "max_peers_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento in quanto non dispone di una memoria libera per esso.", + "max_tries_error": "Il dispositivo ha rifiutato di aggiungere l'abbinamento poich\u00e9 ha ricevuto pi\u00f9 di 100 tentativi di autenticazione non riusciti.", + "pairing_failed": "Si \u00e8 verificato un errore non gestito durante il tentativo di abbinamento con questo dispositivo. Potrebbe trattarsi di un errore temporaneo o il dispositivo potrebbe non essere attualmente supportato.", "unable_to_pair": "Impossibile abbinare, per favore riprova.", "unknown_error": "Il dispositivo ha riportato un errore sconosciuto. L'abbinamento non \u00e8 riuscito." }, @@ -19,7 +24,7 @@ "data": { "pairing_code": "Codice di abbinamento" }, - "description": "Inserisci il codice di abbinamento HomeKit per usare questo accessorio", + "description": "Immettere il codice di abbinamento HomeKit (nel formato XXX-XX-XXX) per utilizzare questo accessorio", "title": "Abbina con accessorio HomeKit" }, "user": { diff --git a/homeassistant/components/homekit_controller/.translations/pl.json b/homeassistant/components/homekit_controller/.translations/pl.json index 031a7440ed0..e66353c5000 100644 --- a/homeassistant/components/homekit_controller/.translations/pl.json +++ b/homeassistant/components/homekit_controller/.translations/pl.json @@ -13,8 +13,8 @@ "authentication_error": "Niepoprawny kod parowania HomeKit. Sprawd\u017a go i spr\u00f3buj ponownie.", "busy_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c jest ju\u017c powi\u0105zane z innym kontrolerem.", "max_peers_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c nie ma wolnej pami\u0119ci parowania.", - "max_tries_error": "Urz\u0105dzenie odm\u00f3wi\u0142o parowania, poniewa\u017c otrzyma\u0142o ponad 100 nieudanych pr\u00f3b uwierzytelnienia.", - "pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub Twoje urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.", + "max_tries_error": "Urz\u0105dzenie odm\u00f3wi\u0142o dodania parowania, poniewa\u017c otrzyma\u0142o ponad 100 nieudanych pr\u00f3b uwierzytelnienia.", + "pairing_failed": "Wyst\u0105pi\u0142 nieobs\u0142ugiwany b\u0142\u0105d podczas pr\u00f3by sparowania z tym urz\u0105dzeniem. Mo\u017ce to by\u0107 tymczasowa awaria lub urz\u0105dzenie mo\u017ce nie by\u0107 obecnie obs\u0142ugiwane.", "unable_to_pair": "Nie mo\u017cna sparowa\u0107, spr\u00f3buj ponownie.", "unknown_error": "Urz\u0105dzenie zg\u0142osi\u0142o nieznany b\u0142\u0105d. Parowanie nie powiod\u0142o si\u0119." }, diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 5ae82d0f124..6a649284722 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -106,7 +106,7 @@ class HomeKitEntity(Entity): # Callback to allow entity to configure itself based on this # characteristics metadata (valid values, value ranges, features, etc) setup_fn_name = escape_characteristic_name(short_name) - setup_fn = getattr(self, "_setup_{}".format(setup_fn_name), None) + setup_fn = getattr(self, f"_setup_{setup_fn_name}", None) if not setup_fn: return # pylint: disable=not-callable @@ -128,7 +128,7 @@ class HomeKitEntity(Entity): # Callback to update the entity with this characteristic value char_name = escape_characteristic_name(self._char_names[iid]) - update_fn = getattr(self, "_update_{}".format(char_name), None) + update_fn = getattr(self, f"_update_{char_name}", None) if not update_fn: continue @@ -141,7 +141,7 @@ class HomeKitEntity(Entity): def unique_id(self): """Return the ID of this device.""" serial = self._accessory_info["serial-number"] - return "homekit-{}-{}".format(serial, self._iid) + return f"homekit-{serial}-{self._iid}" @property def name(self): diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 6aa5dc93662..09a7df2a2bf 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -1,9 +1,9 @@ """Constants for the homekit_controller component.""" DOMAIN = "homekit_controller" -KNOWN_DEVICES = "{}-devices".format(DOMAIN) -CONTROLLER = "{}-controller".format(DOMAIN) -ENTITY_MAP = "{}-entity-map".format(DOMAIN) +KNOWN_DEVICES = f"{DOMAIN}-devices" +CONTROLLER = f"{DOMAIN}-controller" +ENTITY_MAP = f"{DOMAIN}-entity-map" HOMEKIT_DIR = ".homekit" PAIRING_FILE = "pairing.json" @@ -25,4 +25,5 @@ HOMEKIT_ACCESSORY_DISPATCH = { "humidity": "sensor", "light": "sensor", "temperature": "sensor", + "battery": "sensor", } diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 596b697bede..f91dae26ba0 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,7 +1,7 @@ """Support for Homekit sensors.""" from homekit.model.characteristics import CharacteristicsTypes -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import DEVICE_CLASS_BATTERY, TEMP_CELSIUS from . import KNOWN_DEVICES, HomeKitEntity @@ -30,7 +30,7 @@ class HomeKitHumiditySensor(HomeKitEntity): @property def name(self): """Return the name of the device.""" - return "{} {}".format(super().name, "Humidity") + return f"{super().name} Humidity" @property def icon(self): @@ -66,7 +66,7 @@ class HomeKitTemperatureSensor(HomeKitEntity): @property def name(self): """Return the name of the device.""" - return "{} {}".format(super().name, "Temperature") + return f"{super().name} Temperature" @property def icon(self): @@ -102,7 +102,7 @@ class HomeKitLightSensor(HomeKitEntity): @property def name(self): """Return the name of the device.""" - return "{} {}".format(super().name, "Light Level") + return f"{super().name} Light Level" @property def icon(self): @@ -138,7 +138,7 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity): @property def name(self): """Return the name of the device.""" - return "{} {}".format(super().name, "CO2") + return f"{super().name} CO2" @property def icon(self): @@ -159,11 +159,85 @@ class HomeKitCarbonDioxideSensor(HomeKitEntity): return self._state +class HomeKitBatterySensor(HomeKitEntity): + """Representation of a Homekit battery sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + self._low_battery = False + self._charging = False + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [ + CharacteristicsTypes.BATTERY_LEVEL, + CharacteristicsTypes.STATUS_LO_BATT, + CharacteristicsTypes.CHARGING_STATE, + ] + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def name(self): + """Return the name of the device.""" + return f"{super().name} Battery" + + @property + def icon(self): + """Return the sensor icon.""" + if not self.available or self.state is None: + return "mdi:battery-unknown" + + # This is similar to the logic in helpers.icon, but we have delegated the + # decision about what mdi:battery-alert is to the device. + icon = "mdi:battery" + if self._charging and self.state > 10: + percentage = int(round(self.state / 20 - 0.01)) * 20 + icon += f"-charging-{percentage}" + elif self._charging: + icon += "-outline" + elif self._low_battery: + icon += "-alert" + elif self.state < 95: + percentage = max(int(round(self.state / 10 - 0.01)) * 10, 10) + icon += f"-{percentage}" + + return icon + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return UNIT_PERCENT + + def _update_battery_level(self, value): + self._state = value + + def _update_status_lo_batt(self, value): + self._low_battery = value == 1 + + def _update_charging_state(self, value): + # 0 = not charging + # 1 = charging + # 2 = not chargeable + self._charging = value == 1 + + @property + def state(self): + """Return the current battery level percentage.""" + return self._state + + ENTITY_TYPES = { "humidity": HomeKitHumiditySensor, "temperature": HomeKitTemperatureSensor, "light": HomeKitLightSensor, "carbon-dioxide": HomeKitCarbonDioxideSensor, + "battery": HomeKitBatterySensor, } diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index ec5a2e7cc43..46d095b5631 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from .const import DOMAIN -ENTITY_MAP_STORAGE_KEY = "{}-entity-map".format(DOMAIN) +ENTITY_MAP_STORAGE_KEY = f"{DOMAIN}-entity-map" ENTITY_MAP_STORAGE_VERSION = 1 ENTITY_MAP_SAVE_DELAY = 10 diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 0ab47247edc..598e3765612 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -711,15 +711,15 @@ def _create_ha_id(name, channel, param, count): # Has multiple elements/channels if count > 1 and param is None: - return "{} {}".format(name, channel) + return f"{name} {channel}" # With multiple parameters on first channel if count == 1 and param is not None: - return "{} {}".format(name, param) + return f"{name} {param}" # Multiple parameters with multiple channels if count > 1 and param is not None: - return "{} {} {}".format(name, channel, param) + return f"{name} {channel} {param}" def _hm_event_handler(hass, interface, device, caller, attribute, value): diff --git a/homeassistant/components/homematicip_cloud/.translations/it.json b/homeassistant/components/homematicip_cloud/.translations/it.json index 6e6d7c8a59f..c7f1af21f22 100644 --- a/homeassistant/components/homematicip_cloud/.translations/it.json +++ b/homeassistant/components/homematicip_cloud/.translations/it.json @@ -15,7 +15,7 @@ "init": { "data": { "hapid": "ID del punto di accesso (SGTIN)", - "name": "Nome (facoltativo, utilizzato come prefisso del nome per tutti i dispositivi)", + "name": "Nome (opzionale, usato come prefisso del nome per tutti i dispositivi)", "pin": "Codice Pin (opzionale)" }, "title": "Scegli punto di accesso HomematicIP" diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index f2d84095b19..c8fb31998ef 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -234,7 +234,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: device_registry = await dr.async_get_registry(hass) home = hap.home # Add the HAP name from configuration if set. - hapname = home.label if not home.name else "{} {}".format(home.label, home.name) + hapname = home.label if not home.name else f"{home.label} {home.name}" device_registry.async_get_or_create( config_entry_id=home.id, identifiers={(DOMAIN, home.id)}, diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 38097afc1b6..592d234225c 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -112,7 +112,7 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): """Return the name of the generic device.""" name = CONST_ALARM_CONTROL_PANEL_NAME if self._home.name: - name = "{} {}".format(self._home.name, name) + name = f"{self._home.name} {name}" return name @property @@ -131,7 +131,7 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}".format(self.__class__.__name__, self._home.id) + return f"{self.__class__.__name__}_{self._home.id}" def _get_zone_alarm_state(security_zone) -> bool: diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 97746f3f472..594f4f6c54a 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -43,14 +43,25 @@ from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) ATTR_LOW_BATTERY = "low_battery" -ATTR_MOTIONDETECTED = "motion detected" -ATTR_PRESENCEDETECTED = "presence detected" -ATTR_POWERMAINSFAILURE = "power mains failure" -ATTR_WINDOWSTATE = "window state" -ATTR_MOISTUREDETECTED = "moisture detected" -ATTR_WATERLEVELDETECTED = "water level detected" -ATTR_SMOKEDETECTORALARM = "smoke detector alarm" +ATTR_MOISTURE_DETECTED = "moisture_detected" +ATTR_MOTION_DETECTED = "motion_detected" +ATTR_POWER_MAINS_FAILURE = "power_mains_failure" +ATTR_PRESENCE_DETECTED = "presence_detected" +ATTR_SMOKE_DETECTOR_ALARM = "smoke_detector_alarm" ATTR_TODAY_SUNSHINE_DURATION = "today_sunshine_duration_in_minutes" +ATTR_WATER_LEVEL_DETECTED = "water_level_detected" +ATTR_WINDOW_STATE = "window_state" + +GROUP_ATTRIBUTES = { + "lowBat": ATTR_LOW_BATTERY, + "modelType": ATTR_MODEL_TYPE, + "moistureDetected": ATTR_MOISTURE_DETECTED, + "motionDetected": ATTR_MOTION_DETECTED, + "powerMainsFailure": ATTR_POWER_MAINS_FAILURE, + "presenceDetected": ATTR_PRESENCE_DETECTED, + "unreach": ATTR_GROUP_MEMBER_UNREACHABLE, + "waterlevelDetected": ATTR_WATER_LEVEL_DETECTED, +} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -118,8 +129,6 @@ class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self) -> bool: """Return true if the contact interface is on/open.""" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return True if self._device.windowState is None: return None return self._device.windowState != WindowState.CLOSED @@ -136,8 +145,6 @@ class HomematicipShutterContact(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self) -> bool: """Return true if the shutter contact is on/open.""" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return True if self._device.windowState is None: return None return self._device.windowState != WindowState.CLOSED @@ -154,8 +161,6 @@ class HomematicipMotionDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self) -> bool: """Return true if motion is detected.""" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return True return self._device.motionDetected @@ -170,8 +175,6 @@ class HomematicipPresenceDetector(HomematicipGenericDevice, BinarySensorDevice): @property def is_on(self) -> bool: """Return true if presence is detected.""" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return True return self._device.presenceDetected @@ -259,13 +262,13 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): @property def device_state_attributes(self): """Return the state attributes of the illuminance sensor.""" - attr = super().device_state_attributes - if ( - hasattr(self._device, "todaySunshineDuration") - and self._device.todaySunshineDuration - ): - attr[ATTR_TODAY_SUNSHINE_DURATION] = self._device.todaySunshineDuration - return attr + state_attr = super().device_state_attributes + + today_sunshine_duration = getattr(self._device, "todaySunshineDuration", None) + if today_sunshine_duration: + state_attr[ATTR_TODAY_SUNSHINE_DURATION] = today_sunshine_duration + + return state_attr class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): @@ -291,7 +294,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD def __init__(self, home: AsyncHome, device, post: str = "SecurityZone") -> None: """Initialize security zone group.""" - device.modelType = "HmIP-{}".format(post) + device.modelType = f"HmIP-{post}" super().__init__(home, device, post) @property @@ -309,21 +312,18 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - attr = {ATTR_MODEL_TYPE: self._device.modelType} + state_attr = {ATTR_MODEL_TYPE: self._device.modelType} - if self._device.motionDetected: - attr[ATTR_MOTIONDETECTED] = True - if self._device.presenceDetected: - attr[ATTR_PRESENCEDETECTED] = True + for attr, attr_key in GROUP_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value - if ( - self._device.windowState is not None - and self._device.windowState != WindowState.CLOSED - ): - attr[ATTR_WINDOWSTATE] = str(self._device.windowState) - if self._device.unreach: - attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True - return attr + window_state = getattr(self._device, "windowState", None) + if window_state and window_state != WindowState.CLOSED: + state_attr[ATTR_WINDOW_STATE] = str(window_state) + + return state_attr @property def is_on(self) -> bool: @@ -356,23 +356,13 @@ class HomematicipSecuritySensorGroup( @property def device_state_attributes(self): """Return the state attributes of the security group.""" - attr = super().device_state_attributes + state_attr = super().device_state_attributes - if self._device.powerMainsFailure: - attr[ATTR_POWERMAINSFAILURE] = True - if self._device.moistureDetected: - attr[ATTR_MOISTUREDETECTED] = True - if self._device.waterlevelDetected: - attr[ATTR_WATERLEVELDETECTED] = True - if self._device.lowBat: - attr[ATTR_LOW_BATTERY] = True - if ( - self._device.smokeDetectorAlarmType is not None - and self._device.smokeDetectorAlarmType != SmokeDetectorAlarmType.IDLE_OFF - ): - attr[ATTR_SMOKEDETECTORALARM] = str(self._device.smokeDetectorAlarmType) + smoke_detector_at = getattr(self._device, "smokeDetectorAlarmType", None) + if smoke_detector_at and smoke_detector_at != SmokeDetectorAlarmType.IDLE_OFF: + state_attr[ATTR_SMOKE_DETECTOR_ALARM] = str(smoke_detector_at) - return attr + return state_attr @property def is_on(self) -> bool: diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 71855d7c3f5..5eeb14b6359 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -92,9 +92,9 @@ class HomematicipGenericDevice(Entity): """Return the name of the generic device.""" name = self._device.label if self._home.name is not None and self._home.name != "": - name = "{} {}".format(self._home.name, name) + name = f"{self._home.name} {name}" if self.post is not None and self.post != "": - name = "{} {}".format(name, self.post) + name = f"{name} {self.post}" return name @property @@ -110,7 +110,7 @@ class HomematicipGenericDevice(Entity): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}".format(self.__class__.__name__, self._device.id) + return f"{self.__class__.__name__}_{self._device.id}" @property def icon(self) -> Optional[str]: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index c034b19bb3a..42ff6d30478 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -93,13 +93,15 @@ class HomematicipLightMeasuring(HomematicipLight): @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = super().device_state_attributes - if self._device.currentPowerConsumption > 0.05: - attr[ATTR_POWER_CONSUMPTION] = round( - self._device.currentPowerConsumption, 2 - ) - attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2) - return attr + state_attr = super().device_state_attributes + + current_power_consumption = self._device.currentPowerConsumption + if current_power_consumption > 0.05: + state_attr[ATTR_POWER_CONSUMPTION] = round(current_power_consumption, 2) + + state_attr[ATTR_ENERGY_COUNTER] = round(self._device.energyCounter, 2) + + return state_attr class HomematicipDimmer(HomematicipGenericDevice, Light): @@ -187,15 +189,17 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = super().device_state_attributes + state_attr = super().device_state_attributes + if self.is_on: - attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState - return attr + state_attr[ATTR_COLOR_NAME] = self._func_channel.simpleRGBColorState + + return state_attr @property def name(self) -> str: """Return the name of the generic device.""" - return "{} {}".format(super().name, "Notification") + return f"{super().name} Notification" @property def supported_features(self) -> int: @@ -205,7 +209,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}_{}".format(self.__class__.__name__, self.post, self._device.id) + return f"{self.__class__.__name__}_{self.post}_{self._device.id}" async def async_turn_on(self, **kwargs): """Turn the light on.""" diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index ee0d2cb1271..2a041ce6689 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -7,5 +7,7 @@ "homematicip==0.10.10" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@SukramJ" + ] } diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index c15b3121d3a..43812df94d2 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -10,6 +10,7 @@ from homematicip.aio.device import ( AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, + AsyncPassageDetector, AsyncPlugableSwitchMeasuring, AsyncPresenceDetectorIndoor, AsyncTemperatureHumiditySensorDisplay, @@ -38,6 +39,8 @@ from .device import ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) +ATTR_LEFT_COUNTER = "left_counter" +ATTR_RIGHT_COUNTER = "right_counter" ATTR_TEMPERATURE_OFFSET = "temperature_offset" ATTR_WIND_DIRECTION = "wind_direction" ATTR_WIND_DIRECTION_VARIATION = "wind_direction_variation_in_degree" @@ -100,6 +103,8 @@ async def async_setup_entry( devices.append(HomematicipWindspeedSensor(home, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): devices.append(HomematicipTodayRainSensor(home, device)) + if isinstance(device, AsyncPassageDetector): + devices.append(HomematicipPassageDetectorDeltaCounter(home, device)) if devices: async_add_entities(devices) @@ -229,13 +234,13 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): @property def device_state_attributes(self): """Return the state attributes of the windspeed sensor.""" - attr = super().device_state_attributes - if ( - hasattr(self._device, "temperatureOffset") - and self._device.temperatureOffset - ): - attr[ATTR_TEMPERATURE_OFFSET] = self._device.temperatureOffset - return attr + state_attr = super().device_state_attributes + + temperature_offset = getattr(self._device, "temperatureOffset", None) + if temperature_offset: + state_attr[ATTR_TEMPERATURE_OFFSET] = temperature_offset + + return state_attr class HomematicipIlluminanceSensor(HomematicipGenericDevice): @@ -307,15 +312,17 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): @property def device_state_attributes(self): """Return the state attributes of the wind speed sensor.""" - attr = super().device_state_attributes - if hasattr(self._device, "windDirection") and self._device.windDirection: - attr[ATTR_WIND_DIRECTION] = _get_wind_direction(self._device.windDirection) - if ( - hasattr(self._device, "windDirectionVariation") - and self._device.windDirectionVariation - ): - attr[ATTR_WIND_DIRECTION_VARIATION] = self._device.windDirectionVariation - return attr + state_attr = super().device_state_attributes + + wind_direction = getattr(self._device, "windDirection", None) + if wind_direction: + state_attr[ATTR_WIND_DIRECTION] = _get_wind_direction(wind_direction) + + wind_direction_variation = getattr(self._device, "windDirectionVariation", None) + if wind_direction_variation: + state_attr[ATTR_WIND_DIRECTION_VARIATION] = wind_direction_variation + + return state_attr class HomematicipTodayRainSensor(HomematicipGenericDevice): @@ -336,6 +343,29 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): return "mm" +class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice): + """Representation of a HomematicIP passage detector delta counter.""" + + def __init__(self, home: AsyncHome, device) -> None: + """Initialize the device.""" + super().__init__(home, device) + + @property + def state(self) -> int: + """Representation of the HomematicIP passage detector delta counter value.""" + return self._device.leftRightCounterDelta + + @property + def device_state_attributes(self): + """Return the state attributes of the delta counter.""" + state_attr = super().device_state_attributes + + state_attr[ATTR_LEFT_COUNTER] = self._device.leftCounter + state_attr[ATTR_RIGHT_COUNTER] = self._device.rightCounter + + return state_attr + + def _get_wind_direction(wind_direction_degree: float) -> str: """Convert wind direction degree to named direction.""" if 11.25 <= wind_direction_degree < 33.75: diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index a9535736d0f..058e21262e3 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -93,7 +93,7 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): def __init__(self, home: AsyncHome, device, post: str = "Group") -> None: """Initialize switching group.""" - device.modelType = "HmIP-{}".format(post) + device.modelType = f"HmIP-{post}" super().__init__(home, device, post) @property @@ -113,10 +113,10 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): @property def device_state_attributes(self): """Return the state attributes of the switch-group.""" - attr = {} + state_attr = {} if self._device.unreach: - attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True - return attr + state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True + return state_attr async def async_turn_on(self, **kwargs): """Turn the group on.""" @@ -149,12 +149,12 @@ class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): def __init__(self, home: AsyncHome, device, channel: int): """Initialize the multi switch device.""" self.channel = channel - super().__init__(home, device, "Channel{}".format(channel)) + super().__init__(home, device, f"Channel{channel}") @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}_{}_{}".format(self.__class__.__name__, self.post, self._device.id) + return f"{self.__class__.__name__}_{self.post}_{self._device.id}" @property def is_on(self) -> bool: diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 463e1bfb741..2d0a69d7d06 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -79,7 +79,7 @@ class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): @property def condition(self) -> str: """Return the current condition.""" - if hasattr(self._device, "raining") and self._device.raining: + if getattr(self._device, "raining", None): return "rainy" if self._device.storm: return "windy" diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index dcc2ce5dde6..bd40336b8ba 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -106,7 +106,7 @@ class HomeworksDevice: @property def unique_id(self): """Return a unique identifier.""" - return "homeworks.{}".format(self._addr) + return f"homeworks.{self._addr}" @property def name(self): diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 62a370f60fa..4b73cf4f2b5 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -161,9 +161,7 @@ class HoneywellUSThermostat(ClimateDevice): self._password = password _LOGGER.debug( - # noqa; pylint: disable=protected-access - "latestData = %s ", - device._data, + "latestData = %s ", device._data # pylint: disable=protected-access ) # not all honeywell HVACs support all modes @@ -176,8 +174,7 @@ class HoneywellUSThermostat(ClimateDevice): | SUPPORT_TARGET_TEMPERATURE_RANGE ) - # noqa; pylint: disable=protected-access - if device._data["canControlHumidification"]: + if device._data["canControlHumidification"]: # pylint: disable=protected-access self._supported_features |= SUPPORT_TARGET_HUMIDITY if device.raw_ui_data["SwitchEmergencyHeatAllowed"]: @@ -318,19 +315,17 @@ class HoneywellUSThermostat(ClimateDevice): # Get current mode mode = self._device.system_mode # Set hold if this is not the case - if getattr(self._device, "hold_{}".format(mode)) is False: + if getattr(self._device, f"hold_{mode}") is False: # Get next period key - next_period_key = "{}NextPeriod".format(mode.capitalize()) + next_period_key = f"{mode.capitalize()}NextPeriod" # Get next period raw value next_period = self._device.raw_ui_data.get(next_period_key) # Get next period time hour, minute = divmod(next_period * 15, 60) # Set hold time - setattr( - self._device, "hold_{}".format(mode), datetime.time(hour, minute) - ) + setattr(self._device, f"hold_{mode}", datetime.time(hour, minute)) # Set temperature - setattr(self._device, "setpoint_{}".format(mode), temperature) + setattr(self._device, f"setpoint_{mode}", temperature) except somecomfort.SomeComfortError: _LOGGER.error("Temperature %.1f out of range", temperature) @@ -375,17 +370,14 @@ class HoneywellUSThermostat(ClimateDevice): try: # Set permanent hold - setattr(self._device, "hold_{}".format(mode), True) + setattr(self._device, f"hold_{mode}", True) # Set temperature setattr( - self._device, - "setpoint_{}".format(mode), - getattr(self, "_{}_away_temp".format(mode)), + self._device, f"setpoint_{mode}", getattr(self, f"_{mode}_away_temp") ) except somecomfort.SomeComfortError: _LOGGER.error( - "Temperature %.1f out of range", - getattr(self, "_{}_away_temp".format(mode)), + "Temperature %.1f out of range", getattr(self, f"_{mode}_away_temp") ) def _turn_away_mode_off(self) -> None: @@ -465,7 +457,5 @@ class HoneywellUSThermostat(ClimateDevice): _LOGGER.error("SomeComfort update failed, Retrying - Error: %s", exp) _LOGGER.debug( - # noqa; pylint: disable=protected-access - "latestData = %s ", - self._device._data, + "latestData = %s ", self._device._data # pylint: disable=protected-access ) diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index 1ad70c06397..cf95c21a8d1 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -194,4 +194,4 @@ class HpIloData: hpilo.IloCommunicationError, hpilo.IloLoginFailed, ) as error: - raise ValueError("Unable to init HP ILO, {}".format(error)) + raise ValueError(f"Unable to init HP ILO, {error}") diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 18882968cf9..ac76911b9f6 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -570,8 +570,8 @@ def create_vapid_headers(vapid_email, subscription_info, vapid_private_key): if vapid_email and vapid_private_key and ATTR_ENDPOINT in subscription_info: url = urlparse(subscription_info.get(ATTR_ENDPOINT)) vapid_claims = { - "sub": "mailto:{}".format(vapid_email), - "aud": "{}://{}".format(url.scheme, url.netloc), + "sub": f"mailto:{vapid_email}", + "aud": f"{url.scheme}://{url.netloc}", } vapid = Vapid.from_string(private_key=vapid_private_key) return vapid.sign(vapid_claims) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 5e474dafa07..a8aaa3390a7 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -133,12 +133,12 @@ class ApiConfig: if host.startswith(("http://", "https://")): self.base_url = host elif use_ssl: - self.base_url = "https://{}".format(host) + self.base_url = f"https://{host}" else: - self.base_url = "http://{}".format(host) + self.base_url = f"http://{host}" if port is not None: - self.base_url += ":{}".format(port) + self.base_url += f":{port}" async def async_setup(hass, config): @@ -268,15 +268,11 @@ class HomeAssistantHTTP: if not hasattr(view, "url"): class_name = view.__class__.__name__ - raise AttributeError( - '{0} missing required attribute "url"'.format(class_name) - ) + raise AttributeError(f'{class_name} missing required attribute "url"') if not hasattr(view, "name"): class_name = view.__class__.__name__ - raise AttributeError( - '{0} missing required attribute "name"'.format(class_name) - ) + raise AttributeError(f'{class_name} missing required attribute "name"') view.register(self.app, self.app.router) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 71e7ff38924..d8fa8853c7f 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -127,7 +127,7 @@ async def process_wrong_login(request): _LOGGER.warning("Banned IP %s for too many login attempts", remote_addr) hass.components.persistent_notification.async_create( - "Too many login attempts from {}".format(remote_addr), + f"Too many login attempts from {remote_addr}", "Banning IP address", NOTIFICATION_ID_BAN, ) diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 634a96aa312..5945a4ca402 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -43,9 +43,7 @@ class RequestDataValidator: kwargs["data"] = self._schema(data) except vol.Invalid as err: _LOGGER.error("Data does not match schema: %s", err) - return view.json_message( - "Message format incorrect: {}".format(err), 400 - ) + return view.json_message(f"Message format incorrect: {err}", 400) result = await method(view, request, *args, **kwargs) return result diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 76844407f7d..952ca473fdc 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -10,7 +10,7 @@ from aiohttp.web_urldispatcher import StaticResource # mypy: allow-untyped-defs CACHE_TIME = 31 * 86400 # = 1 month -CACHE_HEADERS = {hdrs.CACHE_CONTROL: "public, max-age={}".format(CACHE_TIME)} +CACHE_HEADERS = {hdrs.CACHE_CONTROL: f"public, max-age={CACHE_TIME}"} # https://github.com/PyCQA/astroid/issues/633 diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index c2223720eb5..f94b11d5ada 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -76,7 +76,7 @@ class HTU21DSensor(Entity): def __init__(self, htu21d_client, name, variable, unit): """Initialize the sensor.""" - self._name = "{}_{}".format(name, variable) + self._name = f"{name}_{variable}" self._variable = variable self._unit_of_measurement = unit self._client = htu21d_client diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 2cbc271219b..f09788b7220 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,4 +1,5 @@ """Support for Huawei LTE routers.""" + from datetime import timedelta from functools import reduce from urllib.parse import urlparse @@ -22,6 +23,14 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle +from .const import ( + DOMAIN, + KEY_DEVICE_INFORMATION, + KEY_DEVICE_SIGNAL, + KEY_MONITORING_TRAFFIC_STATISTICS, + KEY_WLAN_HOST_LIST, +) + _LOGGER = logging.getLogger(__name__) @@ -31,9 +40,6 @@ logging.getLogger("dicttoxml").setLevel(logging.WARNING) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) -DOMAIN = "huawei_lte" -DATA_KEY = "huawei_lte" - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -107,12 +113,12 @@ class RouterData: finally: _LOGGER.debug("%s=%s", path, getattr(self, path)) - get_data("device_information", self.client.device.information) - get_data("device_signal", self.client.device.signal) + get_data(KEY_DEVICE_INFORMATION, self.client.device.information) + get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) get_data( - "monitoring_traffic_statistics", self.client.monitoring.traffic_statistics + KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) - get_data("wlan_host_list", self.client.wlan.host_list) + get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) @attr.s @@ -133,8 +139,8 @@ class HuaweiLteData: def setup(hass, config) -> bool: """Set up Huawei LTE component.""" - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = HuaweiLteData() + if DOMAIN not in hass.data: + hass.data[DOMAIN] = HuaweiLteData() for conf in config.get(DOMAIN, []): _setup_lte(hass, conf) return True @@ -164,10 +170,13 @@ def _setup_lte(hass, lte_config) -> None: client = Client(connection) data = RouterData(client, mac) - hass.data[DATA_KEY].data[url] = data + hass.data[DOMAIN].data[url] = data def cleanup(event): """Clean up resources.""" - client.user.logout() + try: + client.user.logout() + except ResponseErrorNotSupportedException as ex: + _LOGGER.debug("Logout not supported by device", exc_info=ex) hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py new file mode 100644 index 00000000000..0134417d5fe --- /dev/null +++ b/homeassistant/components/huawei_lte/const.py @@ -0,0 +1,8 @@ +"""Huawei LTE constants.""" + +DOMAIN = "huawei_lte" + +KEY_DEVICE_INFORMATION = "device_information" +KEY_DEVICE_SIGNAL = "device_signal" +KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" +KEY_WLAN_HOST_LIST = "wlan_host_list" diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 697b2a3ed3c..bad9253f4e7 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,4 +1,5 @@ """Support for device tracking of Huawei LTE routers.""" + import logging from typing import Any, Dict, List, Optional @@ -8,19 +9,20 @@ import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import PLATFORM_SCHEMA, DeviceScanner from homeassistant.const import CONF_URL -from . import DATA_KEY, RouterData +from . import RouterData +from .const import DOMAIN, KEY_WLAN_HOST_LIST _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_URL): cv.url}) -HOSTS_PATH = "wlan_host_list.Hosts.Host" +HOSTS_PATH = f"{KEY_WLAN_HOST_LIST}.Hosts.Host" def get_scanner(hass, config): """Get a Huawei LTE router scanner.""" - data = hass.data[DATA_KEY].get_data(config) + data = hass.data[DOMAIN].get_data(config) data.subscribe(HOSTS_PATH) return HuaweiLteScanner(data) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 85077511768..3af23be4f0b 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/huawei_lte", "requirements": [ "getmac==0.8.1", - "huawei-lte-api==1.2.0" + "huawei-lte-api==1.3.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 31804f722c6..e882509c04c 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -1,4 +1,5 @@ """Support for Huawei LTE router notifications.""" + import logging import voluptuous as vol @@ -12,7 +13,8 @@ from homeassistant.components.notify import ( from homeassistant.const import CONF_RECIPIENT, CONF_URL import homeassistant.helpers.config_validation as cv -from . import DATA_KEY +from .const import DOMAIN + _LOGGER = logging.getLogger(__name__) @@ -44,7 +46,7 @@ class HuaweiLteSmsNotificationService(BaseNotificationService): if not targets or not message: return - data = self.hass.data[DATA_KEY].get_data(self.config) + data = self.hass.data[DOMAIN].get_data(self.config) if not data: _LOGGER.error("Router not available") return diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index da78dc7d8cf..cb8f5fb5766 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -1,4 +1,5 @@ """Support for Huawei LTE sensors.""" + import logging import re from typing import Optional @@ -15,7 +16,14 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -from . import DATA_KEY, RouterData +from . import RouterData +from .const import ( + DOMAIN, + KEY_DEVICE_INFORMATION, + KEY_DEVICE_SIGNAL, + KEY_MONITORING_TRAFFIC_STATISTICS, +) + _LOGGER = logging.getLogger(__name__) @@ -23,26 +31,30 @@ DEFAULT_NAME_TEMPLATE = "Huawei {} {}" DEFAULT_DEVICE_NAME = "LTE" DEFAULT_SENSORS = [ - "device_information.WanIPAddress", - "device_signal.rsrq", - "device_signal.rsrp", - "device_signal.rssi", - "device_signal.sinr", + f"{KEY_DEVICE_INFORMATION}.WanIPAddress", + f"{KEY_DEVICE_SIGNAL}.rsrq", + f"{KEY_DEVICE_SIGNAL}.rsrp", + f"{KEY_DEVICE_SIGNAL}.rssi", + f"{KEY_DEVICE_SIGNAL}.sinr", ] SENSOR_META = { - "device_information.SoftwareVersion": dict(name="Software version"), - "device_information.WanIPAddress": dict(name="WAN IP address", icon="mdi:ip"), - "device_information.WanIPv6Address": dict(name="WAN IPv6 address", icon="mdi:ip"), - "device_signal.band": dict(name="Band"), - "device_signal.cell_id": dict(name="Cell ID"), - "device_signal.lac": dict(name="LAC"), - "device_signal.mode": dict( + f"{KEY_DEVICE_INFORMATION}.SoftwareVersion": dict(name="Software version"), + f"{KEY_DEVICE_INFORMATION}.WanIPAddress": dict( + name="WAN IP address", icon="mdi:ip" + ), + f"{KEY_DEVICE_INFORMATION}.WanIPv6Address": dict( + name="WAN IPv6 address", icon="mdi:ip" + ), + f"{KEY_DEVICE_SIGNAL}.band": dict(name="Band"), + f"{KEY_DEVICE_SIGNAL}.cell_id": dict(name="Cell ID"), + f"{KEY_DEVICE_SIGNAL}.lac": dict(name="LAC"), + f"{KEY_DEVICE_SIGNAL}.mode": dict( name="Mode", formatter=lambda x: ({"0": "2G", "2": "3G", "7": "4G"}.get(x, "Unknown"), None), ), - "device_signal.pci": dict(name="PCI"), - "device_signal.rsrq": dict( + f"{KEY_DEVICE_SIGNAL}.pci": dict(name="PCI"), + f"{KEY_DEVICE_SIGNAL}.rsrq": dict( name="RSRQ", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrq.php @@ -54,7 +66,7 @@ SENSOR_META = { and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", ), - "device_signal.rsrp": dict( + f"{KEY_DEVICE_SIGNAL}.rsrp": dict( name="RSRP", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/rsrp.php @@ -66,7 +78,7 @@ SENSOR_META = { and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", ), - "device_signal.rssi": dict( + f"{KEY_DEVICE_SIGNAL}.rssi": dict( name="RSSI", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # https://eyesaas.com/wi-fi-signal-strength/ @@ -78,7 +90,7 @@ SENSOR_META = { and "mdi:signal-cellular-2" or "mdi:signal-cellular-3", ), - "device_signal.sinr": dict( + f"{KEY_DEVICE_SIGNAL}.sinr": dict( name="SINR", device_class=DEVICE_CLASS_SIGNAL_STRENGTH, # http://www.lte-anbieter.info/technik/sinr.php @@ -104,11 +116,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Huawei LTE sensor devices.""" - data = hass.data[DATA_KEY].get_data(config) + data = hass.data[DOMAIN].get_data(config) sensors = [] for path in config.get(CONF_MONITORED_CONDITIONS): if path == "traffic_statistics": # backwards compatibility - path = "monitoring_traffic_statistics" + path = KEY_MONITORING_TRAFFIC_STATISTICS data.subscribe(path) sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {}))) @@ -119,7 +131,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # *_d.e.v.i.c.e._.s.i.g.n.a.l...s.i.n.r entreg = await entity_registry.async_get_registry(hass) for entid, ent in entreg.entities.items(): - if ent.platform != "huawei_lte": + if ent.platform != DOMAIN: continue for sensor in sensors: oldsuf = ".".join(sensor.path) @@ -163,13 +175,13 @@ class HuaweiLteSensor(Entity): @property def unique_id(self) -> str: """Return unique ID for sensor.""" - return "{}-{}".format(self.data.mac, self.path) + return f"{self.data.mac}-{self.path}" @property def name(self) -> str: """Return sensor name.""" try: - dname = self.data["device_information.DeviceName"] + dname = self.data[f"{KEY_DEVICE_INFORMATION}.DeviceName"] except KeyError: dname = None vname = self.meta.get("name", self.path) diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py index 08b7c9ec859..b7b5731dfd3 100644 --- a/homeassistant/components/huawei_router/device_tracker.py +++ b/homeassistant/components/huawei_router/device_tracker.py @@ -119,12 +119,12 @@ class HuaweiDeviceScanner(DeviceScanner): def _get_devices_response(self): """Get the raw string with the devices from the router.""" - cnt = requests.post("http://{}/asp/GetRandCount.asp".format(self.host)) + cnt = requests.post(f"http://{self.host}/asp/GetRandCount.asp") cnt_str = str(cnt.content, cnt.apparent_encoding, errors="replace") _LOGGER.debug("Logging in") cookie = requests.post( - "http://{}/login.cgi".format(self.host), + f"http://{self.host}/login.cgi", data=[ ("UserName", self.username), ("PassWord", self.password), @@ -136,13 +136,13 @@ class HuaweiDeviceScanner(DeviceScanner): _LOGGER.debug("Requesting lan user info update") # this request is needed or else some devices' state won't be updated requests.get( - "http://{}/html/bbsp/common/lanuserinfo.asp".format(self.host), + f"http://{self.host}/html/bbsp/common/lanuserinfo.asp", cookies=cookie.cookies, ) _LOGGER.debug("Requesting lan user info data") devices = requests.get( - "http://{}/html/bbsp/common/GetLanUserDevInfo.asp".format(self.host), + f"http://{self.host}/html/bbsp/common/GetLanUserDevInfo.asp", cookies=cookie.cookies, ) diff --git a/homeassistant/components/hue/.translations/it.json b/homeassistant/components/hue/.translations/it.json index 72b2fd6445b..5dd64364c10 100644 --- a/homeassistant/components/hue/.translations/it.json +++ b/homeassistant/components/hue/.translations/it.json @@ -1,11 +1,13 @@ { "config": { "abort": { - "all_configured": "Tutti i bridge Philips Hue sono gi\u00e0 configurati", - "already_configured": "Il Bridge \u00e8 gi\u00e0 configurato", + "all_configured": "Tutti i bridge di Philips Hue sono gi\u00e0 configurati", + "already_configured": "Il bridge \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione per bridge \u00e8 gi\u00e0 in corso.", "cannot_connect": "Impossibile connettersi al bridge", "discover_timeout": "Impossibile trovare i bridge Hue", - "no_bridges": "Nessun bridge Hue di Philips trovato", + "no_bridges": "Nessun bridge di Philips Hue trovato", + "not_hue_bridge": "Non \u00e8 un bridge Hue", "unknown": "Si \u00e8 verificato un errore" }, "error": { @@ -24,6 +26,6 @@ "title": "Collega Hub" } }, - "title": "Philips Hue Bridge" + "title": "Philips Hue" } } \ No newline at end of file diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 0b0e3723b13..9c0e94bc3bd 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -160,7 +160,7 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): { "host": host, # This format is the legacy format that Hue used for discovery - "path": "phue-{}.conf".format(serial), + "path": f"phue-{serial}.conf", } ) diff --git a/homeassistant/components/hydroquebec/sensor.py b/homeassistant/components/hydroquebec/sensor.py index 79de222397b..c3ad79c1c98 100644 --- a/homeassistant/components/hydroquebec/sensor.py +++ b/homeassistant/components/hydroquebec/sensor.py @@ -28,9 +28,9 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) KILOWATT_HOUR = ENERGY_KILO_WATT_HOUR -PRICE = "CAD" # type: str -DAYS = "days" # type: str -CONF_CONTRACT = "contract" # type: str +PRICE = "CAD" +DAYS = "days" +CONF_CONTRACT = "contract" DEFAULT_NAME = "HydroQuebec" @@ -104,7 +104,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) HOST = "https://www.hydroquebec.com" -HOME_URL = "{}/portail/web/clientele/authentification".format(HOST) +HOME_URL = f"{HOST}/portail/web/clientele/authentification" PROFILE_URL = "{}/portail/fr/group/clientele/" "portrait-de-consommation".format(HOST) MONTHLY_MAP = ( ("period_total_bill", "montantFacturePeriode"), @@ -164,7 +164,7 @@ class HydroQuebecSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index e98e712bc6f..845c6b9021f 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -28,7 +28,7 @@ def no_application_protocol(value): """Validate that value is without the application protocol.""" protocol_separator = "://" if not value or protocol_separator in value: - raise vol.Invalid("Invalid host, {} is not allowed".format(protocol_separator)) + raise vol.Invalid(f"Invalid host, {protocol_separator} is not allowed") return value @@ -52,7 +52,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) - url = "http://{}".format(host) + url = f"http://{host}" ialarm = IAlarmPanel(name, code, username, password, url) add_entities([ialarm], True) diff --git a/homeassistant/components/iaqualink/.translations/ca.json b/homeassistant/components/iaqualink/.translations/ca.json new file mode 100644 index 00000000000..a5456c7b0cd --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s pots configurar una \u00fanica connexi\u00f3 d'iAqualink." + }, + "error": { + "connection_failure": "No s'ha pogut connectar amb iAqualink. Comprova el nom d'usuari i la contrasenya." + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari / Correu electr\u00f2nic" + }, + "description": "Introdueix el nom d'usuari i la contrasenya del teu compte d'iAqualink.", + "title": "Connexi\u00f3 amb iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/da.json b/homeassistant/components/iaqualink/.translations/da.json new file mode 100644 index 00000000000..a1e1c20cbc5 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en enkelt iAqualink-forbindelse." + }, + "error": { + "connection_failure": "Kan ikke oprette forbindelse til iAqualink. Kontroller dit brugernavn og din adgangskode." + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn / e-mail-adresse" + }, + "description": "Indtast brugernavn og adgangskode til din iAqualink-konto.", + "title": "Opret forbindelse til iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/en.json b/homeassistant/components/iaqualink/.translations/en.json new file mode 100644 index 00000000000..4972c3d3ff7 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "You can only configure a single iAqualink connection." + }, + "error": { + "connection_failure": "Unable to connect to iAqualink. Check your username and password." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username / Email Address" + }, + "description": "Please enter the username and password for your iAqualink account.", + "title": "Connect to iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/it.json b/homeassistant/components/iaqualink/.translations/it.json new file mode 100644 index 00000000000..73d840bdbd3 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare una sola connessione iAqualink." + }, + "error": { + "connection_failure": "Impossibile connettersi a iAqualink. Controllare il nome utente e la password." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome Utente / Indirizzo E-mail" + }, + "description": "Inserisci il nome utente e la password del tuo account iAqualink.", + "title": "Collegati a iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/ko.json b/homeassistant/components/iaqualink/.translations/ko.json new file mode 100644 index 00000000000..9b2519077e2 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 iAqualink \uc5f0\uacb0\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_failure": "iAqualink \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984 / \uc774\uba54\uc77c \uc8fc\uc18c" + }, + "description": "iAqualink \uacc4\uc815\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "iAqualink \uc5f0\uacb0" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/nl.json b/homeassistant/components/iaqualink/.translations/nl.json new file mode 100644 index 00000000000..c0a515bb741 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n iAqualink-verbinding configureren." + }, + "error": { + "connection_failure": "Kan geen verbinding maken met iAqualink. Controleer je gebruikersnaam en wachtwoord." + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam/E-mailadres" + }, + "description": "Voer de gebruikersnaam en het wachtwoord voor uw iAqualink-account in.", + "title": "Verbinding maken met iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/pl.json b/homeassistant/components/iaqualink/.translations/pl.json new file mode 100644 index 00000000000..211a65f5ccb --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno po\u0142\u0105czenie iAqualink." + }, + "error": { + "connection_failure": "Nie mo\u017cna po\u0142\u0105czy\u0107 z iAqualink. Sprawd\u017a nazw\u0119 u\u017cytkownika i has\u0142o." + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika / adres e-mail" + }, + "description": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o do konta iAqualink.", + "title": "Po\u0142\u0105cz z iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json new file mode 100644 index 00000000000..35444dd422b --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a iAqualink. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0412\u0430\u0448 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", + "title": "Jandy iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/zh-Hant.json b/homeassistant/components/iaqualink/.translations/zh-Hant.json new file mode 100644 index 00000000000..146088b4eff --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 iAqualink \u9023\u7dda\u3002" + }, + "error": { + "connection_failure": "\u7121\u6cd5\u9023\u7dda\u81f3 iAqualink\uff0c\u8acb\u78ba\u8a8d\u60a8\u7684\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31 / \u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8acb\u8f38\u5165 iAqualink \u5e33\u865f\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc\u3002", + "title": "\u9023\u7dda\u81f3 iAqualink" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py new file mode 100644 index 00000000000..56a39df64c9 --- /dev/null +++ b/homeassistant/components/iaqualink/__init__.py @@ -0,0 +1,192 @@ +"""Component to embed Aqualink devices.""" +import asyncio +from functools import wraps +import logging + +from aiohttp import CookieJar +import voluptuous as vol + +from iaqualink import ( + AqualinkClient, + AqualinkLight, + AqualinkLoginException, + AqualinkSensor, + AqualinkThermostat, + AqualinkToggle, +) + +from homeassistant import config_entries +from homeassistant.components.climate import DOMAIN as CLIMATE_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.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import DOMAIN, UPDATE_INTERVAL + + +_LOGGER = logging.getLogger(__name__) + +ATTR_CONFIG = "config" +PARALLEL_UPDATES = 0 + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> None: + """Set up the Aqualink component.""" + conf = config.get(DOMAIN) + + hass.data[DOMAIN] = {} + + if conf is not None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None: + """Set up Aqualink from a config entry.""" + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + + # These will contain the initialized devices + climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = [] + lights = hass.data[DOMAIN][LIGHT_DOMAIN] = [] + sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] + switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] + + session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + aqualink = AqualinkClient(username, password, session) + try: + await aqualink.login() + except AqualinkLoginException as login_exception: + _LOGGER.error("Exception raised while attempting to login: %s", login_exception) + return False + + systems = await aqualink.get_systems() + systems = list(systems.values()) + if not systems: + _LOGGER.error("No systems detected or supported") + return False + + # Only supporting the first system for now. + devices = await systems[0].get_devices() + + for dev in devices.values(): + if isinstance(dev, AqualinkThermostat): + climates += [dev] + elif isinstance(dev, AqualinkLight): + lights += [dev] + elif isinstance(dev, AqualinkSensor): + sensors += [dev] + elif isinstance(dev, AqualinkToggle): + switches += [dev] + + forward_setup = hass.config_entries.async_forward_entry_setup + if climates: + _LOGGER.debug("Got %s climates: %s", len(climates), climates) + hass.async_create_task(forward_setup(entry, CLIMATE_DOMAIN)) + if lights: + _LOGGER.debug("Got %s lights: %s", len(lights), lights) + hass.async_create_task(forward_setup(entry, LIGHT_DOMAIN)) + if sensors: + _LOGGER.debug("Got %s sensors: %s", len(sensors), sensors) + hass.async_create_task(forward_setup(entry, SENSOR_DOMAIN)) + if switches: + _LOGGER.debug("Got %s switches: %s", len(switches), switches) + hass.async_create_task(forward_setup(entry, SWITCH_DOMAIN)) + + async def _async_systems_update(now): + """Refresh internal state for all systems.""" + await systems[0].update() + async_dispatcher_send(hass, DOMAIN) + + async_track_time_interval(hass, _async_systems_update, UPDATE_INTERVAL) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + forward_unload = hass.config_entries.async_forward_entry_unload + + tasks = [] + + if hass.data[DOMAIN][CLIMATE_DOMAIN]: + tasks += [forward_unload(entry, CLIMATE_DOMAIN)] + if hass.data[DOMAIN][LIGHT_DOMAIN]: + tasks += [forward_unload(entry, LIGHT_DOMAIN)] + if hass.data[DOMAIN][SENSOR_DOMAIN]: + tasks += [forward_unload(entry, SENSOR_DOMAIN)] + if hass.data[DOMAIN][SWITCH_DOMAIN]: + tasks += [forward_unload(entry, SWITCH_DOMAIN)] + + hass.data[DOMAIN].clear() + + return all(await asyncio.gather(*tasks)) + + +def refresh_system(func): + """Force update all entities after state change.""" + + @wraps(func) + async def wrapper(self, *args, **kwargs): + """Call decorated function and send update signal to all entities.""" + await func(self, *args, **kwargs) + async_dispatcher_send(self.hass, DOMAIN) + + return wrapper + + +class AqualinkEntity(Entity): + """Abstract class for all Aqualink platforms. + + Entity state is updated via the interval timer within the integration. + Any entity state change via the iaqualink library triggers an internal + state refresh which is then propagated to all the entities in the system + via the refresh_system decorator above to the _update_callback in this + class. + """ + + async def async_added_to_hass(self) -> None: + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._update_callback) + + @callback + def _update_callback(self) -> None: + self.async_schedule_update_ha_state() + + @property + def should_poll(self) -> bool: + """Return False as entities shouldn't be polled. + + Entities are checked periodically as the integration runs periodic + updates on a timer. + """ + return False diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py new file mode 100644 index 00000000000..321c54329a2 --- /dev/null +++ b/homeassistant/components/iaqualink/climate.py @@ -0,0 +1,142 @@ +"""Support for Aqualink Thermostats.""" +import logging +from typing import List, Optional + +from iaqualink import ( + AqualinkState, + AqualinkHeater, + AqualinkPump, + AqualinkSensor, + AqualinkThermostat, +) +from iaqualink.const import ( + AQUALINK_TEMP_CELSIUS_HIGH, + AQUALINK_TEMP_CELSIUS_LOW, + AQUALINK_TEMP_FAHRENHEIT_HIGH, + AQUALINK_TEMP_FAHRENHEIT_LOW, +) + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + DOMAIN, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity, refresh_system +from .const import DOMAIN as AQUALINK_DOMAIN, CLIMATE_SUPPORTED_MODES + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered switches.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkThermostat(dev)) + async_add_entities(devs, True) + + +class HassAqualinkThermostat(ClimateDevice, AqualinkEntity): + """Representation of a thermostat.""" + + def __init__(self, dev: AqualinkThermostat): + """Initialize the thermostat.""" + self.dev = dev + + @property + def name(self) -> str: + """Return the name of the thermostat.""" + return self.dev.label.split(" ")[0] + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def hvac_modes(self) -> List[str]: + """Return the list of supported HVAC modes.""" + return CLIMATE_SUPPORTED_MODES + + @property + def pump(self) -> AqualinkPump: + """Return the pump device for the current thermostat.""" + pump = f"{self.name.lower()}_pump" + return self.dev.system.devices[pump] + + @property + def hvac_mode(self) -> str: + """Return the current HVAC mode.""" + state = AqualinkState(self.heater.state) + if state == AqualinkState.ON: + return HVAC_MODE_HEAT + return HVAC_MODE_OFF + + @refresh_system + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Turn the underlying heater switch on or off.""" + if hvac_mode == HVAC_MODE_HEAT: + await self.heater.turn_on() + elif hvac_mode == HVAC_MODE_OFF: + await self.heater.turn_off() + else: + _LOGGER.warning("Unknown operation mode: %s", hvac_mode) + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + if self.dev.system.temp_unit == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def min_temp(self) -> int: + """Return the minimum temperature supported by the thermostat.""" + if self.temperature_unit == TEMP_FAHRENHEIT: + return AQUALINK_TEMP_FAHRENHEIT_LOW + return AQUALINK_TEMP_CELSIUS_LOW + + @property + def max_temp(self) -> int: + """Return the minimum temperature supported by the thermostat.""" + if self.temperature_unit == TEMP_FAHRENHEIT: + return AQUALINK_TEMP_FAHRENHEIT_HIGH + return AQUALINK_TEMP_CELSIUS_HIGH + + @property + def target_temperature(self) -> float: + """Return the current target temperature.""" + return float(self.dev.state) + + @refresh_system + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + await self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE])) + + @property + def sensor(self) -> AqualinkSensor: + """Return the sensor device for the current thermostat.""" + sensor = f"{self.name.lower()}_temp" + return self.dev.system.devices[sensor] + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + if self.sensor.state != "": + return float(self.sensor.state) + return None + + @property + def heater(self) -> AqualinkHeater: + """Return the heater device for the current thermostat.""" + heater = f"{self.name.lower()}_heater" + return self.dev.system.devices[heater] diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py new file mode 100644 index 00000000000..ec83477d253 --- /dev/null +++ b/homeassistant/components/iaqualink/config_flow.py @@ -0,0 +1,52 @@ +"""Config flow to configure zone component.""" +from typing import Optional + +import voluptuous as vol + +from iaqualink import AqualinkClient, AqualinkLoginException + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import ConfigType + +from .const import DOMAIN + + +@config_entries.HANDLERS.register(DOMAIN) +class AqualinkFlowHandler(config_entries.ConfigFlow): + """Aqualink config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input: Optional[ConfigType] = None): + """Handle a flow start.""" + # Supporting a single account. + entries = self.hass.config_entries.async_entries(DOMAIN) + if entries: + return self.async_abort(reason="already_setup") + + errors = {} + + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + try: + aqualink = AqualinkClient(username, password) + await aqualink.login() + return self.async_create_entry(title=username, data=user_input) + except AqualinkLoginException: + errors["base"] = "connection_failure" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} + ), + errors=errors, + ) + + async def async_step_import(self, user_input: Optional[ConfigType] = None): + """Occurs when an entry is setup through config.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/iaqualink/const.py b/homeassistant/components/iaqualink/const.py new file mode 100644 index 00000000000..219eb912994 --- /dev/null +++ b/homeassistant/components/iaqualink/const.py @@ -0,0 +1,8 @@ +"""Constants for the the iaqualink component.""" +from datetime import timedelta + +from homeassistant.components.climate.const import HVAC_MODE_HEAT, HVAC_MODE_OFF + +DOMAIN = "iaqualink" +CLIMATE_SUPPORTED_MODES = [HVAC_MODE_HEAT, HVAC_MODE_OFF] +UPDATE_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py new file mode 100644 index 00000000000..fbfb10783ee --- /dev/null +++ b/homeassistant/components/iaqualink/light.py @@ -0,0 +1,105 @@ +"""Support for Aqualink pool lights.""" +import logging + +from iaqualink import AqualinkLight, AqualinkLightEffect + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_EFFECT, + DOMAIN, + SUPPORT_BRIGHTNESS, + SUPPORT_EFFECT, + Light, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity, refresh_system +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered lights.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkLight(dev)) + async_add_entities(devs, True) + + +class HassAqualinkLight(Light, AqualinkEntity): + """Representation of a light.""" + + def __init__(self, dev: AqualinkLight): + """Initialize the light.""" + self.dev = dev + + @property + def name(self) -> str: + """Return the name of the light.""" + return self.dev.label + + @property + def is_on(self) -> bool: + """Return whether the light is on or off.""" + return self.dev.is_on + + @refresh_system + async def async_turn_on(self, **kwargs) -> None: + """Turn on the light. + + This handles brightness and light effects for lights that do support + them. + """ + brightness = kwargs.get(ATTR_BRIGHTNESS) + effect = kwargs.get(ATTR_EFFECT) + + # For now I'm assuming lights support either effects or brightness. + if effect: + effect = AqualinkLightEffect[effect].value + await self.dev.set_effect(effect) + elif brightness: + # Aqualink supports percentages in 25% increments. + pct = int(round(brightness * 4.0 / 255)) * 25 + await self.dev.set_brightness(pct) + else: + await self.dev.turn_on() + + @refresh_system + async def async_turn_off(self, **kwargs) -> None: + """Turn off the light.""" + await self.dev.turn_off() + + @property + def brightness(self) -> int: + """Return current brightness of the light. + + The scale needs converting between 0-100 and 0-255. + """ + return self.dev.brightness * 255 / 100 + + @property + def effect(self) -> str: + """Return the current light effect if supported.""" + return AqualinkLightEffect(self.dev.effect).name + + @property + def effect_list(self) -> list: + """Return supported light effects.""" + return list(AqualinkLightEffect.__members__) + + @property + def supported_features(self) -> int: + """Return the list of features supported by the light.""" + if self.dev.is_dimmer: + return SUPPORT_BRIGHTNESS + + if self.dev.is_color: + return SUPPORT_EFFECT + + return 0 diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json new file mode 100644 index 00000000000..25e02536897 --- /dev/null +++ b/homeassistant/components/iaqualink/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "iaqualink", + "name": "Jandy iAqualink", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/iaqualink/", + "dependencies": [], + "codeowners": [ + "@flz" + ], + "requirements": [ + "iaqualink==0.2.9" + ] +} diff --git a/homeassistant/components/iaqualink/sensor.py b/homeassistant/components/iaqualink/sensor.py new file mode 100644 index 00000000000..4a1691e0314 --- /dev/null +++ b/homeassistant/components/iaqualink/sensor.py @@ -0,0 +1,59 @@ +"""Support for Aqualink temperature sensors.""" +import logging +from typing import Optional + +from iaqualink import AqualinkSensor + +from homeassistant.components.sensor import DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered sensors.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkSensor(dev)) + async_add_entities(devs, True) + + +class HassAqualinkSensor(AqualinkEntity): + """Representation of a sensor.""" + + def __init__(self, dev: AqualinkSensor): + """Initialize the sensor.""" + self.dev = dev + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self.dev.label + + @property + def unit_of_measurement(self) -> str: + """Return the measurement unit for the sensor.""" + if self.dev.system.temp_unit == "F": + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def state(self) -> str: + """Return the state of the sensor.""" + return int(self.dev.state) if self.dev.state != "" else None + + @property + def device_class(self) -> Optional[str]: + """Return the class of the sensor.""" + if self.dev.name.endswith("_temp"): + return DEVICE_CLASS_TEMPERATURE + return None diff --git a/homeassistant/components/iaqualink/strings.json b/homeassistant/components/iaqualink/strings.json new file mode 100644 index 00000000000..4c706522198 --- /dev/null +++ b/homeassistant/components/iaqualink/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Jandy iAqualink", + "step": { + "user": { + "title": "Connect to iAqualink", + "description": "Please enter the username and password for your iAqualink account.", + "data": { + "username": "Username / Email Address", + "password": "Password" + } + } + }, + "error": { + "connection_failure": "Unable to connect to iAqualink. Check your username and password." + }, + "abort": { + "already_setup": "You can only configure a single iAqualink connection." + } + } +} diff --git a/homeassistant/components/iaqualink/switch.py b/homeassistant/components/iaqualink/switch.py new file mode 100644 index 00000000000..f2fc51ce713 --- /dev/null +++ b/homeassistant/components/iaqualink/switch.py @@ -0,0 +1,65 @@ +"""Support for Aqualink pool feature switches.""" +import logging + +from iaqualink import AqualinkToggle + +from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from . import AqualinkEntity, refresh_system +from .const import DOMAIN as AQUALINK_DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities +) -> None: + """Set up discovered switches.""" + devs = [] + for dev in hass.data[AQUALINK_DOMAIN][DOMAIN]: + devs.append(HassAqualinkSwitch(dev)) + async_add_entities(devs, True) + + +class HassAqualinkSwitch(SwitchDevice, AqualinkEntity): + """Representation of a switch.""" + + def __init__(self, dev: AqualinkToggle): + """Initialize the switch.""" + self.dev = dev + + @property + def name(self) -> str: + """Return the name of the switch.""" + return self.dev.label + + @property + def icon(self) -> str: + """Return an icon based on the switch type.""" + if self.name == "Cleaner": + return "mdi:robot-vacuum" + if self.name == "Waterfall" or self.name.endswith("Dscnt"): + return "mdi:fountain" + if self.name.endswith("Pump") or self.name.endswith("Blower"): + return "mdi:fan" + if self.name.endswith("Heater"): + return "mdi:radiator" + + @property + def is_on(self) -> bool: + """Return whether the switch is on or not.""" + return self.dev.is_on + + @refresh_system + async def async_turn_on(self, **kwargs) -> None: + """Turn on the switch.""" + await self.dev.turn_on() + + @refresh_system + async def async_turn_off(self, **kwargs) -> None: + """Turn off the switch.""" + await self.dev.turn_off() diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index dbd4f54bac6..2ecf904314f 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -281,10 +281,10 @@ class Icloud(DeviceScanner): devicename = device.get( "deviceName", "SMS to %s" % device.get("phoneNumber") ) - devicesstring += "{}: {};".format(i, devicename) + devicesstring += f"{i}: {devicename};" _CONFIGURING[self.accountname] = configurator.request_config( - "iCloud {}".format(self.accountname), + f"iCloud {self.accountname}", self.icloud_trusted_device_callback, description=( "Please choose your trusted device by entering" @@ -327,7 +327,7 @@ class Icloud(DeviceScanner): return _CONFIGURING[self.accountname] = configurator.request_config( - "iCloud {}".format(self.accountname), + f"iCloud {self.accountname}", self.icloud_verification_callback, description=("Please enter the validation code:"), entity_picture="/static/images/config_icloud.png", @@ -528,7 +528,7 @@ class Icloud(DeviceScanner): """Set the interval of the given devices.""" devs = [devicename] if devicename else self.devices for device in devs: - devid = "{}.{}".format(DOMAIN, device) + devid = f"{DOMAIN}.{device}" devicestate = self.hass.states.get(devid) if interval is not None: if devicestate is not None: diff --git a/homeassistant/components/ifttt/.translations/it.json b/homeassistant/components/ifttt/.translations/it.json index e5dc76b7923..d6faf60d618 100644 --- a/homeassistant/components/ifttt/.translations/it.json +++ b/homeassistant/components/ifttt/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Home Assistant deve essere accessibile da internet per ricevere messaggi IFTTT", + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi IFTTT.", "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, "create_entry": { diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index abba8749663..057d832b4fa 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -201,6 +201,11 @@ class IgnSismologiaLocationEvent(GeolocationEvent): self._publication_date = feed_entry.published self._image_url = feed_entry.image_url + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:pulse" + @property def source(self) -> str: """Return source value of this external event.""" @@ -210,9 +215,9 @@ class IgnSismologiaLocationEvent(GeolocationEvent): def name(self) -> Optional[str]: """Return the name of the entity.""" if self._magnitude and self._region: - return "M {:.1f} - {}".format(self._magnitude, self._region) + return f"M {self._magnitude:.1f} - {self._region}" if self._magnitude: - return "M {:.1f}".format(self._magnitude) + return f"M {self._magnitude:.1f}" if self._region: return self._region return self._title diff --git a/homeassistant/components/ihc/__init__.py b/homeassistant/components/ihc/__init__.py index 3be40058fec..a55b94eb26a 100644 --- a/homeassistant/components/ihc/__init__.py +++ b/homeassistant/components/ihc/__init__.py @@ -61,7 +61,7 @@ def validate_name(config): if CONF_NAME in config: return config ihcid = config[CONF_ID] - name = "ihc_{}".format(ihcid) + name = f"ihc_{ihcid}" config[CONF_NAME] = name return config @@ -312,7 +312,7 @@ def get_discovery_info(component_setup, groups, controller_id): if "setting" in node.attrib and node.attrib["setting"] == "yes": continue ihc_id = int(node.attrib["id"].strip("_"), 0) - name = "{}_{}".format(groupname, ihc_id) + name = f"{groupname}_{ihc_id}" device = { "ihc_id": ihc_id, "ctrl_id": controller_id, diff --git a/homeassistant/components/ihc/ihc_auto_setup.yaml b/homeassistant/components/ihc/ihc_auto_setup.yaml index 81d5bf37977..0495ed58458 100644 --- a/homeassistant/components/ihc/ihc_auto_setup.yaml +++ b/homeassistant/components/ihc/ihc_auto_setup.yaml @@ -1,6 +1,6 @@ # IHC auto setup configuration. # To customize this, copy this file to the home assistant configuration -# folder and make your changes. +# folder and make your changes. binary_sensor: # Magnet contact diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index b1bab09fdcb..c5171cde646 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -118,7 +118,7 @@ class EmailReader: if not self._unread_ids: search = "SINCE {0:%d-%b-%Y}".format(datetime.date.today()) if self._last_id is not None: - search = "UID {}:*".format(self._last_id) + search = f"UID {self._last_id}:*" _, data = self.connection.uid("search", None, search) self._unread_ids = deque(data[0].split()) diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 0d13caca3b7..cccb9d25644 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -30,7 +30,7 @@ class InComfortClimate(ClimateDevice): """Initialize the climate device.""" self._client = client self._room = room - self._name = "Room {}".format(room.room_no) + self._name = f"Room {room.room_no}" async def async_added_to_hass(self) -> None: """Set up a listener when this entity is added to HA.""" diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 7eeb618c874..2449a1223cc 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -96,7 +96,7 @@ class IncomfortWaterHeater(WaterHeaterDevice): def current_operation(self): """Return the current operation mode.""" if self._heater.is_failed: - return "Fault code: {}".format(self._heater.fault_code) + return f"Fault code: {self._heater.fault_code}" return self._heater.display_text diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index e8d2cc54bf1..2bb5207aa85 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -247,7 +247,7 @@ def setup(hass, config): try: json["fields"][key] = float(value) except (ValueError, TypeError): - new_key = "{}_str".format(key) + new_key = f"{key}_str" new_value = str(value) json["fields"][new_key] = new_value diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 2564b8b31b4..007ed6517ef 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -49,13 +49,11 @@ def _cv_input_number(cfg): maximum = cfg.get(CONF_MAX) if minimum >= maximum: raise vol.Invalid( - "Maximum ({}) is not greater than minimum ({})".format(minimum, maximum) + f"Maximum ({minimum}) is not greater than minimum ({maximum})" ) state = cfg.get(CONF_INITIAL) if state is not None and (state < minimum or state > maximum): - raise vol.Invalid( - "Initial value {} not in range {}-{}".format(state, minimum, maximum) - ) + raise vol.Invalid(f"Initial value {state} not in range {minimum}-{maximum}") return cfg diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 2b7c7312f71..41d78e6e7c5 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -45,12 +45,12 @@ def _cv_input_text(cfg): maximum = cfg.get(CONF_MAX) if minimum > maximum: raise vol.Invalid( - "Max len ({}) is not greater than min len ({})".format(minimum, maximum) + f"Max len ({minimum}) is not greater than min len ({maximum})" ) state = cfg.get(CONF_INITIAL) if state is not None and (len(state) < minimum or len(state) > maximum): raise vol.Invalid( - "Initial value {} length not in range {}-{}".format(state, minimum, maximum) + f"Initial value {state} length not in range {minimum}-{maximum}" ) return cfg @@ -58,20 +58,23 @@ def _cv_input_text(cfg): CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( - vol.All( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN, default=0): vol.Coerce(int), - vol.Optional(CONF_MAX, default=100): vol.Coerce(int), - vol.Optional(CONF_INITIAL, ""): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(ATTR_PATTERN): cv.string, - vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In( - [MODE_TEXT, MODE_PASSWORD] - ), - }, - _cv_input_text, + vol.Any( + vol.All( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MIN, default=0): vol.Coerce(int), + vol.Optional(CONF_MAX, default=100): vol.Coerce(int), + vol.Optional(CONF_INITIAL, ""): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(ATTR_PATTERN): cv.string, + vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In( + [MODE_TEXT, MODE_PASSWORD] + ), + }, + _cv_input_text, + ), + None, ) ) }, @@ -87,6 +90,8 @@ async def async_setup(hass, config): entities = [] for object_id, cfg in config[DOMAIN].items(): + if cfg is None: + cfg = {} name = cfg.get(CONF_NAME) minimum = cfg.get(CONF_MIN) maximum = cfg.get(CONF_MAX) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 2dda073aa18..4015d472ce8 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -314,7 +314,7 @@ async def async_setup(hass, config): def _send_load_aldb_signal(entity_id, reload): """Send the load All-Link database signal to INSTEON entity.""" - signal = "{}_{}".format(entity_id, SIGNAL_LOAD_ALDB) + signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" dispatcher_send(hass, signal, reload) def print_aldb(service): @@ -322,7 +322,7 @@ async def async_setup(hass, config): # For now this sends logs to the log file. # Furture direction is to create an INSTEON control panel. entity_id = service.data[CONF_ENTITY_ID] - signal = "{}_{}".format(entity_id, SIGNAL_PRINT_ALDB) + signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" dispatcher_send(hass, signal) def print_im_aldb(service): @@ -652,9 +652,9 @@ class InsteonEntity(Entity): ) self._insteon_device_state.register_updates(self.async_entity_update) self.hass.data[DOMAIN][INSTEON_ENTITIES][self.entity_id] = self - load_signal = "{}_{}".format(self.entity_id, SIGNAL_LOAD_ALDB) + load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" async_dispatcher_connect(self.hass, load_signal, self._load_aldb) - print_signal = "{}_{}".format(self.entity_id, SIGNAL_PRINT_ALDB) + print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" async_dispatcher_connect(self.hass, print_signal, self._print_aldb) def _load_aldb(self, reload=False): @@ -679,7 +679,7 @@ class InsteonEntity(Entity): if self._insteon_device_state.name in STATE_NAME_LABEL_MAP: label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name] else: - label = "Group {:d}".format(self.group) + label = f"Group {self.group:d}" return label diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index d24b70c4be0..236a996794a 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -94,7 +94,7 @@ class IntegrationSensor(RestoreEntity): self._state = 0 self._method = integration_method - self._name = name if name is not None else "{} integral".format(source_entity) + self._name = name if name is not None else f"{source_entity} integral" if unit_of_measurement is None: self._unit_template = "{}{}{}".format( diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index 443a4cbc854..75a0c0e8f97 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -55,7 +55,7 @@ async def async_setup(hass, config): for intent_type, conf in intents.items(): if CONF_ACTION in conf: conf[CONF_ACTION] = script.Script( - hass, conf[CONF_ACTION], "Intent Script {}".format(intent_type) + hass, conf[CONF_ACTION], f"Intent Script {intent_type}" ) intent.async_register(hass, ScriptIntentHandler(intent_type, conf)) diff --git a/homeassistant/components/ios/sensor.py b/homeassistant/components/ios/sensor.py index 4da0d148f9c..47c54c3face 100644 --- a/homeassistant/components/ios/sensor.py +++ b/homeassistant/components/ios/sensor.py @@ -67,7 +67,7 @@ class IOSSensor(Entity): def unique_id(self): """Return the unique ID of this sensor.""" device_id = self._device[ios.ATTR_DEVICE_ID] - return "{}_{}".format(self.type, device_id) + return f"{self.type}_{device_id}" @property def unit_of_measurement(self): @@ -100,11 +100,11 @@ class IOSSensor(Entity): ios.ATTR_BATTERY_STATE_UNPLUGGED, ): charging = False - icon_state = "{}-off".format(DEFAULT_ICON_STATE) + icon_state = f"{DEFAULT_ICON_STATE}-off" elif battery_state == ios.ATTR_BATTERY_STATE_UNKNOWN: battery_level = None charging = False - icon_state = "{}-unknown".format(DEFAULT_ICON_LEVEL) + icon_state = f"{DEFAULT_ICON_LEVEL}-unknown" if self.type == "state": return icon_state diff --git a/homeassistant/components/iota/sensor.py b/homeassistant/components/iota/sensor.py index a34d6ed0214..8a0b17aa63b 100644 --- a/homeassistant/components/iota/sensor.py +++ b/homeassistant/components/iota/sensor.py @@ -46,7 +46,7 @@ class IotaBalanceSensor(IotaDevice): @property def name(self): """Return the name of the sensor.""" - return "{} Balance".format(self._name) + return f"{self._name} Balance" @property def state(self): diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index 1a68eccb312..eda601b09de 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval DOMAIN = "iperf3" -DATA_UPDATED = "{}_data_updated".format(DOMAIN) +DATA_UPDATED = f"{DOMAIN}_data_updated" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index fdaf5904aa1..9f1836c7389 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -132,7 +132,7 @@ class IPMAWeather(WeatherEntity): @property def unique_id(self) -> str: """Return a unique id.""" - return "{}, {}".format(self._station.latitude, self._station.longitude) + return f"{self._station.latitude}, {self._station.longitude}" @property def attribution(self): diff --git a/homeassistant/components/iqvia/.translations/it.json b/homeassistant/components/iqvia/.translations/it.json index 37079cf571d..492654c660c 100644 --- a/homeassistant/components/iqvia/.translations/it.json +++ b/homeassistant/components/iqvia/.translations/it.json @@ -9,6 +9,7 @@ "data": { "zip_code": "CAP" }, + "description": "Compila il tuo CAP americano o canadese.", "title": "IQVIA" } }, diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index b6930e1070f..e3add21c3a4 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -234,7 +234,7 @@ class IQVIAEntity(Entity): @property def unique_id(self): """Return a unique, HASS-friendly identifier for this entity.""" - return "{0}_{1}".format(self._zip_code, self._type) + return f"{self._zip_code}_{self._type}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 357bfca607a..7392c931f48 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/iqvia", "requirements": [ - "numpy==1.17.0", + "numpy==1.17.1", "pyiqvia==0.2.1" ], "dependencies": [], "codeowners": [ "@bachya" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index f2fd1143b6a..90aa89f06d1 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -174,9 +174,9 @@ class IndexSensor(IQVIAEntity): index = idx + 1 self._attrs.update( { - "{0}_{1}".format(ATTR_ALLERGEN_GENUS, index): attrs["Genus"], - "{0}_{1}".format(ATTR_ALLERGEN_NAME, index): attrs["Name"], - "{0}_{1}".format(ATTR_ALLERGEN_TYPE, index): attrs["PlantType"], + f"{ATTR_ALLERGEN_GENUS}_{index}": attrs["Genus"], + f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"], + f"{ATTR_ALLERGEN_TYPE}_{index}": attrs["PlantType"], } ) elif self._type in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): @@ -184,8 +184,8 @@ class IndexSensor(IQVIAEntity): index = idx + 1 self._attrs.update( { - "{0}_{1}".format(ATTR_ALLERGEN_NAME, index): attrs["Name"], - "{0}_{1}".format(ATTR_ALLERGEN_AMOUNT, index): attrs["PPM"], + f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"], + f"{ATTR_ALLERGEN_AMOUNT}_{index}": attrs["PPM"], } ) elif self._type == TYPE_DISEASE_TODAY: diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 9ad0f6beef1..727ec91dc37 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -138,7 +138,7 @@ NODE_FILTERS = { "Siren", "Siren_ADV", ], - "insteon_type": ["2.", "9.10.", "9.11."], + "insteon_type": ["2.", "9.10.", "9.11.", "113."], }, } @@ -182,6 +182,7 @@ def _check_for_node_def(hass: HomeAssistant, node, single_domain: str = None) -> hass.data[ISY994_NODES][domain].append(node) return True + _LOGGER.warning("Unsupported node: %s, type: %s", node.name, node.type) return False @@ -343,7 +344,7 @@ def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: """Categorize the ISY994 programs.""" for domain in SUPPORTED_PROGRAM_DOMAINS: try: - folder = programs[KEY_MY_PROGRAMS]["HA.{}".format(domain)] + folder = programs[KEY_MY_PROGRAMS][f"HA.{domain}"] except KeyError: pass else: @@ -378,10 +379,10 @@ def _categorize_weather(hass: HomeAssistant, climate) -> None: WeatherNode( getattr(climate, attr), attr.replace("_", " "), - getattr(climate, "{}_units".format(attr)), + getattr(climate, f"{attr}_units"), ) for attr in climate_attrs - if "{}_units".format(attr) in climate_attrs + if f"{attr}_units" in climate_attrs ] hass.data[ISY994_WEATHER].extend(weather_nodes) @@ -458,7 +459,7 @@ class ISYDevice(Entity): """Representation of an ISY994 device.""" _attrs = {} - _name = None # type: str + _name: str = None def __init__(self, node) -> None: """Initialize the insteon device.""" diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index a382c2f0830..a9746b004d0 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -272,7 +272,7 @@ class ISYSensorDevice(ISYDevice): int_prec = int(self._node.prec) decimal_part = str_val[-int_prec:] whole_part = str_val[: len(str_val) - int_prec] - val = float("{}.{}".format(whole_part, decimal_part)) + val = float(f"{whole_part}.{decimal_part}") raw_units = self.raw_unit_of_measurement if raw_units in (TEMP_CELSIUS, TEMP_FAHRENHEIT): val = self.hass.config.units.temperature(val, raw_units) diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index db646338f40..9895b54a50d 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -78,7 +78,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): cmddata = cmd[CONF_DATA].strip() if not cmddata: cmddata = '""' - cmddatas += "{}\n{}\n".format(cmdname, cmddata) + cmddatas += f"{cmdname}\n{cmddata}\n" itachip2ir.addDevice(name, modaddr, connaddr, cmddatas) devices.append(ITachIP2IRRemote(itachip2ir, name)) add_entities(devices, True) diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index df8ae7bd556..aebe16ffa26 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -84,13 +84,13 @@ class Itunes: uri_scheme = "http://" if self.port: - return "{}{}:{}".format(uri_scheme, self.host, self.port) + return f"{uri_scheme}{self.host}:{self.port}" - return "{}{}".format(uri_scheme, self.host) + return f"{uri_scheme}{self.host}" def _request(self, method, path, params=None): """Make the actual request and return the parsed response.""" - url = "{}{}".format(self._base_url, path) + url = f"{self._base_url}{path}" try: if method == "GET": diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 93a60e363e1..c7bbbdb2d90 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1 +1,109 @@ """The jewish_calendar component.""" +import logging + +import voluptuous as vol +import hdate + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.helpers.discovery import async_load_platform +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "jewish_calendar" + +SENSOR_TYPES = { + "binary": { + "issur_melacha_in_effect": ["Issur Melacha in Effect", "mdi:power-plug-off"] + }, + "data": { + "date": ["Date", "mdi:judaism"], + "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"], + "holiday_name": ["Holiday name", "mdi:calendar-star"], + "holiday_type": ["Holiday type", "mdi:counter"], + "omer_count": ["Day of the Omer", "mdi:counter"], + }, + "time": { + "first_light": ["Alot Hashachar", "mdi:weather-sunset-up"], + "gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"], + "mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"], + "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"], + "first_stars": ["T'set Hakochavim", "mdi:weather-night"], + "upcoming_shabbat_candle_lighting": [ + "Upcoming Shabbat Candle Lighting", + "mdi:candle", + ], + "upcoming_shabbat_havdalah": ["Upcoming Shabbat Havdalah", "mdi:weather-night"], + "upcoming_candle_lighting": ["Upcoming Candle Lighting", "mdi:candle"], + "upcoming_havdalah": ["Upcoming Havdalah", "mdi:weather-night"], + }, +} + +CONF_DIASPORA = "diaspora" +CONF_LANGUAGE = "language" +CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset" +CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset" + +CANDLE_LIGHT_DEFAULT = 18 + +DEFAULT_NAME = "Jewish Calendar" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DIASPORA, default=False): cv.boolean, + vol.Inclusive(CONF_LATITUDE, "coordinates"): cv.latitude, + vol.Inclusive(CONF_LONGITUDE, "coordinates"): cv.longitude, + vol.Optional(CONF_LANGUAGE, default="english"): vol.In( + ["hebrew", "english"] + ), + vol.Optional( + CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT + ): int, + # Default of 0 means use 8.5 degrees / 'three_stars' time. + vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Jewish Calendar component.""" + name = config[DOMAIN][CONF_NAME] + language = config[DOMAIN][CONF_LANGUAGE] + + latitude = config[DOMAIN].get(CONF_LATITUDE, hass.config.latitude) + longitude = config[DOMAIN].get(CONF_LONGITUDE, hass.config.longitude) + diaspora = config[DOMAIN][CONF_DIASPORA] + + candle_lighting_offset = config[DOMAIN][CONF_CANDLE_LIGHT_MINUTES] + havdalah_offset = config[DOMAIN][CONF_HAVDALAH_OFFSET_MINUTES] + + location = hdate.Location( + latitude=latitude, + longitude=longitude, + timezone=hass.config.time_zone, + diaspora=diaspora, + ) + + hass.data[DOMAIN] = { + "location": location, + "name": name, + "language": language, + "candle_lighting_offset": candle_lighting_offset, + "havdalah_offset": havdalah_offset, + "diaspora": diaspora, + } + + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) + + hass.async_create_task( + async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) + ) + + return True diff --git a/homeassistant/components/jewish_calendar/binary_sensor.py b/homeassistant/components/jewish_calendar/binary_sensor.py new file mode 100644 index 00000000000..7362fce3cd0 --- /dev/null +++ b/homeassistant/components/jewish_calendar/binary_sensor.py @@ -0,0 +1,66 @@ +"""Support for Jewish Calendar binary sensors.""" +import logging + +import hdate + +from homeassistant.components.binary_sensor import BinarySensorDevice +import homeassistant.util.dt as dt_util + +from . import DOMAIN, SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Jewish Calendar binary sensor devices.""" + if discovery_info is None: + return + + async_add_entities( + [ + JewishCalendarBinarySensor(hass.data[DOMAIN], sensor, sensor_info) + for sensor, sensor_info in SENSOR_TYPES["binary"].items() + ] + ) + + +class JewishCalendarBinarySensor(BinarySensorDevice): + """Representation of an Jewish Calendar binary sensor.""" + + def __init__(self, data, sensor, sensor_info): + """Initialize the binary sensor.""" + self._location = data["location"] + self._type = sensor + self._name = f"{data['name']} {sensor_info[0]}" + self._icon = sensor_info[1] + self._hebrew = data["language"] == "hebrew" + self._candle_lighting_offset = data["candle_lighting_offset"] + self._havdalah_offset = data["havdalah_offset"] + self._state = False + + @property + def icon(self): + """Return the icon of the entity.""" + return self._icon + + @property + def name(self): + """Return the name of the entity.""" + return self._name + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state + + async def async_update(self): + """Update the state of the sensor.""" + zmanim = hdate.Zmanim( + date=dt_util.now(), + location=self._location, + candle_lighting_offset=self._candle_lighting_offset, + havdalah_offset=self._havdalah_offset, + hebrew=self._hebrew, + ) + + self._state = zmanim.issur_melacha_in_effect diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 7e119494a20..405838b1fb1 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -1,140 +1,59 @@ """Platform to retrieve Jewish calendar information for Home Assistant.""" import logging -import voluptuous as vol +import hdate -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_NAME, - SUN_EVENT_SUNSET, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import SUN_EVENT_SUNSET from homeassistant.helpers.entity import Entity from homeassistant.helpers.sun import get_astral_event_date import homeassistant.util.dt as dt_util +from . import DOMAIN, SENSOR_TYPES + _LOGGER = logging.getLogger(__name__) -SENSOR_TYPES = { - "date": ["Date", "mdi:judaism"], - "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"], - "holiday_name": ["Holiday", "mdi:calendar-star"], - "holyness": ["Holyness", "mdi:counter"], - "first_light": ["Alot Hashachar", "mdi:weather-sunset-up"], - "gra_end_shma": ['Latest time for Shm"a GR"A', "mdi:calendar-clock"], - "mga_end_shma": ['Latest time for Shm"a MG"A', "mdi:calendar-clock"], - "plag_mincha": ["Plag Hamincha", "mdi:weather-sunset-down"], - "first_stars": ["T'set Hakochavim", "mdi:weather-night"], - "upcoming_shabbat_candle_lighting": [ - "Upcoming Shabbat Candle Lighting", - "mdi:candle", - ], - "upcoming_shabbat_havdalah": ["Upcoming Shabbat Havdalah", "mdi:weather-night"], - "upcoming_candle_lighting": ["Upcoming Candle Lighting", "mdi:candle"], - "upcoming_havdalah": ["Upcoming Havdalah", "mdi:weather-night"], - "issur_melacha_in_effect": ["Issur Melacha in Effect", "mdi:power-plug-off"], - "omer_count": ["Day of the Omer", "mdi:counter"], -} - -CONF_DIASPORA = "diaspora" -CONF_LANGUAGE = "language" -CONF_SENSORS = "sensors" -CONF_CANDLE_LIGHT_MINUTES = "candle_lighting_minutes_before_sunset" -CONF_HAVDALAH_OFFSET_MINUTES = "havdalah_minutes_after_sunset" - -CANDLE_LIGHT_DEFAULT = 18 - -DEFAULT_NAME = "Jewish Calendar" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DIASPORA, default=False): cv.boolean, - vol.Optional(CONF_LATITUDE): cv.latitude, - vol.Optional(CONF_LONGITUDE): cv.longitude, - vol.Optional(CONF_LANGUAGE, default="english"): vol.In(["hebrew", "english"]), - vol.Optional(CONF_CANDLE_LIGHT_MINUTES, default=CANDLE_LIGHT_DEFAULT): int, - # Default of 0 means use 8.5 degrees / 'three_stars' time. - vol.Optional(CONF_HAVDALAH_OFFSET_MINUTES, default=0): int, - vol.Optional(CONF_SENSORS, default=["date"]): vol.All( - cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES)] - ), - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Jewish calendar sensor platform.""" - language = config.get(CONF_LANGUAGE) - name = config.get(CONF_NAME) - latitude = config.get(CONF_LATITUDE, hass.config.latitude) - longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - diaspora = config.get(CONF_DIASPORA) - candle_lighting_offset = config.get(CONF_CANDLE_LIGHT_MINUTES) - havdalah_offset = config.get(CONF_HAVDALAH_OFFSET_MINUTES) - - if None in (latitude, longitude): - _LOGGER.error("Latitude or longitude not set in Home Assistant config") + if discovery_info is None: return - dev = [] - for sensor_type in config[CONF_SENSORS]: - dev.append( - JewishCalSensor( - name, - language, - sensor_type, - latitude, - longitude, - hass.config.time_zone, - diaspora, - candle_lighting_offset, - havdalah_offset, - ) - ) - async_add_entities(dev, True) + sensors = [ + JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info) + for sensor, sensor_info in SENSOR_TYPES["data"].items() + ] + sensors.extend( + JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info) + for sensor, sensor_info in SENSOR_TYPES["time"].items() + ) + + async_add_entities(sensors) -class JewishCalSensor(Entity): +class JewishCalendarSensor(Entity): """Representation of an Jewish calendar sensor.""" - def __init__( - self, - name, - language, - sensor_type, - latitude, - longitude, - timezone, - diaspora, - candle_lighting_offset=CANDLE_LIGHT_DEFAULT, - havdalah_offset=0, - ): + def __init__(self, data, sensor, sensor_info): """Initialize the Jewish calendar sensor.""" - self.client_name = name - self._name = SENSOR_TYPES[sensor_type][0] - self.type = sensor_type - self._hebrew = language == "hebrew" + self._location = data["location"] + self._type = sensor + self._name = f"{data['name']} {sensor_info[0]}" + self._icon = sensor_info[1] + self._hebrew = data["language"] == "hebrew" + self._candle_lighting_offset = data["candle_lighting_offset"] + self._havdalah_offset = data["havdalah_offset"] + self._diaspora = data["diaspora"] self._state = None - self.latitude = latitude - self.longitude = longitude - self.timezone = timezone - self.diaspora = diaspora - self.candle_lighting_offset = candle_lighting_offset - self.havdalah_offset = havdalah_offset - _LOGGER.debug("Sensor %s initialized", self.type) @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return self._name @property def icon(self): """Icon to display in the front end.""" - return SENSOR_TYPES[self.type][1] + return self._icon @property def state(self): @@ -143,9 +62,7 @@ class JewishCalSensor(Entity): async def async_update(self): """Update the state of the sensor.""" - import hdate - - now = dt_util.as_local(dt_util.now()) + now = dt_util.now() _LOGGER.debug("Now: %s Timezone = %s", now, now.tzinfo) today = now.date() @@ -155,66 +72,65 @@ class JewishCalSensor(Entity): _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - location = hdate.Location( - latitude=self.latitude, - longitude=self.longitude, - timezone=self.timezone, - diaspora=self.diaspora, - ) - def make_zmanim(date): """Create a Zmanim object.""" return hdate.Zmanim( date=date, - location=location, - candle_lighting_offset=self.candle_lighting_offset, - havdalah_offset=self.havdalah_offset, + location=self._location, + candle_lighting_offset=self._candle_lighting_offset, + havdalah_offset=self._havdalah_offset, hebrew=self._hebrew, ) - date = hdate.HDate(today, diaspora=self.diaspora, hebrew=self._hebrew) - lagging_date = date + date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) - # Advance Hebrew date if sunset has passed. - # Not all sensors should advance immediately when the Hebrew date - # officially changes (i.e. after sunset), hence lagging_date. - if now > sunset: - date = date.next_day + # The Jewish day starts after darkness (called "tzais") and finishes at + # sunset ("shkia"). The time in between is a gray area (aka "Bein + # Hashmashot" - literally: "in between the sun and the moon"). + + # For some sensors, it is more interesting to consider the date to be + # tomorrow based on sunset ("shkia"), for others based on "tzais". + # Hence the following variables. + after_tzais_date = after_shkia_date = date today_times = make_zmanim(today) + + if now > sunset: + after_shkia_date = date.next_day + if today_times.havdalah and now > today_times.havdalah: - lagging_date = lagging_date.next_day + after_tzais_date = date.next_day # Terminology note: by convention in py-libhdate library, "upcoming" # refers to "current" or "upcoming" dates. - if self.type == "date": - self._state = date.hebrew_date - elif self.type == "weekly_portion": + if self._type == "date": + self._state = after_shkia_date.hebrew_date + elif self._type == "weekly_portion": # Compute the weekly portion based on the upcoming shabbat. - self._state = lagging_date.upcoming_shabbat.parasha - elif self.type == "holiday_name": - self._state = date.holiday_description - elif self.type == "holyness": - self._state = date.holiday_type - elif self.type == "upcoming_shabbat_candle_lighting": - times = make_zmanim(lagging_date.upcoming_shabbat.previous_day.gdate) + self._state = after_tzais_date.upcoming_shabbat.parasha + elif self._type == "holiday_name": + self._state = after_shkia_date.holiday_description + elif self._type == "holiday_type": + self._state = after_shkia_date.holiday_type + elif self._type == "upcoming_shabbat_candle_lighting": + times = make_zmanim(after_tzais_date.upcoming_shabbat.previous_day.gdate) self._state = times.candle_lighting - elif self.type == "upcoming_candle_lighting": + elif self._type == "upcoming_candle_lighting": times = make_zmanim( - lagging_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate + after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate ) self._state = times.candle_lighting - elif self.type == "upcoming_shabbat_havdalah": - times = make_zmanim(lagging_date.upcoming_shabbat.gdate) + elif self._type == "upcoming_shabbat_havdalah": + times = make_zmanim(after_tzais_date.upcoming_shabbat.gdate) self._state = times.havdalah - elif self.type == "upcoming_havdalah": - times = make_zmanim(lagging_date.upcoming_shabbat_or_yom_tov.last_day.gdate) + elif self._type == "upcoming_havdalah": + times = make_zmanim( + after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate + ) self._state = times.havdalah - elif self.type == "issur_melacha_in_effect": - self._state = make_zmanim(now).issur_melacha_in_effect - elif self.type == "omer_count": - self._state = date.omer_day + elif self._type == "omer_count": + self._state = after_shkia_date.omer_day else: times = make_zmanim(today).zmanim - self._state = times[self.type].time() + self._state = times[self._type].time() _LOGGER.debug("New value: %s", self._state) diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index 5d41b2360e9..63f289862f6 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -66,7 +66,7 @@ class KankunSwitch(SwitchDevice): self._hass = hass self._name = name self._state = False - self._url = "http://{}:{}{}".format(host, port, path) + self._url = f"http://{host}:{port}{path}" if user is not None: self._auth = (user, passwd) else: @@ -78,7 +78,7 @@ class KankunSwitch(SwitchDevice): try: req = requests.get( - "{}?set={}".format(self._url, newstate), auth=self._auth, timeout=5 + f"{self._url}?set={newstate}", auth=self._auth, timeout=5 ) return req.json()["ok"] except requests.RequestException: @@ -89,9 +89,7 @@ class KankunSwitch(SwitchDevice): _LOGGER.info("Querying state from: %s", self._url) try: - req = requests.get( - "{}?get=state".format(self._url), auth=self._auth, timeout=5 - ) + req = requests.get(f"{self._url}?get=state", auth=self._auth, timeout=5) return req.json()["state"] == "on" except requests.RequestException: _LOGGER.error("State query failed") diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index 1a3b41f74bd..8b901dcc61e 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -157,7 +157,7 @@ class KeyboardRemoteThread(threading.Thread): try: event = self.dev.read_one() - except IOError: # Keyboard Disconnected + except OSError: # Keyboard Disconnected self.dev = None self.hass.bus.fire( KEYBOARD_REMOTE_DISCONNECTED, diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index 24eb8b06aef..77f91a50dfa 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -32,7 +32,7 @@ CONF_REMOTES = "remotes" CONF_SENSOR = "sensor" CONF_REMOTE = "remote" -CODES_YAML = "{}_codes.yaml".format(DOMAIN) +CODES_YAML = f"{DOMAIN}_codes.yaml" CODE_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index c04feed2337..71a82c6df2a 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -305,7 +305,10 @@ class KNXLight(Light): await self.device.set_color_temperature(kelvin) elif self.device.supports_tunable_white and update_color_temp: # calculate relative_ct from Kelvin to fit typical KNX devices - kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) + kelvin = min( + self._max_kelvin, + int(color_util.color_temperature_mired_to_kelvin(mireds)), + ) relative_ct = int( 255 * (kelvin - self._min_kelvin) diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 5b751bac17c..5faaf0678d1 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -1,9 +1,9 @@ send: description: "Send arbitrary data directly to the KNX bus." fields: - address: + address: description: "Group address(es) to write to." example: "1/1/0" - payload: + payload: description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." example: "[0, 4]" diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 14ef0292ecc..9f0aab6c00c 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -140,7 +140,7 @@ def _check_deprecated_turn_off(hass, turn_off_action): method = DEPRECATED_TURN_OFF_ACTIONS[turn_off_action] new_config = OrderedDict( [ - ("service", "{}.{}".format(DOMAIN, SERVICE_CALL_METHOD)), + ("service", f"{DOMAIN}.{SERVICE_CALL_METHOD}"), ( "data_template", OrderedDict([("entity_id", "{{ entity_id }}"), ("method", method)]), @@ -281,18 +281,18 @@ class KodiDevice(MediaPlayerDevice): if username is not None: kwargs["auth"] = aiohttp.BasicAuth(username, password) - image_auth_string = "{}:{}@".format(username, password) + image_auth_string = f"{username}:{password}@" else: image_auth_string = "" http_protocol = "https" if encryption else "http" ws_protocol = "wss" if encryption else "ws" - self._http_url = "{}://{}:{}/jsonrpc".format(http_protocol, host, port) + self._http_url = f"{http_protocol}://{host}:{port}/jsonrpc" self._image_url = "{}://{}{}:{}/image".format( http_protocol, image_auth_string, host, port ) - self._ws_url = "{}://{}:{}/jsonrpc".format(ws_protocol, host, tcp_port) + self._ws_url = f"{ws_protocol}://{host}:{tcp_port}/jsonrpc" self._http_server = jsonrpc_async.Server(self._http_url, **kwargs) if websocket: @@ -326,14 +326,14 @@ class KodiDevice(MediaPlayerDevice): turn_on_action = script.Script( self.hass, turn_on_action, - "{} turn ON script".format(self.name), + f"{self.name} turn ON script", self.async_update_ha_state(True), ) if turn_off_action is not None: turn_off_action = script.Script( self.hass, _check_deprecated_turn_off(hass, turn_off_action), - "{} turn OFF script".format(self.name), + f"{self.name} turn OFF script", ) self._turn_on_action = turn_on_action self._turn_off_action = turn_off_action diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 70c8669019c..41dfc42b5de 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -62,7 +62,7 @@ async def async_get_service(hass, config, discovery_info=None): ) http_protocol = "https" if encryption else "http" - url = "{}://{}:{}/jsonrpc".format(http_protocol, host, port) + url = f"{http_protocol}://{host}:{port}/jsonrpc" if username is not None: auth = aiohttp.BasicAuth(username, password) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index d98f44bd3d9..4cc872fb78b 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -538,7 +538,7 @@ class KonnectedView(HomeAssistantView): ) auth = request.headers.get(AUTHORIZATION, None) - if not hmac.compare_digest("Bearer {}".format(self.auth_token), auth): + if not hmac.compare_digest(f"Bearer {self.auth_token}", auth): return self.json_message("unauthorized", status_code=HTTP_UNAUTHORIZED) pin_num = int(pin_num) device = data[CONF_DEVICES].get(device_id) diff --git a/homeassistant/components/kwb/sensor.py b/homeassistant/components/kwb/sensor.py index 911884d2a9e..49815faf7ae 100644 --- a/homeassistant/components/kwb/sensor.py +++ b/homeassistant/components/kwb/sensor.py @@ -92,7 +92,7 @@ class KWBSensor(Entity): @property def name(self): """Return the name.""" - return "{} {}".format(self._client_name, self._name) + return f"{self._client_name} {self._name}" @property def available(self) -> bool: diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index ce92cffee53..736792aefd8 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -72,7 +72,7 @@ class LastfmSensor(Entity): @property def entity_id(self): """Return the entity ID.""" - return "sensor.lastfm_{}".format(self._name) + return f"sensor.lastfm_{self._name}" @property def state(self): @@ -84,7 +84,7 @@ class LastfmSensor(Entity): self._cover = self._user.get_image() self._playcount = self._user.get_playcount() last = self._user.get_recent_tracks(limit=2)[0] - self._lastplayed = "{} - {}".format(last.track.artist, last.track.title) + self._lastplayed = f"{last.track.artist} - {last.track.title}" top = self._user.get_top_tracks(limit=1)[0] toptitle = re.search("', '(.+?)',", str(top)) topartist = re.search("'(.+?)',", str(top)) @@ -93,7 +93,7 @@ class LastfmSensor(Entity): self._state = "Not Scrobbling" return now = self._user.get_now_playing() - self._state = "{} - {}".format(now.artist, now.title) + self._state = f"{now.artist} - {now.title}" @property def device_state_attributes(self): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 92d51512726..236035b0400 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -38,7 +38,7 @@ def has_unique_connection_names(connections): if suffix == 0: connection[CONF_NAME] = DEFAULT_NAME else: - connection[CONF_NAME] = "{}{:d}".format(DEFAULT_NAME, suffix) + connection[CONF_NAME] = f"{DEFAULT_NAME}{suffix:d}" schema = vol.Schema(vol.Unique()) schema([connection.get(CONF_NAME) for connection in connections]) diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml old mode 100755 new mode 100644 index b8f4fbb20a7..80c636577f8 --- a/homeassistant/components/lcn/services.yaml +++ b/homeassistant/components/lcn/services.yaml @@ -14,7 +14,7 @@ output_abs: example: 50 transition: description: Transition time in seconds - example: 5 + example: 5 output_rel: description: Set relative brightness of output port in percent. @@ -30,7 +30,7 @@ output_rel: example: 50 transition: description: Transition time in seconds - example: 5 + example: 5 output_toggle: description: Toggle output port. @@ -43,7 +43,7 @@ output_toggle: example: "output1" transition: description: Transition time in seconds - example: 5 + example: 5 relays: description: Set the relays status. @@ -72,7 +72,7 @@ led: - off - blink - flicker - + var_abs: description: Set absolute value of a variable or setpoint. fields: @@ -88,7 +88,7 @@ var_abs: unit_of_measurement: description: Unit of value example: 'celsius' - + var_reset: description: Reset value of variable or setpoint. fields: @@ -98,7 +98,7 @@ var_reset: variable: description: Variable or setpoint name example: 'var1' - + var_rel: description: Shift value of a variable, setpoint or threshold. fields: @@ -188,7 +188,7 @@ dyn_text: text: description: Text to send (up to 60 characters encoded as UTF-8) example: 'text up to 60 characters' - + pck: description: Send arbitrary PCK command. fields: @@ -198,4 +198,3 @@ pck: pck: description: PCK command (without address header) example: 'PIN4' - \ No newline at end of file diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/life360/.translations/ca.json b/homeassistant/components/life360/.translations/ca.json index a7189d69185..58401a33d14 100644 --- a/homeassistant/components/life360/.translations/ca.json +++ b/homeassistant/components/life360/.translations/ca.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Credencials inv\u00e0lides", "invalid_username": "Nom d'usuari incorrecte", + "unexpected": "S'ha produ\u00eft un error inesperat en comunicar-se amb el servidor de Life360.", "user_already_configured": "El compte ja ha estat configurat" }, "step": { diff --git a/homeassistant/components/life360/.translations/da.json b/homeassistant/components/life360/.translations/da.json index 1870c3fdb51..933fce4a4e8 100644 --- a/homeassistant/components/life360/.translations/da.json +++ b/homeassistant/components/life360/.translations/da.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ugyldige legitimationsoplysninger", "invalid_username": "Ugyldigt brugernavn", + "unexpected": "Uventet fejl under kommunikation med Life360-serveren", "user_already_configured": "Kontoen er allerede konfigureret" }, "step": { diff --git a/homeassistant/components/life360/.translations/de.json b/homeassistant/components/life360/.translations/de.json index 27dfbaed2bc..08a55d26cae 100644 --- a/homeassistant/components/life360/.translations/de.json +++ b/homeassistant/components/life360/.translations/de.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", "invalid_username": "Ung\u00fcltiger Benutzername", + "unexpected": "Unerwarteter Fehler bei der Kommunikation mit dem Life360-Server", "user_already_configured": "Konto wurde bereits konfiguriert" }, "step": { diff --git a/homeassistant/components/life360/.translations/hu.json b/homeassistant/components/life360/.translations/hu.json new file mode 100644 index 00000000000..227e784b065 --- /dev/null +++ b/homeassistant/components/life360/.translations/hu.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "unexpected": "V\u00e1ratlan hiba t\u00f6rt\u00e9nt a kommunik\u00e1ci\u00f3ban a Life360 szerverrel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/it.json b/homeassistant/components/life360/.translations/it.json index 9c4cb1cc4cb..b7d2d6c8f1b 100644 --- a/homeassistant/components/life360/.translations/it.json +++ b/homeassistant/components/life360/.translations/it.json @@ -5,11 +5,12 @@ "user_already_configured": "L'account \u00e8 gi\u00e0 stato configurato" }, "create_entry": { - "default": "Per impostare le opzioni avanzate, consultare la [Documentazione Life360] ( {docs_url} )." + "default": "Per impostare le opzioni avanzate, consultare la [Documentazione Life360]({docs_url})." }, "error": { "invalid_credentials": "Credenziali non valide", "invalid_username": "Nome utente non valido", + "unexpected": "Errore imprevisto durante la comunicazione con il server di Life360", "user_already_configured": "L'account \u00e8 gi\u00e0 stato configurato" }, "step": { @@ -18,6 +19,7 @@ "password": "Password", "username": "Nome utente" }, + "description": "Per impostare le opzioni avanzate, vedere [Documentazione di Life360]({docs_url}).\n\u00c8 consigliabile eseguire questa operazione prima di aggiungere gli account.", "title": "Informazioni sull'account Life360" } }, diff --git a/homeassistant/components/life360/.translations/ko.json b/homeassistant/components/life360/.translations/ko.json index b81a6fd059f..067b305b80c 100644 --- a/homeassistant/components/life360/.translations/ko.json +++ b/homeassistant/components/life360/.translations/ko.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "\ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_username": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unexpected": "Life360 \uc11c\ubc84 \uc5f0\uacb0\uc911 \uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "user_already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { diff --git a/homeassistant/components/life360/.translations/nl.json b/homeassistant/components/life360/.translations/nl.json index ec7a5332950..08be66a8963 100644 --- a/homeassistant/components/life360/.translations/nl.json +++ b/homeassistant/components/life360/.translations/nl.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ongeldige gebruikersgegevens", "invalid_username": "Ongeldige gebruikersnaam", + "unexpected": "Onverwachte fout bij communicatie met Life360-server", "user_already_configured": "Account is al geconfigureerd" }, "step": { diff --git a/homeassistant/components/life360/.translations/no.json b/homeassistant/components/life360/.translations/no.json index 1a1e98c526e..032dd606cbd 100644 --- a/homeassistant/components/life360/.translations/no.json +++ b/homeassistant/components/life360/.translations/no.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Ugyldig legitimasjon", "invalid_username": "Ugyldig brukernavn", + "unexpected": "Uventet feil under kommunikasjon med Life360-servern", "user_already_configured": "Kontoen er allerede konfigurert" }, "step": { diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json index 15aabaa6308..e9cd9920304 100644 --- a/homeassistant/components/life360/.translations/pl.json +++ b/homeassistant/components/life360/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", - "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." + "user_already_configured": "Konto jest ju\u017c skonfigurowane" }, "create_entry": { "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." @@ -10,7 +10,8 @@ "error": { "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", - "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." + "unexpected": "Nieoczekiwany b\u0142\u0105d komunikacji z serwerem Life360", + "user_already_configured": "Konto jest ju\u017c skonfigurowane" }, "step": { "user": { diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index 0f698457bf7..c03ad0f7e1f 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", + "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" }, "step": { diff --git a/homeassistant/components/life360/.translations/sl.json b/homeassistant/components/life360/.translations/sl.json index 36e4917256b..2bb3bb4833e 100644 --- a/homeassistant/components/life360/.translations/sl.json +++ b/homeassistant/components/life360/.translations/sl.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "Napa\u010dno geslo", "invalid_username": "Napa\u010dno uporabni\u0161ko ime", + "unexpected": "Nepri\u010dakovana napaka pri komunikaciji s stre\u017enikom Life360", "user_already_configured": "Ra\u010dun \u017ee nastavljen" }, "step": { diff --git a/homeassistant/components/life360/.translations/zh-Hant.json b/homeassistant/components/life360/.translations/zh-Hant.json index 8ab5dcf5369..75081c62d41 100644 --- a/homeassistant/components/life360/.translations/zh-Hant.json +++ b/homeassistant/components/life360/.translations/zh-Hant.json @@ -10,6 +10,7 @@ "error": { "invalid_credentials": "\u6191\u8b49\u7121\u6548", "invalid_username": "\u4f7f\u7528\u8005\u540d\u7a31\u7121\u6548", + "unexpected": "\u8207 Life360 \u4f3a\u670d\u5668\u901a\u8a0a\u767c\u751f\u672a\u77e5\u932f\u8aa4", "user_already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, "step": { diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index be84d276422..6af999a575d 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -99,7 +99,7 @@ class Life360ConfigFlow(config_entries.ConfigFlow): ) return self.async_abort(reason="unexpected") return self.async_create_entry( - title="{} (from configuration)".format(username), + title=f"{username} (from configuration)", data={ CONF_USERNAME: username, CONF_PASSWORD: password, diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index dc5645a5216..ddd562ebfac 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -159,7 +159,7 @@ class Life360Scanner: _errs = self._errs.get(key, 0) if _errs < self._max_errs: self._errs[key] = _errs = _errs + 1 - msg = "{}: {}".format(key, err_msg) + msg = f"{key}: {err_msg}" if _errs >= self._error_threshold: if _errs == self._max_errs: msg = "Suppressing further errors until OK: " + msg @@ -233,14 +233,12 @@ class Life360Scanner: convert(float(gps_accuracy), LENGTH_FEET, LENGTH_METERS) ) except (TypeError, ValueError): - self._err( - dev_id, "GPS data invalid: {}, {}, {}".format(lat, lon, gps_accuracy) - ) + self._err(dev_id, f"GPS data invalid: {lat}, {lon}, {gps_accuracy}") return self._ok(dev_id) - msg = "Updating {}".format(dev_id) + msg = f"Updating {dev_id}" if prev_seen: msg += "; Time since last update: {}".format(last_seen - prev_seen) _LOGGER.debug(msg) @@ -401,7 +399,7 @@ class Life360Scanner: except (Life360Error, KeyError): pass if incl_circle: - err_key = 'get_circle_members "{}"'.format(circle_name) + err_key = f'get_circle_members "{circle_name}"' try: members = api.get_circle_members(circle_id) except Life360Error as exc: diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index b3ec9ed288f..ed26db3d49e 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -509,7 +509,7 @@ class LIFXLight(Light): @property def who(self): """Return a string identifying the bulb.""" - return "%s (%s)" % (self.bulb.ip_addr, self.name) + return f"{self.bulb.ip_addr} ({self.name})" @property def min_mireds(self): diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index 6fc9fac1267..ac4e0201fb8 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -31,7 +31,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= token = config.get(CONF_TOKEN) timeout = config.get(CONF_TIMEOUT) - headers = {AUTHORIZATION: "Bearer {}".format(token)} + headers = {AUTHORIZATION: f"Bearer {token}"} url = LIFX_API_URL.format("scenes") diff --git a/homeassistant/components/light/.translations/ca.json b/homeassistant/components/light/.translations/ca.json new file mode 100644 index 00000000000..5017af8e576 --- /dev/null +++ b/homeassistant/components/light/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Commuta {name}", + "turn_off": "Apaga {name}", + "turn_on": "Enc\u00e9n {name}" + }, + "condition_type": { + "is_off": "{name} est\u00e0 apagat", + "is_on": "{name} est\u00e0 enc\u00e8s" + }, + "trigger_type": { + "turn_off": "{name} apagat", + "turn_on": "{name} enc\u00e8s" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json new file mode 100644 index 00000000000..7b266ba7412 --- /dev/null +++ b/homeassistant/components/light/.translations/da.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} slukket", + "turn_on": "{name} t\u00e6ndt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json new file mode 100644 index 00000000000..fcfc2773ed8 --- /dev/null +++ b/homeassistant/components/light/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} ausgeschaltet", + "turn_on": "{name} eingeschaltet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/en.json b/homeassistant/components/light/.translations/en.json new file mode 100644 index 00000000000..60ccbd99348 --- /dev/null +++ b/homeassistant/components/light/.translations/en.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_off": "Turn off {entity_name}", + "turn_on": "Turn on {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} is off", + "is_on": "{entity_name} is on" + }, + "trigger_type": { + "turn_off": "{entity_name} turned off", + "turn_on": "{entity_name} turned on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/es.json b/homeassistant/components/light/.translations/es.json new file mode 100644 index 00000000000..b56875453dd --- /dev/null +++ b/homeassistant/components/light/.translations/es.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{nombre} desactivado", + "turn_on": "{nombre} activado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json new file mode 100644 index 00000000000..00d03b12d01 --- /dev/null +++ b/homeassistant/components/light/.translations/fr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} d\u00e9sactiv\u00e9", + "turn_on": "{name} activ\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/it.json b/homeassistant/components/light/.translations/it.json new file mode 100644 index 00000000000..85a117f0b53 --- /dev/null +++ b/homeassistant/components/light/.translations/it.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Commuta {entity_name}", + "turn_off": "Spegnere {entity_name}", + "turn_on": "Accendere {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u00e8 disattivato", + "is_on": "{entity_name} \u00e8 attivo" + }, + "trigger_type": { + "turn_off": "{entity_name} disattivato", + "turn_on": "{entity_name} attivato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json new file mode 100644 index 00000000000..7277ef5900f --- /dev/null +++ b/homeassistant/components/light/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \ud1a0\uae00", + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" + }, + "condition_type": { + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + }, + "trigger_type": { + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/nl.json b/homeassistant/components/light/.translations/nl.json new file mode 100644 index 00000000000..546fea78b6d --- /dev/null +++ b/homeassistant/components/light/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Omschakelen {naam}", + "turn_off": "{Naam} uitschakelen", + "turn_on": "{Naam} inschakelen" + }, + "condition_type": { + "is_off": "{name} is uitgeschakeld", + "is_on": "{name} is ingeschakeld" + }, + "trigger_type": { + "turn_off": "{name} is uitgeschakeld", + "turn_on": "{name} is ingeschakeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/no.json b/homeassistant/components/light/.translations/no.json new file mode 100644 index 00000000000..39c391eff33 --- /dev/null +++ b/homeassistant/components/light/.translations/no.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} sl\u00e5tt av", + "turn_on": "{name} sl\u00e5tt p\u00e5" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json new file mode 100644 index 00000000000..9debeaf4169 --- /dev/null +++ b/homeassistant/components/light/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Prze\u0142\u0105cz {entity_name}", + "turn_off": "Wy\u0142\u0105cz {entity_name}", + "turn_on": "W\u0142\u0105cz {entity_name}" + }, + "condition_type": { + "is_off": "(entity_name} jest wy\u0142\u0105czony.", + "is_on": "(entity_name} jest w\u0142\u0105czony." + }, + "trigger_type": { + "turn_off": "{nazwa} wy\u0142\u0105czone", + "turn_on": "{name} w\u0142\u0105czone" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json new file mode 100644 index 00000000000..3154e17a509 --- /dev/null +++ b/homeassistant/components/light/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "trigger_type": { + "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json new file mode 100644 index 00000000000..68e770e8873 --- /dev/null +++ b/homeassistant/components/light/.translations/sl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turn_off": "{name} izklopljeno", + "turn_on": "{name} vklopljeno" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/zh-Hant.json b/homeassistant/components/light/.translations/zh-Hant.json new file mode 100644 index 00000000000..269715b7cc3 --- /dev/null +++ b/homeassistant/components/light/.translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u5207\u63db {name}", + "turn_off": "\u95dc\u9589 {name}", + "turn_on": "\u958b\u555f {name}" + }, + "condition_type": { + "is_off": "{name} \u5df2\u95dc\u9589", + "is_on": "{name} \u5df2\u958b\u555f" + }, + "trigger_type": { + "turn_off": "\u7531 {name} \u95dc\u9589", + "turn_on": "\u7531 {name} \u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index c70a209a35a..ed61d961d88 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -128,9 +128,12 @@ LIGHT_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( } ) -LIGHT_TURN_OFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( - {ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG])} -) + +LIGHT_TURN_OFF_SCHEMA = { + ATTR_TRANSITION: VALID_TRANSITION, + ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), +} + LIGHT_TOGGLE_SCHEMA = LIGHT_TURN_ON_SCHEMA @@ -234,16 +237,16 @@ class SetIntentHandler(intent.IntentHandler): response = intent_obj.create_response() if not speech_parts: # No attributes changed - speech = "Turned on {}".format(state.name) + speech = f"Turned on {state.name}" else: - parts = ["Changed {} to".format(state.name)] + parts = [f"Changed {state.name} to"] for index, part in enumerate(speech_parts): if index == 0: - parts.append(" {}".format(part)) + parts.append(f" {part}") elif index != len(speech_parts) - 1: - parts.append(", {}".format(part)) + parts.append(f", {part}") else: - parts.append(" and {}".format(part)) + parts.append(f" and {part}") speech = "".join(parts) response.async_set_speech(speech) diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py index ed75b5f906f..61292d47449 100644 --- a/homeassistant/components/light/device_automation.py +++ b/homeassistant/components/light/device_automation.py @@ -1,91 +1,56 @@ """Provides device automations for lights.""" import voluptuous as vol -import homeassistant.components.automation.state as state -from homeassistant.core import split_entity_id -from homeassistant.const import ( - CONF_DEVICE_ID, - CONF_DOMAIN, - CONF_ENTITY_ID, - CONF_PLATFORM, - CONF_TYPE, -) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN from . import DOMAIN # mypy: allow-untyped-defs, no-check-untyped-defs -CONF_TURN_OFF = "turn_off" -CONF_TURN_ON = "turn_on" +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) -ENTITY_TRIGGERS = [ - { - # Trigger when light is turned on - CONF_PLATFORM: "device", - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_TURN_OFF, - }, - { - # Trigger when light is turned off - CONF_PLATFORM: "device", - CONF_DOMAIN: DOMAIN, - CONF_TYPE: CONF_TURN_ON, - }, -] +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) -TRIGGER_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_PLATFORM): "device", - vol.Optional(CONF_DEVICE_ID): str, - vol.Required(CONF_DOMAIN): DOMAIN, - vol.Required(CONF_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TYPE): str, - } - ) +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} ) -def _is_domain(entity, domain): - return split_entity_id(entity.entity_id)[0] == domain +async def async_call_action_from_config(hass, config, variables, context): + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) -async def async_attach_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" - trigger_type = config.get(CONF_TYPE) - if trigger_type == CONF_TURN_ON: - from_state = "off" - to_state = "on" - else: - from_state = "on" - to_state = "off" - state_config = { - state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, - } - - return await state.async_trigger(hass, state_config, action, automation_info) +def async_condition_from_config(config, config_validation): + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) async def async_trigger(hass, config, action, automation_info): - """Temporary so existing automation framework can be used for testing.""" - return await async_attach_trigger(hass, config, action, automation_info) + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_actions(hass, device_id): + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + + +async def async_get_conditions(hass, device_id): + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) async def async_get_triggers(hass, device_id): """List device triggers.""" - triggers = [] - entity_registry = await hass.helpers.entity_registry.async_get_registry() - - entities = async_entries_for_device(entity_registry, device_id) - domain_entities = [x for x in entities if _is_domain(x, DOMAIN)] - for entity in domain_entities: - for trigger in ENTITY_TRIGGERS: - trigger = dict(trigger) - trigger.update(device_id=device_id, entity_id=entity.entity_id) - triggers.append(trigger) - - return triggers + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/light/strings.json b/homeassistant/components/light/strings.json new file mode 100644 index 00000000000..77b842ba078 --- /dev/null +++ b/homeassistant/components/light/strings.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" + }, + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + } + } +} diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 9877f6ed091..1af84a4c4ab 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -100,7 +100,7 @@ class LinksysSmartWifiDeviceScanner(DeviceScanner): ] headers = {"X-JNAP-Action": "http://linksys.com/jnap/core/Transaction"} return requests.post( - "http://{}/JNAP/".format(self.host), + f"http://{self.host}/JNAP/", timeout=DEFAULT_TIMEOUT, headers=headers, json=data, diff --git a/homeassistant/components/linky/.translations/ca.json b/homeassistant/components/linky/.translations/ca.json new file mode 100644 index 00000000000..ca437417f59 --- /dev/null +++ b/homeassistant/components/linky/.translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "El compte ja ha estat configurat" + }, + "error": { + "access": "No s'ha pogut accedir a Enedis.fr, comprova la teva connexi\u00f3 a Internet", + "enedis": "Enedis.fr ha respost amb un error: torna-ho a provar m\u00e9s tard (millo no entre les 23:00 i les 14:00)", + "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard (millor no entre les 23:00 i les 14:00)", + "username_exists": "El compte ja ha estat configurat", + "wrong_login": "Error d\u2019inici de sessi\u00f3: comprova el teu correu electr\u00f2nic i la contrasenya" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "description": "Introdueix les teves credencials", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/da.json b/homeassistant/components/linky/.translations/da.json new file mode 100644 index 00000000000..cacad99de58 --- /dev/null +++ b/homeassistant/components/linky/.translations/da.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigureret" + }, + "error": { + "access": "Kunne ikke f\u00e5 adgang til Enedis.fr, kontroller din internetforbindelse", + "enedis": "Enedis.fr svarede med en fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)", + "unknown": "Ukendt fejl: Pr\u00f8v igen senere (normalt ikke mellem 23:00 og 02:00)", + "username_exists": "Kontoen er allerede konfigureret", + "wrong_login": "Loginfejl: Kontroller din e-mail og adgangskode" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "E-mail" + }, + "description": "Indtast dine legitimationsoplysninger", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/de.json b/homeassistant/components/linky/.translations/de.json new file mode 100644 index 00000000000..3fc13126270 --- /dev/null +++ b/homeassistant/components/linky/.translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Konto bereits konfiguriert" + }, + "error": { + "access": "Konnte nicht auf Enedis.fr zugreifen, \u00fcberpr\u00fcfe bitte die Internetverbindung", + "enedis": "Enedis.fr antwortete mit einem Fehler: wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)", + "unknown": "Unbekannter Fehler: Wiederhole den Vorgang sp\u00e4ter (in der Regel nicht zwischen 23 Uhr und 2 Uhr morgens)", + "username_exists": "Konto bereits konfiguriert", + "wrong_login": "Login-Fehler: Pr\u00fcfe bitte E-Mail & Passwort" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail" + }, + "description": "Gib deine Zugangsdaten ein", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/en.json b/homeassistant/components/linky/.translations/en.json new file mode 100644 index 00000000000..6c655b83581 --- /dev/null +++ b/homeassistant/components/linky/.translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Account already configured" + }, + "error": { + "access": "Could not access to Enedis.fr, please check your internet connection", + "enedis": "Enedis.fr answered with an error: please retry later (usually not between 11PM and 2AM)", + "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)", + "username_exists": "Account already configured", + "wrong_login": "Login error: please check your email & password" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "description": "Enter your credentials", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/fr.json b/homeassistant/components/linky/.translations/fr.json new file mode 100644 index 00000000000..af12c2b654d --- /dev/null +++ b/homeassistant/components/linky/.translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "access": "Impossible d'acc\u00e9der \u00e0 Enedis.fr, merci de v\u00e9rifier votre connexion internet", + "enedis": "Erreur d'Enedis.fr: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", + "unknown": "Erreur inconnue: merci de r\u00e9essayer plus tard (pas entre 23h et 2h)", + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", + "wrong_login": "Impossible de vous identifier: merci de v\u00e9rifier vos identifiants" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "description": "Entrez vos identifiants", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/it.json b/homeassistant/components/linky/.translations/it.json new file mode 100644 index 00000000000..09d5f7e2d2b --- /dev/null +++ b/homeassistant/components/linky/.translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Account gi\u00e0 configurato" + }, + "error": { + "access": "Impossibile accedere a Enedis.fr, si prega di controllare la connessione internet", + "enedis": "Enedis.fr ha risposto con un errore: si prega di riprovare pi\u00f9 tardi (di solito non tra le 23:00 e le 02:00).", + "unknown": "Errore sconosciuto: riprova pi\u00f9 tardi (in genere non tra le 23:00 e le 02:00)", + "username_exists": "Account gi\u00e0 configurato", + "wrong_login": "Errore di accesso: si prega di controllare la tua E-mail e la password" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "E-mail" + }, + "description": "Inserisci le tue credenziali", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ko.json b/homeassistant/components/linky/.translations/ko.json new file mode 100644 index 00000000000..45172e70097 --- /dev/null +++ b/homeassistant/components/linky/.translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "access": "Enedis.fr \uc5d0 \uc811\uc18d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc778\ud130\ub137 \uc5f0\uacb0\uc744 \ud655\uc778\ud574\ubcf4\uc138\uc694", + "enedis": "Enedis.fr \uc774 \uc624\ub958\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)", + "unknown": "\uc54c \uc218\uc5c6\ub294 \uc624\ub958: \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694 (\uc800\ub141 11\uc2dc \ubd80\ud130 \uc0c8\ubcbd 2\uc2dc\ub294 \ud53c\ud574\uc8fc\uc138\uc694)", + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "wrong_login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + }, + "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/nl.json b/homeassistant/components/linky/.translations/nl.json new file mode 100644 index 00000000000..89759fdf216 --- /dev/null +++ b/homeassistant/components/linky/.translations/nl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Account reeds geconfigureerd" + }, + "error": { + "access": "Geen toegang tot Enedis.fr, controleer uw internetverbinding", + "enedis": "Enedis.fr antwoordde met een fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", + "unknown": "Onbekende fout: probeer het later opnieuw (meestal niet tussen 23.00 en 02.00 uur)", + "username_exists": "Account reeds geconfigureerd", + "wrong_login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + }, + "description": "Voer uw gegevens in", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json new file mode 100644 index 00000000000..a4f68fa8687 --- /dev/null +++ b/homeassistant/components/linky/.translations/pl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "access": "Nie mo\u017cna uzyska\u0107 dost\u0119pu do Enedis.fr, sprawd\u017a po\u0142\u0105czenie internetowe", + "enedis": "Enedis.fr odpowiedzia\u0142 b\u0142\u0119dem: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy 23:00, a 2:00)", + "unknown": "Nieznany b\u0142\u0105d: spr\u00f3buj ponownie p\u00f3\u017aniej (zwykle nie mi\u0119dzy godzin\u0105 23:00, a 2:00)", + "username_exists": "Konto jest ju\u017c skonfigurowane", + "wrong_login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "E-mail" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json new file mode 100644 index 00000000000..498b5b2f12f --- /dev/null +++ b/homeassistant/components/linky/.translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + }, + "error": { + "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", + "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430", + "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/sl.json b/homeassistant/components/linky/.translations/sl.json new file mode 100644 index 00000000000..9e9d6668fcb --- /dev/null +++ b/homeassistant/components/linky/.translations/sl.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "Ra\u010dun \u017ee nastavljen" + }, + "error": { + "access": "Do Enedis.fr ni bilo mogo\u010de dostopati, preverite internetno povezavo", + "enedis": "Enedis.fr je odgovoril z napako: poskusite pozneje (ponavadi med 23. in 2. uro)", + "unknown": "Neznana napaka: Prosimo, poskusite pozneje (obi\u010dajno ne med 23. in 2. uro)", + "username_exists": "Ra\u010dun \u017ee nastavljen", + "wrong_login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "description": "Vnesite svoje poverilnice", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/zh-Hant.json b/homeassistant/components/linky/.translations/zh-Hant.json new file mode 100644 index 00000000000..bcfac6643c8 --- /dev/null +++ b/homeassistant/components/linky/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "access": "\u7121\u6cd5\u8a2a\u554f Enedis.fr\uff0c\u8acb\u6aa2\u67e5\u60a8\u7684\u7db2\u969b\u7db2\u8def\u9023\u7dda", + "enedis": "Endis.fr \u56de\u5831\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09", + "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66\uff08\u901a\u5e38\u907f\u958b\u591c\u9593 11 - \u51cc\u6668 2 \u9ede\u4e4b\u9593\uff09", + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "wrong_login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8f38\u5165\u6191\u8b49", + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py index 345f13e8a57..a7f3d7bb03e 100644 --- a/homeassistant/components/linky/__init__.py +++ b/homeassistant/components/linky/__init__.py @@ -1 +1,55 @@ """The linky component.""" +import logging + +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DEFAULT_TIMEOUT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up Linky sensors from legacy config file.""" + + conf = config.get(DOMAIN) + if conf is None: + return True + + for linky_account_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=linky_account_conf.copy(), + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Linky sensors.""" + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + return True diff --git a/homeassistant/components/linky/config_flow.py b/homeassistant/components/linky/config_flow.py new file mode 100644 index 00000000000..3b882eed2ad --- /dev/null +++ b/homeassistant/components/linky/config_flow.py @@ -0,0 +1,118 @@ +"""Config flow to configure the Linky integration.""" +import logging + +import voluptuous as vol +from pylinky.client import LinkyClient +from pylinky.exceptions import ( + PyLinkyAccessException, + PyLinkyEnedisException, + PyLinkyException, + PyLinkyWrongLoginException, +) + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +from homeassistant.core import callback + +from .const import DEFAULT_TIMEOUT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class LinkyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize Linky config flow.""" + self._username = None + self._password = None + self._timeout = None + + def _configuration_exists(self, username: str) -> bool: + """Return True if username exists in configuration.""" + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_USERNAME] == username: + return True + return False + + @callback + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=user_input.get(CONF_USERNAME, "") + ): str, + vol.Required( + CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "") + ): str, + } + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self._show_setup_form(user_input, None) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + self._timeout = user_input.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) + + if self._configuration_exists(self._username): + errors[CONF_USERNAME] = "username_exists" + return self._show_setup_form(user_input, errors) + + client = LinkyClient(self._username, self._password, None, self._timeout) + try: + await self.hass.async_add_executor_job(client.login) + await self.hass.async_add_executor_job(client.fetch_data) + except PyLinkyAccessException as exp: + _LOGGER.error(exp) + errors["base"] = "access" + return self._show_setup_form(user_input, errors) + except PyLinkyEnedisException as exp: + _LOGGER.error(exp) + errors["base"] = "enedis" + return self._show_setup_form(user_input, errors) + except PyLinkyWrongLoginException as exp: + _LOGGER.error(exp) + errors["base"] = "wrong_login" + return self._show_setup_form(user_input, errors) + except PyLinkyException as exp: + _LOGGER.error(exp) + errors["base"] = "unknown" + return self._show_setup_form(user_input, errors) + finally: + client.close_session() + + return self.async_create_entry( + title=self._username, + data={ + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_TIMEOUT: self._timeout, + }, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry. + + Only host was required in the yaml file all other fields are optional + """ + if self._configuration_exists(user_input[CONF_USERNAME]): + return self.async_abort(reason="username_exists") + + return await self.async_step_user(user_input) diff --git a/homeassistant/components/linky/const.py b/homeassistant/components/linky/const.py new file mode 100644 index 00000000000..e8e68867528 --- /dev/null +++ b/homeassistant/components/linky/const.py @@ -0,0 +1,5 @@ +"""Linky component constants.""" + +DOMAIN = "linky" + +DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/linky/manifest.json b/homeassistant/components/linky/manifest.json index cd4ac4665e2..10a5bbcf864 100644 --- a/homeassistant/components/linky/manifest.json +++ b/homeassistant/components/linky/manifest.json @@ -1,13 +1,13 @@ { "domain": "linky", "name": "Linky", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/linky", "requirements": [ - "pylinky==0.3.3" + "pylinky==0.4.0" ], "dependencies": [], "codeowners": [ - "@tiste", "@Quentame" ] } diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 98aca67d8ea..5ff04c5ee70 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -1,12 +1,12 @@ """Support for Linky.""" -from datetime import timedelta import json import logging +from datetime import timedelta -from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient, PyLinkyError -import voluptuous as vol +from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient +from pylinky.client import PyLinkyException -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_PASSWORD, @@ -14,10 +14,11 @@ from homeassistant.const import ( CONF_USERNAME, ENERGY_KILO_WATT_HOUR, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval -import homeassistant.util.dt as dt_util +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -29,71 +30,54 @@ INDEX_CURRENT = -1 INDEX_LAST = -2 ATTRIBUTION = "Data provided by Enedis" -DEFAULT_TIMEOUT = 10 -SENSORS = { - "yesterday": ("Linky yesterday", DAILY, INDEX_LAST), - "current_month": ("Linky current month", MONTHLY, INDEX_CURRENT), - "last_month": ("Linky last month", MONTHLY, INDEX_LAST), - "current_year": ("Linky current year", YEARLY, INDEX_CURRENT), - "last_year": ("Linky last year", YEARLY, INDEX_LAST), -} -SENSORS_INDEX_LABEL = 0 -SENSORS_INDEX_SCALE = 1 -SENSORS_INDEX_WHEN = 2 -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } -) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way of setting up the Linky platform.""" + pass -def setup_platform(hass, config, add_entities, discovery_info=None): - """Configure the platform and add the Linky sensor.""" - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] - timeout = config[CONF_TIMEOUT] +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Add Linky entries.""" + account = LinkyAccount( + entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD], entry.data[CONF_TIMEOUT] + ) - account = LinkyAccount(hass, add_entities, username, password, timeout) - add_entities(account.sensors, True) + await hass.async_add_executor_job(account.update_linky_data) + + sensors = [ + LinkySensor("Linky yesterday", account, DAILY, INDEX_LAST), + LinkySensor("Linky current month", account, MONTHLY, INDEX_CURRENT), + LinkySensor("Linky last month", account, MONTHLY, INDEX_LAST), + LinkySensor("Linky current year", account, YEARLY, INDEX_CURRENT), + LinkySensor("Linky last year", account, YEARLY, INDEX_LAST), + ] + + async_track_time_interval(hass, account.update_linky_data, SCAN_INTERVAL) + + async_add_entities(sensors, True) class LinkyAccount: """Representation of a Linky account.""" - def __init__(self, hass, add_entities, username, password, timeout): + def __init__(self, username, password, timeout): """Initialise the Linky account.""" self._username = username - self.__password = password + self._password = password self._timeout = timeout self._data = None - self.sensors = [] - self.update_linky_data(dt_util.utcnow()) - - self.sensors.append(LinkySensor("Linky yesterday", self, DAILY, INDEX_LAST)) - self.sensors.append( - LinkySensor("Linky current month", self, MONTHLY, INDEX_CURRENT) - ) - self.sensors.append(LinkySensor("Linky last month", self, MONTHLY, INDEX_LAST)) - self.sensors.append( - LinkySensor("Linky current year", self, YEARLY, INDEX_CURRENT) - ) - self.sensors.append(LinkySensor("Linky last year", self, YEARLY, INDEX_LAST)) - - track_time_interval(hass, self.update_linky_data, SCAN_INTERVAL) - - def update_linky_data(self, event_time): + def update_linky_data(self, event_time=None): """Fetch new state data for the sensor.""" - client = LinkyClient(self._username, self.__password, None, self._timeout) + client = LinkyClient(self._username, self._password, None, self._timeout) try: client.login() client.fetch_data() self._data = client.get_data() _LOGGER.debug(json.dumps(self._data, indent=2)) - except PyLinkyError as exp: + except PyLinkyException as exp: _LOGGER.error(exp) finally: client.close_session() @@ -115,12 +99,18 @@ class LinkySensor(Entity): def __init__(self, name, account: LinkyAccount, scale, when): """Initialize the sensor.""" self._name = name - self.__account = account + self._account = account self._scale = scale - self.__when = when + self._when = when self._username = account.username - self.__time = None - self.__consumption = None + self._time = None + self._consumption = None + self._unique_id = f"{self._username}_{scale}_{when}" + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id @property def name(self): @@ -130,7 +120,7 @@ class LinkySensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self.__consumption + return self._consumption @property def unit_of_measurement(self): @@ -147,18 +137,27 @@ class LinkySensor(Entity): """Return the state attributes of the sensor.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - "time": self.__time, + "time": self._time, CONF_USERNAME: self._username, } - def update(self): + @property + def device_info(self): + """Return device information.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Enedis", + } + + async def async_update(self) -> None: """Retrieve the new data for the sensor.""" - data = self.__account.data[self._scale][self.__when] - self.__consumption = data[CONSUMPTION] - self.__time = data[TIME] + data = self._account.data[self._scale][self._when] + self._consumption = data[CONSUMPTION] + self._time = data[TIME] if self._scale is not YEARLY: year_index = INDEX_CURRENT - if self.__time.endswith("Dec"): + if self._time.endswith("Dec"): year_index = INDEX_LAST - self.__time += " " + self.__account.data[YEARLY][year_index][TIME] + self._time += " " + self._account.data[YEARLY][year_index][TIME] diff --git a/homeassistant/components/linky/strings.json b/homeassistant/components/linky/strings.json new file mode 100644 index 00000000000..e5aa04cad1f --- /dev/null +++ b/homeassistant/components/linky/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "title": "Linky", + "step": { + "user": { + "title": "Linky", + "description": "Enter your credentials", + "data": { + "username": "Email", + "password": "Password" + } + } + }, + "error":{ + "username_exists": "Account already configured", + "access": "Could not access to Enedis.fr, please check your internet connection", + "enedis": "Enedis.fr answered with an error: please retry later (usually not between 11PM and 2AM)", + "wrong_login": "Login error: please check your email & password", + "unknown": "Unknown error: please retry later (usually not between 11PM and 2AM)" + }, + "abort":{ + "username_exists": "Account already configured" + } + } +} diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 9eb7090e957..9256c3ad18d 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if system == "android": os.listdir(os.path.join(DEFAULT_PATH, "battery")) else: - os.listdir(os.path.join(DEFAULT_PATH, "BAT{}".format(battery_id))) + os.listdir(os.path.join(DEFAULT_PATH, f"BAT{battery_id}")) except FileNotFoundError: _LOGGER.error("No battery found") return False diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index 3c3c8bb9b38..c466d71c4c5 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -70,7 +70,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: device = LiveboxPlayTvDevice(host, port, name) livebox_devices.append(device) - except IOError: + except OSError: _LOGGER.error( "Failed to connect to Livebox Play TV at %s:%s. " "Please check your configuration", @@ -178,7 +178,7 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): """Title of current playing media.""" if self._current_channel: if self._current_program: - return "{}: {}".format(self._current_channel, self._current_program) + return f"{self._current_channel}: {self._current_program}" return self._current_channel @property diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index e1df5b980a9..61e0b1f7474 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -22,7 +22,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) DOMAIN = "locative" -TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" ATTR_DEVICE_ID = "device" @@ -76,12 +76,10 @@ async def handle_webhook(hass, webhook_id, request): if direction == "enter": async_dispatcher_send(hass, TRACKER_UPDATE, device, gps_location, location_name) - return web.Response( - text="Setting location to {}".format(location_name), status=HTTP_OK - ) + return web.Response(text=f"Setting location to {location_name}", status=HTTP_OK) if direction == "exit": - current_state = hass.states.get("{}.{}".format(DEVICE_TRACKER, device)) + current_state = hass.states.get(f"{DEVICE_TRACKER}.{device}") if current_state is None or current_state.state == location_name: location_name = STATE_NOT_HOME @@ -108,7 +106,7 @@ async def handle_webhook(hass, webhook_id, request): _LOGGER.error("Received unidentified message from Locative: %s", direction) return web.Response( - text="Received unidentified message: {}".format(direction), + text=f"Received unidentified message: {direction}", status=HTTP_UNPROCESSABLE_ENTITY, ) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 0383e73105b..3c5e828765c 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -188,7 +188,7 @@ def humanify(hass, events): - if 2+ sensor updates in GROUP_BY_MINUTES, show last - if home assistant stop and start happen in same minute call it restarted """ - domain_prefixes = tuple("{}.".format(dom) for dom in CONTINUOUS_DOMAINS) + domain_prefixes = tuple(f"{dom}." for dom in CONTINUOUS_DOMAINS) # Group events in batches of GROUP_BY_MINUTES for _, g_events in groupby( @@ -332,7 +332,7 @@ def humanify(hass, events): entity_id = data.get(ATTR_ENTITY_ID) value = data.get(ATTR_VALUE) - value_msg = " to {}".format(value) if value else "" + value_msg = f" to {value}" if value else "" message = "send command {}{} for {}".format( data[ATTR_SERVICE], value_msg, data[ATTR_DISPLAY_NAME] ) @@ -519,7 +519,7 @@ def _keep_event(event, entities_filter): domain = DOMAIN_HOMEKIT if not entity_id and domain: - entity_id = "%s." % (domain,) + entity_id = f"{domain}." return not entity_id or entities_filter(entity_id) @@ -530,7 +530,7 @@ def _entry_message_from_state(domain, state): if domain in ["device_tracker", "person"]: if state.state == STATE_NOT_HOME: return "is away" - return "is at {}".format(state.state) + return f"is at {state.state}" if domain == "sun": if state.state == sun.STATE_ABOVE_HORIZON: @@ -596,9 +596,9 @@ def _entry_message_from_state(domain, state): "vibration", ]: if state.state == STATE_ON: - return "detected {}".format(device_class) + return f"detected {device_class}" if state.state == STATE_OFF: - return "cleared (no {} detected)".format(device_class) + return f"cleared (no {device_class} detected)" if state.state == STATE_ON: # Future: combine groups and its entity entries ? @@ -607,4 +607,4 @@ def _entry_message_from_state(domain, state): if state.state == STATE_OFF: return "turned off" - return "changed to {}".format(state.state) + return f"changed to {state.state}" diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index ba92fb8a672..3601ee275b8 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -24,7 +24,7 @@ def setup(hass, config): """Set up the Logentries component.""" conf = config[DOMAIN] token = conf.get(CONF_TOKEN) - le_wh = "{}{}".format(DEFAULT_HOST, token) + le_wh = f"{DEFAULT_HOST}{token}" def logentries_event_listener(event): """Listen for new messages on the bus and sends them to Logentries.""" diff --git a/homeassistant/components/logi_circle/.translations/it.json b/homeassistant/components/logi_circle/.translations/it.json index 568bf79a40d..d7c1d9ba9de 100644 --- a/homeassistant/components/logi_circle/.translations/it.json +++ b/homeassistant/components/logi_circle/.translations/it.json @@ -12,10 +12,11 @@ "error": { "auth_error": "Autorizzazione API fallita.", "auth_timeout": "Timeout dell'autorizzazione durante la richiesta del token di accesso.", - "follow_link": "Segui il link e autenticati prima di premere Invio" + "follow_link": "Segui il link e autenticati prima di premere Invia" }, "step": { "auth": { + "description": "Segui il link qui sotto e Accetta l'accesso al tuo account Logi Circle, quindi torna indietro e premi Invia qui sotto. \n\n [Link]({authorization_url})", "title": "Autenticarsi con Logi Circle" }, "user": { diff --git a/homeassistant/components/logi_circle/.translations/pl.json b/homeassistant/components/logi_circle/.translations/pl.json index 2c155ffde61..5d8e6a0607d 100644 --- a/homeassistant/components/logi_circle/.translations/pl.json +++ b/homeassistant/components/logi_circle/.translations/pl.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Logi Circle.", - "external_error": "Wyst\u0105pi\u0142 wyj\u0105tek zewn\u0119trzny.", - "external_setup": "Logi Circle pomy\u015blnie skonfigurowano.", + "external_error": "Wyst\u0105pi\u0142 wyj\u0105tek z innego przep\u0142ywu.", + "external_setup": "Logi Circle zosta\u0142o pomy\u015blnie skonfigurowane z innego przep\u0142ywu.", "no_flows": "Musisz skonfigurowa\u0107 Logi Circle, zanim b\u0119dziesz m\u00f3g\u0142 si\u0119 z nim uwierzytelni\u0107. [Przeczytaj instrukcj\u0119](https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { @@ -12,12 +12,12 @@ "error": { "auth_error": "Autoryzacja API nie powiod\u0142a si\u0119.", "auth_timeout": "Up\u0142yn\u0105\u0142 limit czasu \u017c\u0105dania tokena dost\u0119pu.", - "follow_link": "Prosz\u0119 klikn\u0105\u0107 link i uwierzytelni\u0107 przed naci\u015bni\u0119ciem przycisku Prze\u015blij." + "follow_link": "Post\u0119puj zgodnie z linkiem i uwierzytelnij si\u0119 przed naci\u015bni\u0119ciem przycisku Prze\u015blij." }, "step": { "auth": { - "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do swojego konta Logi Circle, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", - "title": "Uwierzytelnienie Logi Circle" + "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do konta Logi Circle, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", + "title": "Uwierzytelnij za pomoc\u0105 Logi Circle" }, "user": { "data": { diff --git a/homeassistant/components/logi_circle/__init__.py b/homeassistant/components/logi_circle/__init__.py index 6f073a064f1..12484a655d6 100644 --- a/homeassistant/components/logi_circle/__init__.py +++ b/homeassistant/components/logi_circle/__init__.py @@ -156,7 +156,7 @@ async def async_setup_entry(hass, entry): except asyncio.TimeoutError: # The TimeoutError exception object returns nothing when casted to a # string, so we'll handle it separately. - err = "{}s timeout exceeded when connecting to Logi Circle API".format(_TIMEOUT) + err = f"{_TIMEOUT}s timeout exceeded when connecting to Logi Circle API" hass.components.persistent_notification.create( "Error: {}
" "You will need to restart hass after fixing." diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index f81db9ba171..2a25c5f00a4 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -179,7 +179,7 @@ class LogiCircleFlowHandler(config_entries.ConfigFlow): account_id = (await logi_session.account)["accountId"] await logi_session.close() return self.async_create_entry( - title="Logi Circle ({})".format(account_id), + title=f"Logi Circle ({account_id})", data={ CONF_CLIENT_ID: client_id, CONF_CLIENT_SECRET: client_secret, diff --git a/homeassistant/components/logi_circle/sensor.py b/homeassistant/components/logi_circle/sensor.py index f229250ea09..fc5ad7155b4 100644 --- a/homeassistant/components/logi_circle/sensor.py +++ b/homeassistant/components/logi_circle/sensor.py @@ -49,7 +49,7 @@ class LogiSensor(Entity): """Initialize a sensor for Logi Circle camera.""" self._sensor_type = sensor_type self._camera = camera - self._id = "{}-{}".format(self._camera.mac_address, self._sensor_type) + self._id = f"{self._camera.mac_address}-{self._sensor_type}" self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[2]) self._name = "{0} {1}".format( self._camera.name, SENSOR_TYPES.get(self._sensor_type)[0] diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index e05967a91fd..86129eafc02 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -34,7 +34,7 @@ SENSOR_PM2_5 = "P2" SENSOR_PRESSURE = "pressure" SENSOR_TEMPERATURE = "temperature" -TOPIC_UPDATE = "{0}_data_update".format(DOMAIN) +TOPIC_UPDATE = f"{DOMAIN}_data_update" VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m3" diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 9f4d81df044..de3ca40fd1d 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -114,7 +114,7 @@ class LutronDevice(Entity): @property def name(self): """Return the name of the device.""" - return "{} {}".format(self._area_name, self._lutron_device.name) + return f"{self._area_name} {self._lutron_device.name}" @property def should_poll(self): @@ -132,7 +132,7 @@ class LutronButton: def __init__(self, hass, keypad, button): """Register callback for activity on the button.""" - name = "{}: {}".format(keypad.name, button.name) + name = f"{keypad.name}: {button.name}" self._hass = hass self._has_release_event = ( button.button_type is not None and "RaiseLower" in button.button_type diff --git a/homeassistant/components/lyft/sensor.py b/homeassistant/components/lyft/sensor.py index 0bf739b040c..339b996c5d8 100644 --- a/homeassistant/components/lyft/sensor.py +++ b/homeassistant/components/lyft/sensor.py @@ -85,7 +85,7 @@ class LyftSensor(Entity): self._sensortype = sensorType self._name = "{} {}".format(self._product["display_name"], self._sensortype) if "lyft" not in self._name.lower(): - self._name = "Lyft{}".format(self._name) + self._name = f"Lyft{self._name}" if self._sensortype == "time": self._unit_of_measurement = "min" elif self._sensortype == "price": diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 61abb9788c5..66ab87a6569 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -108,10 +108,10 @@ class MagicSeaweedSensor(Entity): def name(self): """Return the name of the sensor.""" if self.hour is None and "forecast" in self.type: - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" if self.hour is None: - return "Current {} {}".format(self.client_name, self._name) - return "{} {} {}".format(self.hour, self.client_name, self._name) + return f"Current {self.client_name} {self._name}" + return f"{self.hour} {self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/mailgun/__init__.py b/homeassistant/components/mailgun/__init__.py index 87bbe6bee07..4bcca0848f4 100644 --- a/homeassistant/components/mailgun/__init__.py +++ b/homeassistant/components/mailgun/__init__.py @@ -19,7 +19,7 @@ CONF_SANDBOX = "sandbox" DEFAULT_SANDBOX = False -MESSAGE_RECEIVED = "{}_message_received".format(DOMAIN) +MESSAGE_RECEIVED = f"{DOMAIN}_message_received" CONFIG_SCHEMA = vol.Schema( { @@ -75,7 +75,7 @@ async def verify_webhook(hass, token=None, timestamp=None, signature=None): hmac_digest = hmac.new( key=bytes(hass.data[DOMAIN][CONF_API_KEY], "utf-8"), - msg=bytes("{}{}".format(timestamp, token), "utf-8"), + msg=bytes(f"{timestamp}{token}", "utf-8"), digestmod=hashlib.sha256, ).hexdigest() diff --git a/homeassistant/components/marytts/tts.py b/homeassistant/components/marytts/tts.py index 9f64c088b4a..e5088c5b2df 100644 --- a/homeassistant/components/marytts/tts.py +++ b/homeassistant/components/marytts/tts.py @@ -74,7 +74,7 @@ class MaryTTSProvider(Provider): try: with async_timeout.timeout(10): - url = "http://{}:{}/process?".format(self._host, self._port) + url = f"http://{self._host}:{self._port}/process?" audio = self._codec.upper() if audio == "WAV": diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index f93fe5e77a9..419d4b72864 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/components/media_extractor", "requirements": [ - "youtube_dl==2019.08.13" + "youtube_dl==2019.09.01" ], "dependencies": [ "media_player" diff --git a/homeassistant/components/met/.translations/hu.json b/homeassistant/components/met/.translations/hu.json new file mode 100644 index 00000000000..3b34d8f6354 --- /dev/null +++ b/homeassistant/components/met/.translations/hu.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "Elhelyezked\u00e9s" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/it.json b/homeassistant/components/met/.translations/it.json new file mode 100644 index 00000000000..a1cfd12e8cd --- /dev/null +++ b/homeassistant/components/met/.translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "La posizione esiste gi\u00e0" + }, + "step": { + "user": { + "data": { + "elevation": "Altitudine", + "latitude": "Latitudine", + "longitude": "Longitudine", + "name": "Nome" + }, + "description": "Meteorologisk institutt", + "title": "Posizione" + } + }, + "title": "Met.no" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/ko.json b/homeassistant/components/met/.translations/ko.json index 6900458ba60..81a98b9754f 100644 --- a/homeassistant/components/met/.translations/ko.json +++ b/homeassistant/components/met/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" + "name_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index d298b1e3b07..d92d28d9484 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/sl.json b/homeassistant/components/met/.translations/sl.json index 5dffbe133e7..71ffdaf8509 100644 --- a/homeassistant/components/met/.translations/sl.json +++ b/homeassistant/components/met/.translations/sl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Ime \u017ee obstaja" + "name_exists": "Lokacija \u017ee obstaja" }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/zh-Hant.json b/homeassistant/components/met/.translations/zh-Hant.json index c49c90ee6e4..de7c34ffc87 100644 --- a/homeassistant/components/met/.translations/zh-Hant.json +++ b/homeassistant/components/met/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728" + "name_exists": "\u8a72\u5ea7\u6a19\u5df2\u5b58\u5728" }, "step": { "user": { diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index d6460fd6e5a..cfcd78400bd 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -4,70 +4,17 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS +from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.util import Throttle +from .const import DOMAIN, CONF_CITY, SENSOR_TYPES, DATA_METEO_FRANCE + _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by Météo-France" - -CONF_CITY = "city" - -DATA_METEO_FRANCE = "data_meteo_france" -DEFAULT_WEATHER_CARD = True -DOMAIN = "meteo_france" - SCAN_INTERVAL = datetime.timedelta(minutes=5) -SENSOR_TYPES = { - "rain_chance": ["Rain chance", "%"], - "freeze_chance": ["Freeze chance", "%"], - "thunder_chance": ["Thunder chance", "%"], - "snow_chance": ["Snow chance", "%"], - "weather": ["Weather", None], - "wind_speed": ["Wind Speed", "km/h"], - "next_rain": ["Next rain", "min"], - "temperature": ["Temperature", TEMP_CELSIUS], - "uv": ["UV", None], - "weather_alert": ["Weather Alert", None], -} - -CONDITION_CLASSES = { - "clear-night": ["Nuit Claire"], - "cloudy": ["Très nuageux"], - "fog": ["Brume ou bancs de brouillard", "Brouillard", "Brouillard givrant"], - "hail": ["Risque de grêle"], - "lightning": ["Risque d'orages", "Orages"], - "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], - "partlycloudy": ["Ciel voilé", "Ciel voilé nuit", "Éclaircies"], - "pouring": ["Pluie forte"], - "rainy": [ - "Bruine / Pluie faible", - "Bruine", - "Pluie faible", - "Pluies éparses / Rares averses", - "Pluies éparses", - "Rares averses", - "Pluie / Averses", - "Averses", - "Pluie", - ], - "snowy": [ - "Neige / Averses de neige", - "Neige", - "Averses de neige", - "Neige forte", - "Quelques flocons", - ], - "snowy-rainy": ["Pluie et neige", "Pluie verglaçante"], - "sunny": ["Ensoleillé"], - "windy": [], - "windy-variant": [], - "exceptional": [], -} - def has_all_unique_cities(value): """Validate that all cities are unique.""" diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py new file mode 100644 index 00000000000..223aca20bac --- /dev/null +++ b/homeassistant/components/meteo_france/const.py @@ -0,0 +1,112 @@ +"""Meteo-France component constants.""" + +from homeassistant.const import TEMP_CELSIUS + +DOMAIN = "meteo_france" +DATA_METEO_FRANCE = "data_meteo_france" +ATTRIBUTION = "Data provided by Météo-France" + +CONF_CITY = "city" + +DEFAULT_WEATHER_CARD = True + +SENSOR_TYPE_NAME = "name" +SENSOR_TYPE_UNIT = "unit" +SENSOR_TYPE_ICON = "icon" +SENSOR_TYPE_CLASS = "device_class" +SENSOR_TYPES = { + "rain_chance": { + SENSOR_TYPE_NAME: "Rain chance", + SENSOR_TYPE_UNIT: "%", + SENSOR_TYPE_ICON: "mdi:weather-rainy", + SENSOR_TYPE_CLASS: None, + }, + "freeze_chance": { + SENSOR_TYPE_NAME: "Freeze chance", + SENSOR_TYPE_UNIT: "%", + SENSOR_TYPE_ICON: "mdi:snowflake", + SENSOR_TYPE_CLASS: None, + }, + "thunder_chance": { + SENSOR_TYPE_NAME: "Thunder chance", + SENSOR_TYPE_UNIT: "%", + SENSOR_TYPE_ICON: "mdi:weather-lightning", + SENSOR_TYPE_CLASS: None, + }, + "snow_chance": { + SENSOR_TYPE_NAME: "Snow chance", + SENSOR_TYPE_UNIT: "%", + SENSOR_TYPE_ICON: "mdi:weather-snowy", + SENSOR_TYPE_CLASS: None, + }, + "weather": { + SENSOR_TYPE_NAME: "Weather", + SENSOR_TYPE_UNIT: None, + SENSOR_TYPE_ICON: "mdi:weather-partly-cloudy", + SENSOR_TYPE_CLASS: None, + }, + "wind_speed": { + SENSOR_TYPE_NAME: "Wind Speed", + SENSOR_TYPE_UNIT: "km/h", + SENSOR_TYPE_ICON: "mdi:weather-windy", + SENSOR_TYPE_CLASS: None, + }, + "next_rain": { + SENSOR_TYPE_NAME: "Next rain", + SENSOR_TYPE_UNIT: "min", + SENSOR_TYPE_ICON: "mdi:weather-rainy", + SENSOR_TYPE_CLASS: None, + }, + "temperature": { + SENSOR_TYPE_NAME: "Temperature", + SENSOR_TYPE_UNIT: TEMP_CELSIUS, + SENSOR_TYPE_ICON: "mdi:thermometer", + SENSOR_TYPE_CLASS: "temperature", + }, + "uv": { + SENSOR_TYPE_NAME: "UV", + SENSOR_TYPE_UNIT: None, + SENSOR_TYPE_ICON: "mdi:sunglasses", + SENSOR_TYPE_CLASS: None, + }, + "weather_alert": { + SENSOR_TYPE_NAME: "Weather Alert", + SENSOR_TYPE_UNIT: None, + SENSOR_TYPE_ICON: "mdi:weather-cloudy-alert", + SENSOR_TYPE_CLASS: None, + }, +} + +CONDITION_CLASSES = { + "clear-night": ["Nuit Claire"], + "cloudy": ["Très nuageux"], + "fog": ["Brume ou bancs de brouillard", "Brouillard", "Brouillard givrant"], + "hail": ["Risque de grêle"], + "lightning": ["Risque d'orages", "Orages"], + "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], + "partlycloudy": ["Ciel voilé", "Ciel voilé nuit", "Éclaircies"], + "pouring": ["Pluie forte"], + "rainy": [ + "Bruine / Pluie faible", + "Bruine", + "Pluie faible", + "Pluies éparses / Rares averses", + "Pluies éparses", + "Rares averses", + "Pluie / Averses", + "Averses", + "Pluie", + ], + "snowy": [ + "Neige / Averses de neige", + "Neige", + "Averses de neige", + "Neige forte", + "Quelques flocons", + ], + "snowy-rainy": ["Pluie et neige", "Pluie verglaçante"], + "sunny": ["Ensoleillé"], + "windy": [], + "windy-variant": [], + "exceptional": [], +} diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 95113a60cd3..8c2bd32048f 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -4,7 +4,16 @@ import logging from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity -from . import ATTRIBUTION, CONF_CITY, DATA_METEO_FRANCE, SENSOR_TYPES +from .const import ( + ATTRIBUTION, + CONF_CITY, + DATA_METEO_FRANCE, + SENSOR_TYPES, + SENSOR_TYPE_ICON, + SENSOR_TYPE_NAME, + SENSOR_TYPE_UNIT, + SENSOR_TYPE_CLASS, +) _LOGGER = logging.getLogger(__name__) @@ -44,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): alert_watcher = None else: _LOGGER.info( - "Weather alert watcher added for %s" "in department %s", + "Weather alert watcher added for %s in department %s", city, datas["dept"], ) @@ -79,7 +88,7 @@ class MeteoFranceSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._data["name"], SENSOR_TYPES[self._condition][0]) + return f"{self._data['name']} {SENSOR_TYPES[self._condition][SENSOR_TYPE_NAME]}" @property def state(self): @@ -111,7 +120,17 @@ class MeteoFranceSensor(Entity): @property def unit_of_measurement(self): """Return the unit of measurement.""" - return SENSOR_TYPES[self._condition][1] + return SENSOR_TYPES[self._condition][SENSOR_TYPE_UNIT] + + @property + def icon(self): + """Return the icon.""" + return SENSOR_TYPES[self._condition][SENSOR_TYPE_ICON] + + @property + def device_class(self): + """Return the device class of the sensor.""" + return SENSOR_TYPES[self._condition][SENSOR_TYPE_CLASS] def update(self): """Fetch new state data for the sensor.""" diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index 9a861d13c2e..00da55809ff 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -12,7 +12,7 @@ from homeassistant.components.weather import ( import homeassistant.util.dt as dt_util from homeassistant.const import TEMP_CELSIUS -from . import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE +from .const import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index e90d6fdc4c2..bb7a64005ce 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -83,7 +83,7 @@ class MetOfficeWeather(WeatherEntity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self.site.name) + return f"{self._name} {self.site.name}" @property def condition(self): diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index ff06e8815ed..3536c788bb9 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -115,8 +115,8 @@ class MicrosoftProvider(Provider): self._gender = gender self._type = ttype self._output = DEFAULT_OUTPUT - self._rate = "{}%".format(rate) - self._volume = "{}%".format(volume) + self._rate = f"{rate}%" + self._volume = f"{volume}%" self._pitch = pitch self._contour = contour self.name = "Microsoft" diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 6c2fc5ae6bf..5d0c50e536a 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -92,7 +92,7 @@ async def async_setup(hass, config): g_id = slugify(name) try: - await face.call_api("put", "persongroups/{0}".format(g_id), {"name": name}) + await face.call_api("put", f"persongroups/{g_id}", {"name": name}) face.store[g_id] = {} entities[g_id] = MicrosoftFaceGroupEntity(hass, face, g_id, name) @@ -109,7 +109,7 @@ async def async_setup(hass, config): g_id = slugify(service.data[ATTR_NAME]) try: - await face.call_api("delete", "persongroups/{0}".format(g_id)) + await face.call_api("delete", f"persongroups/{g_id}") face.store.pop(g_id) entity = entities.pop(g_id) @@ -126,7 +126,7 @@ async def async_setup(hass, config): g_id = service.data[ATTR_GROUP] try: - await face.call_api("post", "persongroups/{0}/train".format(g_id)) + await face.call_api("post", f"persongroups/{g_id}/train") except HomeAssistantError as err: _LOGGER.error("Can't train group '%s' with error: %s", g_id, err) @@ -141,7 +141,7 @@ async def async_setup(hass, config): try: user_data = await face.call_api( - "post", "persongroups/{0}/persons".format(g_id), {"name": name} + "post", f"persongroups/{g_id}/persons", {"name": name} ) face.store[g_id][name] = user_data["personId"] @@ -160,9 +160,7 @@ async def async_setup(hass, config): p_id = face.store[g_id].get(name) try: - await face.call_api( - "delete", "persongroups/{0}/persons/{1}".format(g_id, p_id) - ) + await face.call_api("delete", f"persongroups/{g_id}/persons/{p_id}") face.store[g_id].pop(name) await entities[g_id].async_update_ha_state() @@ -186,7 +184,7 @@ async def async_setup(hass, config): await face.call_api( "post", - "persongroups/{0}/persons/{1}/persistedFaces".format(g_id, p_id), + f"persongroups/{g_id}/persons/{p_id}/persistedFaces", image.content, binary=True, ) @@ -218,7 +216,7 @@ class MicrosoftFaceGroupEntity(Entity): @property def entity_id(self): """Return entity id.""" - return "{0}.{1}".format(DOMAIN, self._id) + return f"{DOMAIN}.{self._id}" @property def state(self): @@ -249,7 +247,7 @@ class MicrosoftFace: self.websession = async_get_clientsession(hass) self.timeout = timeout self._api_key = api_key - self._server_url = "https://{0}.{1}".format(server_loc, FACE_API_URL) + self._server_url = f"https://{server_loc}.{FACE_API_URL}" self._store = {} self._entities = entities @@ -270,9 +268,7 @@ class MicrosoftFace: self.hass, self, g_id, group["name"] ) - persons = await self.call_api( - "get", "persongroups/{0}/persons".format(g_id) - ) + persons = await self.call_api("get", f"persongroups/{g_id}/persons") for person in persons: self._store[g_id][person["name"]] = person["personId"] diff --git a/homeassistant/components/microsoft_face_detect/image_processing.py b/homeassistant/components/microsoft_face_detect/image_processing.py index 243b4533938..c10f7edf9db 100644 --- a/homeassistant/components/microsoft_face_detect/image_processing.py +++ b/homeassistant/components/microsoft_face_detect/image_processing.py @@ -30,7 +30,7 @@ def validate_attributes(list_attributes): """Validate face attributes.""" for attr in list_attributes: if attr not in SUPPORTED_ATTRIBUTES: - raise vol.Invalid("Invalid attribute {0}".format(attr)) + raise vol.Invalid(f"Invalid attribute {attr}") return list_attributes diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index d4e7a333acf..c7ef2b89611 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -3,6 +3,7 @@ "name": "Miflora", "documentation": "https://www.home-assistant.io/components/miflora", "requirements": [ + "bluepy==1.1.4", "miflora==0.4.0" ], "dependencies": [], diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 999dc473c60..28020a80175 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -85,7 +85,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= prefix = config.get(CONF_NAME) if prefix: - name = "{} {}".format(prefix, name) + name = f"{prefix} {name}" devs.append( MiFloraSensor(poller, parameter, name, unit, icon, force_update, median) @@ -157,7 +157,7 @@ class MiFloraSensor(Entity): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except IOError as ioerr: + except OSError as ioerr: _LOGGER.info("Polling error %s", ioerr) return except BluetoothBackendException as bterror: diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index cede3a7aad5..d411d913082 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -166,7 +166,7 @@ def setup(hass, config): def get_minio_endpoint(host: str, port: int) -> str: """Create minio endpoint from host and port.""" - return "{}:{}".format(host, port) + return f"{host}:{port}" class QueueListener(threading.Thread): diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index c9b7837c683..adeba48dbc8 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -94,7 +94,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): prefix = config.get(CONF_NAME) if prefix: - name = "{} {}".format(prefix, name) + name = f"{prefix} {name}" devs.append( MiTempBtSensor(poller, parameter, device, name, unit, force_update, median) @@ -157,7 +157,7 @@ class MiTempBtSensor(Entity): try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) - except IOError as ioerr: + except OSError as ioerr: _LOGGER.warning("Polling error %s", ioerr) return except BluetoothBackendException as bterror: diff --git a/homeassistant/components/mobile_app/.translations/it.json b/homeassistant/components/mobile_app/.translations/it.json index 049e551d19b..37c0deb9c2d 100644 --- a/homeassistant/components/mobile_app/.translations/it.json +++ b/homeassistant/components/mobile_app/.translations/it.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "install_app": "Apri l'app per dispositivi mobili per configurare l'integrazione con Home Assistant. Vedi [i documenti] ( {apps_url} ) per un elenco di app compatibili." + "install_app": "Apri l'App per dispositivi mobili per configurare l'integrazione con Home Assistant. Vedi [i documenti]({apps_url}) per un elenco di app compatibili." }, "step": { "confirm": { - "description": "Vuoi configurare il componente Mobile App?", + "description": "Si desidera configurare il componente App per dispositivi mobili?", "title": "App per dispositivi mobili" } }, diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index c0ea297edc1..975c4c16c32 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect( hass, - "{}_{}_register".format(DOMAIN, ENTITY_TYPE), + f"{DOMAIN}_{ENTITY_TYPE}_register", partial(handle_sensor_registration, webhook_id), ) diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py index c207475dc3c..27cb9934b18 100644 --- a/homeassistant/components/mobile_app/entity.py +++ b/homeassistant/components/mobile_app/entity.py @@ -21,7 +21,7 @@ from .helpers import device_info def sensor_id(webhook_id, unique_id): """Return a unique sensor ID.""" - return "{}_{}".format(webhook_id, unique_id) + return f"{webhook_id}_{unique_id}" class MobileAppEntity(Entity): diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index d6d2247736c..b96a6f1e2f0 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -53,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect( hass, - "{}_{}_register".format(DOMAIN, ENTITY_TYPE), + f"{DOMAIN}_{ENTITY_TYPE}_register", partial(handle_sensor_registration, webhook_id), ) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index ecbd08375e0..f95d5b993f0 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -220,13 +220,13 @@ async def handle_webhook( unique_id = data[ATTR_SENSOR_UNIQUE_ID] - unique_store_key = "{}_{}".format(webhook_id, unique_id) + unique_store_key = f"{webhook_id}_{unique_id}" if unique_store_key in hass.data[DOMAIN][entity_type]: _LOGGER.error("Refusing to re-register existing sensor %s!", unique_id) return error_response( ERR_SENSOR_DUPLICATE_UNIQUE_ID, - "{} {} already exists!".format(entity_type, unique_id), + f"{entity_type} {unique_id} already exists!", status=409, ) @@ -257,13 +257,13 @@ async def handle_webhook( unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] - unique_store_key = "{}_{}".format(webhook_id, unique_id) + unique_store_key = f"{webhook_id}_{unique_id}" if unique_store_key not in hass.data[DOMAIN][entity_type]: _LOGGER.error( "Refusing to update non-registered sensor: %s", unique_store_key ) - err_msg = "{} {} is not registered".format(entity_type, unique_id) + err_msg = f"{entity_type} {unique_id} is not registered" resp[unique_id] = { "success": False, "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg}, diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index a7b0b8c2d0f..899908c34bd 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -50,7 +50,7 @@ class MochadLight(Light): self._controller = ctrl self._address = dev[CONF_ADDRESS] - self._name = dev.get(CONF_NAME, "x10_light_dev_{}".format(self._address)) + self._name = dev.get(CONF_NAME, f"x10_light_dev_{self._address}") self._comm_type = dev.get(mochad.CONF_COMM_TYPE, "pl") self.light = device.Device(ctrl, self._address, comm_type=self._comm_type) self._brightness = 0 @@ -95,12 +95,12 @@ class MochadLight(Light): if self._brightness > brightness: bdelta = self._brightness - brightness mochad_brightness = self._calculate_brightness_value(bdelta) - self.light.send_cmd("dim {}".format(mochad_brightness)) + self.light.send_cmd(f"dim {mochad_brightness}") self._controller.read_data() elif self._brightness < brightness: bdelta = brightness - self._brightness mochad_brightness = self._calculate_brightness_value(bdelta) - self.light.send_cmd("bright {}".format(mochad_brightness)) + self.light.send_cmd(f"bright {mochad_brightness}") self._controller.read_data() def turn_on(self, **kwargs): @@ -109,7 +109,7 @@ class MochadLight(Light): with mochad.REQ_LOCK: if self._brightness_levels > 32: out_brightness = self._calculate_brightness_value(brightness) - self.light.send_cmd("xdim {}".format(out_brightness)) + self.light.send_cmd(f"xdim {out_brightness}") self._controller.read_data() else: self.light.send_cmd("on") diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 22b871cea20..64b45b03c95 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -170,7 +170,7 @@ class ModbusThermostat(ClimateDevice): [x.to_bytes(2, byteorder="big") for x in result.registers] ) val = struct.unpack(self._structure, byte_string)[0] - register_value = format(val, ".{}f".format(self._precision)) + register_value = format(val, f".{self._precision}f") return register_value def write_register(self, register, value): diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 4fc9fb808c6..1a5c71812d6 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -54,7 +54,7 @@ def number(value: Any) -> Union[int, float]: value = float(value) return value except (TypeError, ValueError): - raise vol.Invalid("invalid number {}".format(value)) + raise vol.Invalid(f"invalid number {value}") PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py index 686b927b515..857dbab2a3b 100644 --- a/homeassistant/components/mopar/__init__.py +++ b/homeassistant/components/mopar/__init__.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import track_time_interval DOMAIN = "mopar" -DATA_UPDATED = "{}_data_updated".format(DOMAIN) +DATA_UPDATED = f"{DOMAIN}_data_updated" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index d616ec6d1e8..ae96704be58 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -56,7 +56,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "{}:{}".format(host, port) + url = f"{host}:{port}" add_entities([MpcHcDevice(name, url)], True) @@ -73,9 +73,7 @@ class MpcHcDevice(MediaPlayerDevice): def update(self): """Get the latest details.""" try: - response = requests.get( - "{}/variables.html".format(self._url), data=None, timeout=3 - ) + response = requests.get(f"{self._url}/variables.html", data=None, timeout=3) mpchc_variables = re.findall(r'

(.+?)

', response.text) @@ -88,7 +86,7 @@ class MpcHcDevice(MediaPlayerDevice): """Send a command to MPC-HC via its window message ID.""" try: params = {"wm_command": command_id} - requests.get("{}/command.html".format(self._url), params=params, timeout=3) + requests.get(f"{self._url}/command.html", params=params, timeout=3) except requests.exceptions.RequestException: _LOGGER.error( "Could not send command %d to MPC-HC at: %s", command_id, self._url diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 0d924cdd1d2..c19f8f49226 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -211,7 +211,7 @@ class MpdDevice(MediaPlayerDevice): if title is None: return name - return "{}: {}".format(name, title) + return f"{name}: {title}" @property def media_artist(self): diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 75552d1d14b..8d83cd0cc2b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -10,7 +10,7 @@ import os import socket import ssl import time -from typing import Any, Callable, List, Optional, Union, cast # noqa: F401 +from typing import Any, Callable, List, Optional, Union import attr import requests.certs @@ -479,7 +479,7 @@ async def _async_setup_server(hass: HomeAssistantType, config: ConfigType): This method is a coroutine. """ - conf = config.get(DOMAIN, {}) # type: ConfigType + conf: ConfigType = config.get(DOMAIN, {}) success, broker_config = await server.async_start( hass, conf.get(CONF_PASSWORD), conf.get(CONF_EMBEDDED) @@ -502,16 +502,16 @@ async def _async_setup_discovery( _LOGGER.error("Unable to load MQTT discovery") return False - success = await discovery.async_start( + success: bool = await discovery.async_start( hass, conf[CONF_DISCOVERY_PREFIX], hass_config, config_entry - ) # type: bool + ) return success async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Start the MQTT protocol service.""" - conf = config.get(DOMAIN) # type: Optional[ConfigType] + conf: Optional[ConfigType] = config.get(DOMAIN) # We need this because discovery can cause components to be set up and # otherwise it will not load the users config. @@ -621,7 +621,7 @@ async def async_setup_entry(hass, entry): birth_message = None # Be able to override versions other than TLSv1.0 under Python3.6 - conf_tls_version = conf.get(CONF_TLS_VERSION) # type: str + conf_tls_version: str = conf.get(CONF_TLS_VERSION) if conf_tls_version == "1.2": tls_version = ssl.PROTOCOL_TLSv1_2 elif conf_tls_version == "1.1": @@ -655,7 +655,7 @@ async def async_setup_entry(hass, entry): tls_version=tls_version, ) - result = await hass.data[DATA_MQTT].async_connect() # type: str + result: str = await hass.data[DATA_MQTT].async_connect() if result == CONNECTION_FAILED: return False @@ -671,11 +671,11 @@ async def async_setup_entry(hass, entry): async def async_publish_service(call: ServiceCall): """Handle MQTT publish service calls.""" - msg_topic = call.data[ATTR_TOPIC] # type: str + msg_topic: str = call.data[ATTR_TOPIC] payload = call.data.get(ATTR_PAYLOAD) payload_template = call.data.get(ATTR_PAYLOAD_TEMPLATE) - qos = call.data[ATTR_QOS] # type: int - retain = call.data[ATTR_RETAIN] # type: bool + qos: int = call.data[ATTR_QOS] + retain: bool = call.data[ATTR_RETAIN] if payload_template is not None: try: payload = template.Template(payload_template, hass).async_render() @@ -741,14 +741,14 @@ class MQTT: self.broker = broker self.port = port self.keepalive = keepalive - self.subscriptions = [] # type: List[Subscription] + self.subscriptions: List[Subscription] = [] self.birth_message = birth_message self.connected = False - self._mqttc = None # type: mqtt.Client + self._mqttc: mqtt.Client = None self._paho_lock = asyncio.Lock() if protocol == PROTOCOL_31: - proto = mqtt.MQTTv31 # type: int + proto: int = mqtt.MQTTv31 else: proto = mqtt.MQTTv311 @@ -796,7 +796,7 @@ class MQTT: This method is a coroutine. """ - result = None # type: int + result: int = None try: result = await self.hass.async_add_job( self._mqttc.connect, self.broker, self.port, self.keepalive @@ -870,7 +870,7 @@ class MQTT: This method is a coroutine. """ async with self._paho_lock: - result = None # type: int + result: int = None result, _ = await self.hass.async_add_job(self._mqttc.unsubscribe, topic) _raise_on_error(result) @@ -879,7 +879,7 @@ class MQTT: _LOGGER.debug("Subscribing to %s", topic) async with self._paho_lock: - result = None # type: int + result: int = None result, _ = await self.hass.async_add_job(self._mqttc.subscribe, topic, qos) _raise_on_error(result) @@ -928,7 +928,7 @@ class MQTT: if not _match_topic(subscription.topic, msg.topic): continue - payload = msg.payload # type: SubscribePayloadType + payload: SubscribePayloadType = msg.payload if subscription.encoding is not None: try: payload = msg.payload.decode(subscription.encoding) @@ -1077,7 +1077,7 @@ class MqttAvailability(Entity): def __init__(self, config: dict) -> None: """Initialize the availability mixin.""" self._availability_sub_state = None - self._available = False # type: bool + self._available = False self._avail_config = config diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 1df635bbde4..f3ae36c5746 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -7,13 +7,19 @@ import voluptuous as vol from homeassistant.components import camera, mqtt from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, CONF_DEVICE from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ATTR_DISCOVERY_HASH, CONF_UNIQUE_ID, MqttDiscoveryUpdate, subscription +from . import ( + ATTR_DISCOVERY_HASH, + CONF_UNIQUE_ID, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -26,6 +32,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_UNIQUE_ID): cv.string, + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, } ) @@ -45,7 +52,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): try: discovery_hash = discovery_payload.pop(ATTR_DISCOVERY_HASH) config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity(config, async_add_entities, discovery_hash) + await _async_setup_entity( + config, async_add_entities, config_entry, discovery_hash + ) except Exception: if discovery_hash: clear_discovery_hash(hass, discovery_hash) @@ -56,15 +65,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -async def _async_setup_entity(config, async_add_entities, discovery_hash=None): +async def _async_setup_entity( + config, async_add_entities, config_entry=None, discovery_hash=None +): """Set up the MQTT Camera.""" - async_add_entities([MqttCamera(config, discovery_hash)]) + async_add_entities([MqttCamera(config, config_entry, discovery_hash)]) -class MqttCamera(MqttDiscoveryUpdate, Camera): +class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): """representation of a MQTT camera.""" - def __init__(self, config, discovery_hash): + def __init__(self, config, config_entry, discovery_hash): """Initialize the MQTT Camera.""" self._config = config self._unique_id = config.get(CONF_UNIQUE_ID) @@ -73,8 +84,11 @@ class MqttCamera(MqttDiscoveryUpdate, Camera): self._qos = 0 self._last_image = None + device_config = config.get(CONF_DEVICE) + Camera.__init__(self) MqttDiscoveryUpdate.__init__(self, discovery_hash, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): """Subscribe MQTT events.""" @@ -85,6 +99,7 @@ class MqttCamera(MqttDiscoveryUpdate, Camera): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._config = config + await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index d611b8db13e..f393c315793 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -86,7 +86,7 @@ async def async_start( """Process the received message.""" payload = msg.payload topic = msg.topic - topic_trimmed = topic.replace("{}/".format(discovery_topic), "", 1) + topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1) match = TOPIC_MATCHER.match(topic_trimmed) if not match: @@ -134,9 +134,7 @@ async def async_start( if payload: # Attach MQTT topic to the payload, used for debug prints - setattr( - payload, "__configuration_source__", "MQTT (topic: '{}')".format(topic) - ) + setattr(payload, "__configuration_source__", f"MQTT (topic: '{topic}')") if CONF_PLATFORM in payload and "schema" not in payload: platform = payload[CONF_PLATFORM] diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 20be0dcf89c..f2fa8f8da66 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -339,7 +339,7 @@ class MqttVacuum( elif self._cleaning: self._status = "Cleaning" elif self._error: - self._status = "Error: {}".format(self._error) + self._status = f"Error: {self._error}" else: self._status = "Stopped" @@ -360,7 +360,7 @@ class MqttVacuum( self.hass, self._sub_state, { - "topic{}".format(i): { + f"topic{i}": { "topic": topic, "msg_callback": message_received, "qos": self._qos, @@ -550,7 +550,7 @@ class MqttVacuum( mqtt.async_publish( self.hass, self._set_fan_speed_topic, fan_speed, self._qos, self._retain ) - self._status = "Setting fan to {}...".format(fan_speed) + self._status = f"Setting fan to {fan_speed}..." self.async_write_ha_state() async def async_send_command(self, command, params=None, **kwargs): @@ -566,5 +566,5 @@ class MqttVacuum( mqtt.async_publish( self.hass, self._send_command_topic, message, self._qos, self._retain ) - self._status = "Sending command {}...".format(message) + self._status = f"Sending command {message}..." self.async_write_ha_state() diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index 2bde87976bc..cbedd947843 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -56,7 +56,7 @@ def is_persistence_file(value): """Validate that persistence file path ends in either .pickle or .json.""" if value.endswith((".json", ".pickle")): return value - raise vol.Invalid("{} does not end in either `.json` or `.pickle`".format(value)) + raise vol.Invalid(f"{value} does not end in either `.json` or `.pickle`") def deprecated(key): @@ -138,7 +138,7 @@ def _get_mysensors_name(gateway, node_id, child_id): ), node_name, ) - return "{} {}".format(node_name, child_id) + return f"{node_name} {child_id}" @callback diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 28d49303835..366692205a7 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -44,7 +44,7 @@ def is_serial_port(value): ports = ("COM{}".format(idx + 1) for idx in range(256)) if value in ports: return value - raise vol.Invalid("{} is not a serial port".format(value)) + raise vol.Invalid(f"{value} is not a serial port") return cv.isdevice(value) diff --git a/homeassistant/components/mysensors/notify.py b/homeassistant/components/mysensors/notify.py index ac94a8559a1..99e731762df 100644 --- a/homeassistant/components/mysensors/notify.py +++ b/homeassistant/components/mysensors/notify.py @@ -26,7 +26,7 @@ class MySensorsNotificationDevice(mysensors.device.MySensorsDevice): def __repr__(self): """Return the representation.""" - return "".format(self.name) + return f"" class MySensorsNotificationService(BaseNotificationService): diff --git a/homeassistant/components/mystrom/binary_sensor.py b/homeassistant/components/mystrom/binary_sensor.py index 20d32be199e..ff0063a380e 100644 --- a/homeassistant/components/mystrom/binary_sensor.py +++ b/homeassistant/components/mystrom/binary_sensor.py @@ -41,19 +41,16 @@ class MyStromView(HomeAssistantView): if button_action is None: _LOGGER.error("Received unidentified message from myStrom button: %s", data) - return ( - "Received unidentified message: {}".format(data), - HTTP_UNPROCESSABLE_ENTITY, - ) + return (f"Received unidentified message: {data}", HTTP_UNPROCESSABLE_ENTITY) button_id = data[button_action] - entity_id = "{}.{}_{}".format(DOMAIN, button_id, button_action) + entity_id = f"{DOMAIN}.{button_id}_{button_action}" if entity_id not in self.buttons: _LOGGER.info( "New myStrom button/action detected: %s/%s", button_id, button_action ) self.buttons[entity_id] = MyStromBinarySensor( - "{}_{}".format(button_id, button_action) + f"{button_id}_{button_action}" ) self.add_entities([self.buttons[entity_id]]) else: diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index d7d824c244c..93fe285dcfd 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -127,7 +127,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): """Initialize the Neato Connected Vacuum.""" self.robot = robot self.neato = hass.data[NEATO_LOGIN] - self._name = "{}".format(self.robot.name) + self._name = f"{self.robot.name}" self._status_state = None self._clean_state = None self._state = None diff --git a/homeassistant/components/nello/lock.py b/homeassistant/components/nello/lock.py index 5ae8bb61968..3efe0a9cc5f 100644 --- a/homeassistant/components/nello/lock.py +++ b/homeassistant/components/nello/lock.py @@ -59,7 +59,7 @@ class NelloLock(LockDevice): location_id = self._nello_lock.location_id short_id = self._nello_lock.short_id address = self._nello_lock.address - self._name = "Nello {}".format(short_id) + self._name = f"Nello {short_id}" self._device_attrs = {ATTR_ADDRESS: address, ATTR_LOCATION_ID: location_id} # Process recent activity activity = self._nello_lock.activity diff --git a/homeassistant/components/nest/.translations/ca.json b/homeassistant/components/nest/.translations/ca.json index b242208791b..636568b96d3 100644 --- a/homeassistant/components/nest/.translations/ca.json +++ b/homeassistant/components/nest/.translations/ca.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un \u00fanic compte Nest.", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", - "authorize_url_timeout": "El temps d'espera m\u00e0xim per generar l'URL d'autoritzaci\u00f3 s'ha esgotat.", + "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", "no_flows": "Necessites configurar Nest abans de poder autenticar-t'hi. Llegeix les [instruccions](https://www.home-assistant.io/components/nest/)." }, "error": { diff --git a/homeassistant/components/nest/.translations/zh-Hant.json b/homeassistant/components/nest/.translations/zh-Hant.json index 6b9dbdb19b1..c477557e7ba 100644 --- a/homeassistant/components/nest/.translations/zh-Hant.json +++ b/homeassistant/components/nest/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Nest \u5e33\u865f\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", - "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Nest \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/nest/\uff09\u3002" + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Nest \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/nest/)\u3002" }, "error": { "internal_error": "\u8a8d\u8b49\u78bc\u5167\u90e8\u932f\u8aa4", diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index b7033bbfd63..cf1ba36aa89 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -405,7 +405,7 @@ class NestSensorDevice(Entity): @property def unique_id(self): """Return unique id based on device serial and variable.""" - return "{}-{}".format(self.device.serial, self.variable) + return f"{self.device.serial}-{self.variable}" @property def device_info(self): diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index d335acc2bf1..0f3ae7da710 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -143,12 +143,12 @@ class NestActivityZoneSensor(NestBinarySensor): """Initialize the sensor.""" super(NestActivityZoneSensor, self).__init__(structure, device, "") self.zone = zone - self._name = "{} {} activity".format(self._name, self.zone.name) + 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 "{}-{}".format(self.device.serial, self.zone.zone_id) + return f"{self.device.serial}-{self.zone.zone_id}" @property def device_class(self): diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index c60d09a6002..51d826c242f 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -45,5 +45,5 @@ async def resolve_auth_code(hass, client_id, client_secret, code): if err.response.status_code == 401: raise config_flow.CodeInvalid() raise config_flow.NestAuthError( - "Unknown error: {} ({})".format(err, err.response.status_code) + f"Unknown error: {err} ({err.response.status_code})" ) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 2f2f3f9e182..591cd790ecf 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -152,7 +152,7 @@ class NetatmoBinarySensor(BinarySensorDevice): self._home = home self._timeout = timeout if home: - self._name = "{} / {}".format(home, camera_name) + self._name = f"{home} / {camera_name}" else: self._name = camera_name if module_name: diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index ec55394105c..d18ff9fc46c 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -88,11 +88,11 @@ class NetatmoCamera(Camera): try: if self._localurl: response = requests.get( - "{0}/live/snapshot_720.jpg".format(self._localurl), timeout=10 + f"{self._localurl}/live/snapshot_720.jpg", timeout=10 ) elif self._vpnurl: response = requests.get( - "{0}/live/snapshot_720.jpg".format(self._vpnurl), + f"{self._vpnurl}/live/snapshot_720.jpg", timeout=10, verify=self._verify_ssl, ) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 9656d4a37a4..1465058652d 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -154,7 +154,7 @@ class NetatmoThermostat(ClimateDevice): self._state = None self._room_id = room_id self._room_name = self._data.homedata.rooms[self._data.home_id][room_id]["name"] - self._name = "netatmo_{}".format(self._room_name) + self._name = f"netatmo_{self._room_name}" self._current_temperature = None self._target_temperature = None self._preset = None diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 8fa18a7f19c..aab901506a8 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -112,7 +112,7 @@ class NetdataSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._sensor_name) + return f"{self._name} {self._sensor_name}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index e4909ce68fc..2514b37657f 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -350,7 +350,7 @@ class LTEEntity(Entity): @_unique_id.default def _init_unique_id(self): """Register unique_id while we know data is valid.""" - return "{}_{}".format(self.sensor_type, self.modem_data.data.serial_number) + return f"{self.sensor_type}_{self.modem_data.data.serial_number}" async def async_added_to_hass(self): """Register callback.""" @@ -380,4 +380,4 @@ class LTEEntity(Entity): @property def name(self): """Return the name of the sensor.""" - return "Netgear LTE {}".format(self.sensor_type) + return f"Netgear LTE {self.sensor_type}" diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 3f9a5e01817..661eb75b732 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -62,9 +62,7 @@ def validate_value(value_name, value, value_list): "Invalid %s tag `%s`. Please use one of the following: %s", value_name, value, - ", ".join( - "{}: {}".format(title, tag) for tag, title in valid_values.items() - ), + ", ".join(f"{title}: {tag}" for tag, title in valid_values.items()), ) return False @@ -126,7 +124,7 @@ class NextBusDepartureSensor(Entity): self.stop = stop self._custom_name = name # Maybe pull a more user friendly name from the API here - self._name = "{} {}".format(agency, route) + self._name = f"{agency} {route}" self._client = client # set up default state attributes diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index d828ed9d98a..36eed0a11db 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -137,7 +137,7 @@ class NFAndroidTVNotificationService(BaseNotificationService): is_allowed_path, ): """Initialize the service.""" - self._target = "http://{}:7676".format(remoteip) + self._target = f"http://{remoteip}:7676" self._default_duration = duration self._default_fontsize = fontsize self._default_position = position diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index af93ee0da69..4cb84956002 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -46,7 +46,7 @@ class NikoHomeControlLight(Light): """Set up the Niko Home Control light platform.""" self._data = data self._light = light - self._unique_id = "light-{}".format(light.id) + self._unique_id = f"light-{light.id}" self._name = light.name self._state = light.is_on self._brightness = None diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 246ff10b117..8d3d61befd5 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -147,7 +147,7 @@ class NiluSensor(AirQualityEntity): def __init__(self, api_data: NiluData, name: str, show_on_map: bool): """Initialize the sensor.""" self._api = api_data - self._name = "{} {}".format(name, api_data.data.name) + self._name = f"{name} {api_data.data.name}" self._max_aqi = None self._attrs = {} diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 59aa1126222..8b2182665f6 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -124,7 +124,7 @@ class NMBSLiveBoard(Entity): departure = get_time_until(self._attrs["time"]) attrs = { - "departure": "In {} minutes".format(departure), + "departure": f"In {departure} minutes", "extra_train": int(self._attrs["isExtra"]) > 0, "vehicle_id": self._attrs["vehicle"], "monitored_station": self._station, @@ -132,7 +132,7 @@ class NMBSLiveBoard(Entity): } if delay > 0: - attrs["delay"] = "{} minutes".format(delay) + attrs["delay"] = f"{delay} minutes" return attrs @@ -194,7 +194,7 @@ class NMBSSensor(Entity): departure = get_time_until(self._attrs["departure"]["time"]) attrs = { - "departure": "In {} minutes".format(departure), + "departure": f"In {departure} minutes", "destination": self._station_to, "direction": self._attrs["departure"]["direction"]["name"], "platform_arriving": self._attrs["arrival"]["platform"], @@ -218,7 +218,7 @@ class NMBSSensor(Entity): ) + get_delay_in_minutes(via["departure"]["delay"]) if delay > 0: - attrs["delay"] = "{} minutes".format(delay) + attrs["delay"] = f"{delay} minutes" return attrs diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 2fa9d45a8b2..70ac7099d30 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -34,7 +34,7 @@ NO_IP_ERRORS = { } UPDATE_URL = "https://dynupdate.noip.com/nic/update" -HA_USER_AGENT = "{} {}".format(SERVER_SOFTWARE, EMAIL) +HA_USER_AGENT = f"{SERVER_SOFTWARE} {EMAIL}" CONFIG_SCHEMA = vol.Schema( { @@ -58,7 +58,7 @@ async def async_setup(hass, config): password = config[DOMAIN].get(CONF_PASSWORD) timeout = config[DOMAIN].get(CONF_TIMEOUT) - auth_str = base64.b64encode("{}:{}".format(user, password).encode("utf-8")) + auth_str = base64.b64encode(f"{user}:{password}".encode("utf-8")) session = hass.helpers.aiohttp_client.async_get_clientsession() diff --git a/homeassistant/components/noaa_tides/sensor.py b/homeassistant/components/noaa_tides/sensor.py index 2b3d2e42d4d..e5f31dba156 100644 --- a/homeassistant/components/noaa_tides/sensor.py +++ b/homeassistant/components/noaa_tides/sensor.py @@ -101,10 +101,10 @@ class NOAATidesAndCurrentsSensor(Entity): api_time = self.data.index[0] if self.data["hi_lo"][0] == "H": tidetime = api_time.strftime("%-I:%M %p") - return "High tide at {}".format(tidetime) + return f"High tide at {tidetime}" if self.data["hi_lo"][0] == "L": tidetime = api_time.strftime("%-I:%M %p") - return "Low tide at {}".format(tidetime) + return f"Low tide at {tidetime}" return None def update(self): diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 773c08808c3..6ede7f18da7 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -124,7 +124,7 @@ async def async_setup(hass, config): p_config.get(CONF_NAME) or discovery_info.get(CONF_NAME) or p_type ) for name, target in notify_service.targets.items(): - target_name = slugify("{}_{}".format(platform_name, name)) + target_name = slugify(f"{platform_name}_{name}") targets[target_name] = target hass.services.async_register( DOMAIN, @@ -145,7 +145,7 @@ async def async_setup(hass, config): schema=NOTIFY_SERVICE_SCHEMA, ) - hass.config.components.add("{}.{}".format(DOMAIN, p_type)) + hass.config.components.add(f"{DOMAIN}.{p_type}") return True diff --git a/homeassistant/components/notion/.translations/hu.json b/homeassistant/components/notion/.translations/hu.json new file mode 100644 index 00000000000..2f7664cf74e --- /dev/null +++ b/homeassistant/components/notion/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Felhaszn\u00e1l\u00f3n\u00e9v m\u00e1r regisztr\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3", + "no_devices": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a fi\u00f3kban" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Felhaszn\u00e1l\u00f3n\u00e9v/Email C\u00edm" + }, + "title": "T\u00f6ltse ki adatait" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/it.json b/homeassistant/components/notion/.translations/it.json new file mode 100644 index 00000000000..035c0c38952 --- /dev/null +++ b/homeassistant/components/notion/.translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Nome utente gi\u00e0 registrato", + "invalid_credentials": "Nome utente o password non validi", + "no_devices": "Nessun dispositivo trovato nell'account" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente / indirizzo E-mail" + }, + "title": "Inserisci le tue informazioni" + } + }, + "title": "Nozione" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/ko.json b/homeassistant/components/notion/.translations/ko.json index 32eb4b68855..76dc91cf46b 100644 --- a/homeassistant/components/notion/.translations/ko.json +++ b/homeassistant/components/notion/.translations/ko.json @@ -3,7 +3,7 @@ "error": { "identifier_exists": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + "no_devices": "\uacc4\uc815\uc5d0 \ub4f1\ub85d\ub41c \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json index c35de9c535c..380d4ad151e 100644 --- a/homeassistant/components/notion/.translations/pl.json +++ b/homeassistant/components/notion/.translations/pl.json @@ -9,9 +9,9 @@ "user": { "data": { "password": "Has\u0142o", - "username": "Nazwa u\u017cytkownika/adres e-mail" + "username": "Nazwa u\u017cytkownika / adres e-mail" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "Poj\u0119cie" diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index 4c3258b6eff..a84aa554be9 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -141,7 +141,7 @@ class StationPriceData: None, ) - self._station_name = name or "station {}".format(self.station_id) + self._station_name = name or f"station {self.station_id}" return self._station_name diff --git a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py index 16ef2c10bbd..9a9679f9575 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py +++ b/homeassistant/components/nsw_rural_fire_service_feed/geo_location.py @@ -210,6 +210,13 @@ class NswRuralFireServiceLocationEvent(GeolocationEvent): self._size = feed_entry.size self._responsible_agency = feed_entry.responsible_agency + @property + def icon(self): + """Return the icon to use in the frontend.""" + if self._fire: + return "mdi:fire" + return "mdi:alarm-light" + @property def source(self) -> str: """Return source value of this external event.""" diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index b2bc6aaab24..4542eb45c82 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -6,5 +6,7 @@ "geojson_client==0.4" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@exxamalte" + ] } diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 2e15ac8a68d..c8b19082585 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -1 +1,3 @@ """The nuki component.""" + +DOMAIN = "nuki" diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 31a655dfedd..7fda26b2900 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -1,20 +1,18 @@ """Nuki.io lock platform.""" from datetime import timedelta import logging -import requests +from pynuki import NukiBridge +from requests.exceptions import RequestException import voluptuous as vol -from homeassistant.components.lock import ( - DOMAIN, - PLATFORM_SCHEMA, - LockDevice, - SUPPORT_OPEN, -) +from homeassistant.components.lock import PLATFORM_SCHEMA, SUPPORT_OPEN, LockDevice 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 + _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 8080 @@ -30,7 +28,8 @@ MIN_TIME_BETWEEN_SCANS = timedelta(seconds=30) NUKI_DATA = "nuki" SERVICE_LOCK_N_GO = "lock_n_go" -SERVICE_CHECK_CONNECTION = "check_connection" + +ERROR_STATES = (0, 254, 255) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -47,48 +46,30 @@ LOCK_N_GO_SERVICE_SCHEMA = vol.Schema( } ) -CHECK_CONNECTION_SERVICE_SCHEMA = vol.Schema( - {vol.Optional(ATTR_ENTITY_ID): cv.entity_ids} -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nuki lock platform.""" - from pynuki import NukiBridge - bridge = NukiBridge( config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT ) - add_entities([NukiLock(lock) for lock in bridge.locks]) + devices = [NukiLock(lock) for lock in bridge.locks] def service_handler(service): """Service handler for nuki services.""" entity_ids = extract_entity_ids(hass, service) - all_locks = hass.data[NUKI_DATA][DOMAIN] - target_locks = [] - if not entity_ids: - target_locks = all_locks - else: - for lock in all_locks: - if lock.entity_id in entity_ids: - target_locks.append(lock) - for lock in target_locks: - if service.service == SERVICE_LOCK_N_GO: - unlatch = service.data[ATTR_UNLATCH] - lock.lock_n_go(unlatch=unlatch) - elif service.service == SERVICE_CHECK_CONNECTION: - lock.check_connection() + 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( - "nuki", SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA - ) - hass.services.register( - "nuki", - SERVICE_CHECK_CONNECTION, - service_handler, - schema=CHECK_CONNECTION_SERVICE_SCHEMA, + DOMAIN, SERVICE_LOCK_N_GO, service_handler, schema=LOCK_N_GO_SERVICE_SCHEMA ) + add_entities(devices) + class NukiLock(LockDevice): """Representation of a Nuki lock.""" @@ -99,15 +80,7 @@ class NukiLock(LockDevice): self._locked = nuki_lock.is_locked self._name = nuki_lock.name self._battery_critical = nuki_lock.battery_critical - self._available = nuki_lock.state != 255 - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - if NUKI_DATA not in self.hass.data: - self.hass.data[NUKI_DATA] = {} - if DOMAIN not in self.hass.data[NUKI_DATA]: - self.hass.data[NUKI_DATA][DOMAIN] = [] - self.hass.data[NUKI_DATA][DOMAIN].append(self) + self._available = nuki_lock.state not in ERROR_STATES @property def name(self): @@ -140,13 +113,19 @@ class NukiLock(LockDevice): def update(self): """Update the nuki lock properties.""" - try: - self._nuki_lock.update(aggressive=False) - except requests.exceptions.RequestException: - self._available = False - return + for level in (False, True): + try: + self._nuki_lock.update(aggressive=level) + except RequestException: + _LOGGER.warning("Network issues detect with %s", self.name) + self._available = False + return + + # If in error state, we force an update and repoll data + self._available = self._nuki_lock.state not in ERROR_STATES + if self._available: + break - self._available = self._nuki_lock.state != 255 self._name = self._nuki_lock.name self._locked = self._nuki_lock.is_locked self._battery_critical = self._nuki_lock.battery_critical @@ -170,12 +149,3 @@ class NukiLock(LockDevice): amount of time depending on the lock settings) and relock. """ self._nuki_lock.lock_n_go(unlatch, kwargs) - - def check_connection(self, **kwargs): - """Update the nuki lock properties.""" - try: - self._nuki_lock.update(aggressive=True) - except requests.exceptions.RequestException: - self._available = False - else: - self._available = self._nuki_lock.state != 255 diff --git a/homeassistant/components/nuki/manifest.json b/homeassistant/components/nuki/manifest.json index 932b80690c4..e7f078a1a05 100644 --- a/homeassistant/components/nuki/manifest.json +++ b/homeassistant/components/nuki/manifest.json @@ -2,11 +2,7 @@ "domain": "nuki", "name": "Nuki", "documentation": "https://www.home-assistant.io/components/nuki", - "requirements": [ - "pynuki==1.3.3" - ], + "requirements": ["pynuki==1.3.3"], "dependencies": [], - "codeowners": [ - "@pschmitt" - ] + "codeowners": ["@pvizeli"] } diff --git a/homeassistant/components/nws/__init__.py b/homeassistant/components/nws/__init__.py new file mode 100644 index 00000000000..dde2f6dee11 --- /dev/null +++ b/homeassistant/components/nws/__init__.py @@ -0,0 +1 @@ +"""NWS Integration.""" diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json new file mode 100644 index 00000000000..b0e5fdb2088 --- /dev/null +++ b/homeassistant/components/nws/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "nws", + "name": "National Weather Service", + "documentation": "https://www.home-assistant.io/components/nws", + "dependencies": [], + "codeowners": ["@MatthewFlamm"], + "requirements": ["pynws==0.7.4"] +} diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py new file mode 100644 index 00000000000..23cf84411a3 --- /dev/null +++ b/homeassistant/components/nws/weather.py @@ -0,0 +1,378 @@ +"""Support for NWS weather service.""" +from collections import OrderedDict +from datetime import timedelta +from json import JSONDecodeError +import logging + +import aiohttp +from pynws import SimpleNWS +import voluptuous as vol + +from homeassistant.components.weather import ( + WeatherEntity, + PLATFORM_SCHEMA, + ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, + ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_SPEED, + ATTR_FORECAST_WIND_BEARING, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_MODE, + LENGTH_KILOMETERS, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_HPA, + PRESSURE_PA, + PRESSURE_INHG, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle +from homeassistant.util.distance import convert as convert_distance +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.temperature import convert as convert_temperature + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data from National Weather Service/NOAA" + +SCAN_INTERVAL = timedelta(minutes=15) +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + +CONF_STATION = "station" + +ATTR_FORECAST_DETAIL_DESCRIPTION = "detailed_description" +ATTR_FORECAST_PRECIP_PROB = "precipitation_probability" +ATTR_FORECAST_DAYTIME = "daytime" + +# Ordered so that a single condition can be chosen from multiple weather codes. +# Catalog of NWS icon weather codes listed at: +# https://api.weather.gov/icons +CONDITION_CLASSES = OrderedDict( + [ + ( + "exceptional", + [ + "Tornado", + "Hurricane conditions", + "Tropical storm conditions", + "Dust", + "Smoke", + "Haze", + "Hot", + "Cold", + ], + ), + ("snowy", ["Snow", "Sleet", "Blizzard"]), + ( + "snowy-rainy", + [ + "Rain/snow", + "Rain/sleet", + "Freezing rain/snow", + "Freezing rain", + "Rain/freezing rain", + ], + ), + ("hail", []), + ( + "lightning-rainy", + [ + "Thunderstorm (high cloud cover)", + "Thunderstorm (medium cloud cover)", + "Thunderstorm (low cloud cover)", + ], + ), + ("lightning", []), + ("pouring", []), + ( + "rainy", + [ + "Rain", + "Rain showers (high cloud cover)", + "Rain showers (low cloud cover)", + ], + ), + ("windy-variant", ["Mostly cloudy and windy", "Overcast and windy"]), + ( + "windy", + [ + "Fair/clear and windy", + "A few clouds and windy", + "Partly cloudy and windy", + ], + ), + ("fog", ["Fog/mist"]), + ("clear", ["Fair/clear"]), # sunny and clear-night + ("cloudy", ["Mostly cloudy", "Overcast"]), + ("partlycloudy", ["A few clouds", "Partly cloudy"]), + ] +) + +ERRORS = (aiohttp.ClientError, JSONDecodeError) + +FORECAST_MODE = ["daynight", "hourly"] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME): cv.string, + vol.Inclusive( + CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" + ): cv.latitude, + vol.Inclusive( + CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" + ): cv.longitude, + vol.Optional(CONF_MODE, default="daynight"): vol.In(FORECAST_MODE), + vol.Optional(CONF_STATION): cv.string, + vol.Required(CONF_API_KEY): cv.string, + } +) + + +def convert_condition(time, weather): + """ + Convert NWS codes to HA condition. + + Choose first condition in CONDITION_CLASSES that exists in weather code. + If no match is found, return first condition from NWS + """ + conditions = [w[0] for w in weather] + prec_probs = [w[1] or 0 for w in weather] + + # Choose condition with highest priority. + cond = next( + ( + key + for key, value in CONDITION_CLASSES.items() + if any(condition in value for condition in conditions) + ), + conditions[0], + ) + + if cond == "clear": + if time == "day": + return "sunny", max(prec_probs) + if time == "night": + return "clear-night", max(prec_probs) + return cond, max(prec_probs) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the NWS weather platform.""" + + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + station = config.get(CONF_STATION) + api_key = config[CONF_API_KEY] + mode = config[CONF_MODE] + + websession = async_get_clientsession(hass) + # ID request as being from HA, pynws prepends the api_key in addition + api_key_ha = f"{api_key} homeassistant" + nws = SimpleNWS(latitude, longitude, api_key_ha, mode, websession) + + _LOGGER.debug("Setting up station: %s", station) + try: + await nws.set_station(station) + except ERRORS as status: + _LOGGER.error( + "Error getting station list for %s: %s", (latitude, longitude), status + ) + raise PlatformNotReady + + _LOGGER.debug("Station list: %s", nws.stations) + _LOGGER.debug( + "Initialized for coordinates %s, %s -> station %s", + latitude, + longitude, + nws.station, + ) + + async_add_entities([NWSWeather(nws, mode, hass.config.units, config)], True) + + +class NWSWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, nws, mode, units, config): + """Initialise the platform with a data instance and station name.""" + self.nws = nws + self.station_name = config.get(CONF_NAME, self.nws.station) + self.is_metric = units.is_metric + self.mode = mode + + self.observation = None + self._forecast = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update Condition.""" + _LOGGER.debug("Updating station observations %s", self.nws.station) + try: + await self.nws.update_observation() + except ERRORS as status: + _LOGGER.error( + "Error updating observation from station %s: %s", + self.nws.station, + status, + ) + else: + self.observation = self.nws.observation + _LOGGER.debug("Updating forecast") + try: + await self.nws.update_forecast() + except ERRORS as status: + _LOGGER.error( + "Error updating forecast from station %s: %s", self.nws.station, status + ) + return + self._forecast = self.nws.forecast + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def name(self): + """Return the name of the station.""" + return self.station_name + + @property + def temperature(self): + """Return the current temperature.""" + temp_c = None + if self.observation: + temp_c = self.observation.get("temperature") + if temp_c: + return convert_temperature(temp_c, TEMP_CELSIUS, TEMP_FAHRENHEIT) + return None + + @property + def pressure(self): + """Return the current pressure.""" + pressure_pa = None + if self.observation: + pressure_pa = self.observation.get("seaLevelPressure") + if pressure_pa is None: + return None + if self.is_metric: + pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_HPA) + pressure = round(pressure) + else: + pressure = convert_pressure(pressure_pa, PRESSURE_PA, PRESSURE_INHG) + pressure = round(pressure, 2) + return pressure + + @property + def humidity(self): + """Return the name of the sensor.""" + humidity = None + if self.observation: + humidity = self.observation.get("relativeHumidity") + return humidity + + @property + def wind_speed(self): + """Return the current windspeed.""" + wind_m_s = None + if self.observation: + wind_m_s = self.observation.get("windSpeed") + if wind_m_s is None: + return None + wind_m_hr = wind_m_s * 3600 + + if self.is_metric: + wind = convert_distance(wind_m_hr, LENGTH_METERS, LENGTH_KILOMETERS) + else: + wind = convert_distance(wind_m_hr, LENGTH_METERS, LENGTH_MILES) + return round(wind) + + @property + def wind_bearing(self): + """Return the current wind bearing (degrees).""" + wind_bearing = None + if self.observation: + wind_bearing = self.observation.get("windDirection") + return wind_bearing + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_FAHRENHEIT + + @property + def condition(self): + """Return current condition.""" + weather = None + if self.observation: + weather = self.observation.get("iconWeather") + time = self.observation.get("iconTime") + + if weather: + cond, _ = convert_condition(time, weather) + return cond + return None + + @property + def visibility(self): + """Return visibility.""" + vis_m = None + if self.observation: + vis_m = self.observation.get("visibility") + if vis_m is None: + return None + + if self.is_metric: + vis = convert_distance(vis_m, LENGTH_METERS, LENGTH_KILOMETERS) + else: + vis = convert_distance(vis_m, LENGTH_METERS, LENGTH_MILES) + return round(vis, 0) + + @property + def forecast(self): + """Return forecast.""" + if self._forecast is None: + return None + forecast = [] + for forecast_entry in self._forecast: + data = { + ATTR_FORECAST_DETAIL_DESCRIPTION: forecast_entry.get( + "detailedForecast" + ), + ATTR_FORECAST_TEMP: forecast_entry.get("temperature"), + ATTR_FORECAST_TIME: forecast_entry.get("startTime"), + } + + if self.mode == "daynight": + data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime") + time = forecast_entry.get("iconTime") + weather = forecast_entry.get("iconWeather") + if time and weather: + cond, precip = convert_condition(time, weather) + else: + cond, precip = None, None + data[ATTR_FORECAST_CONDITION] = cond + data[ATTR_FORECAST_PRECIP_PROB] = precip + + data[ATTR_FORECAST_WIND_BEARING] = forecast_entry.get("windBearing") + wind_speed = forecast_entry.get("windSpeedAvg") + if wind_speed: + if self.is_metric: + data[ATTR_FORECAST_WIND_SPEED] = round( + convert_distance(wind_speed, LENGTH_MILES, LENGTH_KILOMETERS) + ) + else: + data[ATTR_FORECAST_WIND_SPEED] = round(wind_speed) + else: + data[ATTR_FORECAST_WIND_SPEED] = None + forecast.append(data) + return forecast diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index abe5f2a126e..d3d867ff378 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) - url = "http://{}:{}".format(host, port) + url = f"http://{host}:{port}" try: add_entities([NX584Alarm(hass, url, name)]) diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index e3af407a53d..8b26a958a6f 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -47,7 +47,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): zone_types = config.get(CONF_ZONE_TYPES) try: - client = nx584_client.Client("http://{}:{}".format(host, port)) + client = nx584_client.Client(f"http://{host}:{port}") zones = client.list_zones() except requests.exceptions.ConnectionError as ex: _LOGGER.error("Unable to connect to NX584: %s", str(ex)) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index 50fdf004739..73643a5383c 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) monitored_types = config.get(CONF_MONITORED_VARIABLES) - url = "http{}://{}:{}/jsonrpc".format(ssl, host, port) + url = f"http{ssl}://{host}:{port}/jsonrpc" try: nzbgetapi = NZBGetAPI(api_url=url, username=username, password=password) diff --git a/homeassistant/components/obihai/__init__.py b/homeassistant/components/obihai/__init__.py new file mode 100644 index 00000000000..8e65423b73b --- /dev/null +++ b/homeassistant/components/obihai/__init__.py @@ -0,0 +1 @@ +"""The Obihai integration.""" diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json new file mode 100644 index 00000000000..b6bad10d608 --- /dev/null +++ b/homeassistant/components/obihai/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "obihai", + "name": "Obihai", + "documentation": "https://www.home-assistant.io/components/obihai", + "requirements": [ + "pyobihai==1.0.2" + ], + "dependencies": [], + "codeowners": ["@dshokouhi"] + } + \ No newline at end of file diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py new file mode 100644 index 00000000000..4eb3881e95b --- /dev/null +++ b/homeassistant/components/obihai/sensor.py @@ -0,0 +1,104 @@ +"""Support for Obihai Sensors.""" +import logging + +from datetime import timedelta +import voluptuous as vol + +from pyobihai import PyObihai + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + DEVICE_CLASS_TIMESTAMP, +) + +from homeassistant.helpers.entity import Entity +import homeassistant.helpers.config_validation as cv + + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=5) + +OBIHAI = "Obihai" +DEFAULT_USERNAME = "admin" +DEFAULT_PASSWORD = "admin" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Obihai sensor platform.""" + + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + host = config[CONF_HOST] + + sensors = [] + + pyobihai = PyObihai() + + services = pyobihai.get_state(host, username, password) + + line_services = pyobihai.get_line_state(host, username, password) + + for key in services: + sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + + for key in line_services: + sensors.append(ObihaiServiceSensors(pyobihai, host, username, password, key)) + + add_entities(sensors) + + +class ObihaiServiceSensors(Entity): + """Get the status of each Obihai Lines.""" + + def __init__(self, pyobihai, host, username, password, service_name): + """Initialize monitor sensor.""" + self._host = host + self._username = username + self._password = password + self._service_name = service_name + self._state = None + self._name = f"{OBIHAI} {self._service_name}" + self._pyobihai = pyobihai + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_class(self): + """Return the device class for uptime sensor.""" + if self._service_name == "Last Reboot": + return DEVICE_CLASS_TIMESTAMP + return None + + def update(self): + """Update the sensor.""" + services = self._pyobihai.get_state(self._host, self._username, self._password) + + if self._service_name in services: + self._state = services.get(self._service_name) + + services = self._pyobihai.get_line_state( + self._host, self._username, self._password + ) + + if self._service_name in services: + self._state = services.get(self._service_name) diff --git a/homeassistant/components/octoprint/binary_sensor.py b/homeassistant/components/octoprint/binary_sensor.py index ea457ee19c7..7ed1170c6a0 100644 --- a/homeassistant/components/octoprint/binary_sensor.py +++ b/homeassistant/components/octoprint/binary_sensor.py @@ -45,9 +45,9 @@ class OctoPrintBinarySensor(BinarySensorDevice): """Initialize a new OctoPrint sensor.""" self.sensor_name = sensor_name if tool is None: - self._name = "{} {}".format(sensor_name, condition) + self._name = f"{sensor_name} {condition}" else: - self._name = "{} {}".format(sensor_name, condition) + self._name = f"{sensor_name} {condition}" self.sensor_type = sensor_type self.api = api self._state = False diff --git a/homeassistant/components/octoprint/sensor.py b/homeassistant/components/octoprint/sensor.py index 0233684c320..d21aac9ff65 100644 --- a/homeassistant/components/octoprint/sensor.py +++ b/homeassistant/components/octoprint/sensor.py @@ -89,7 +89,7 @@ class OctoPrintSensor(Entity): """Initialize a new OctoPrint sensor.""" self.sensor_name = sensor_name if tool is None: - self._name = "{} {}".format(sensor_name, condition) + self._name = f"{sensor_name} {condition}" else: self._name = "{} {} {} {}".format(sensor_name, condition, tool, "temp") self.sensor_type = sensor_type diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index df0f01bcff4..2e79393fe42 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -122,7 +122,7 @@ class UserOnboardingView(_BaseOnboardingView): for area in DEFAULT_AREAS: area_registry.async_create( - translations["component.onboarding.area.{}".format(area)] + translations[f"component.onboarding.area.{area}"] ) await self._async_mark_done(hass) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 6865eb8c9f9..023fb32e6e4 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -1,8 +1,6 @@ """Support for Onkyo Receivers.""" import logging - -# pylint: disable=unused-import -from typing import List # noqa: F401 +from typing import List import voluptuous as vol @@ -54,7 +52,7 @@ SUPPORT_ONKYO_WO_VOLUME = ( | SUPPORT_PLAY_MEDIA ) -KNOWN_HOSTS = [] # type: List[str] +KNOWN_HOSTS: List[str] = [] DEFAULT_SOURCES = { "tv": "TV", "bd": "Bluray", @@ -85,7 +83,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( TIMEOUT_MESSAGE = "Timeout waiting for response." + ATTR_HDMI_OUTPUT = "hdmi_output" +ATTR_PRESET = "preset" + ACCEPTED_VALUES = [ "no", "analog", @@ -177,7 +178,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "2", receiver, config.get(CONF_SOURCES), - name="{} Zone 2".format(config[CONF_NAME]), + name=f"{config[CONF_NAME]} Zone 2", ) ) # Add Zone3 if available @@ -188,7 +189,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "3", receiver, config.get(CONF_SOURCES), - name="{} Zone 3".format(config[CONF_NAME]), + name=f"{config[CONF_NAME]} Zone 3", ) ) except OSError: @@ -210,8 +211,8 @@ class OnkyoDevice(MediaPlayerDevice): self._muted = False self._volume = 0 self._pwstate = STATE_OFF - self._name = name or "{}_{}".format( - receiver.info["model_name"], receiver.info["identifier"] + self._name = ( + name or f"{receiver.info['model_name']}_{receiver.info['identifier']}" ) self._max_volume = max_volume self._current_source = None @@ -249,7 +250,7 @@ class OnkyoDevice(MediaPlayerDevice): mute_raw = self.command("audio-muting query") current_source_raw = self.command("input-selector query") hdmi_out_raw = self.command("hdmi-output-selector query") - + preset_raw = self.command("preset query") if not (volume_raw and mute_raw and current_source_raw): return @@ -265,6 +266,11 @@ class OnkyoDevice(MediaPlayerDevice): break else: self._current_source = "_".join([i for i in current_source_tuples[1]]) + if preset_raw and self._current_source.lower() == "radio": + self._attributes[ATTR_PRESET] = preset_raw[1] + elif ATTR_PRESET in self._attributes: + del self._attributes[ATTR_PRESET] + self._muted = bool(mute_raw[1] == "on") self._volume = volume_raw[1] / self._max_volume @@ -323,7 +329,7 @@ class OnkyoDevice(MediaPlayerDevice): Onkyo ranges from 1-80 however 80 is usually far too loud so allow the user to specify the upper range with CONF_MAX_VOLUME """ - self.command("volume {}".format(int(volume * self._max_volume))) + self.command(f"volume {int(volume * self._max_volume)}") def volume_up(self): """Increase volume by 1 step.""" @@ -348,17 +354,17 @@ class OnkyoDevice(MediaPlayerDevice): """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] - self.command("input-selector {}".format(source)) + self.command(f"input-selector {source}") def play_media(self, media_type, media_id, **kwargs): """Play radio station by preset number.""" source = self._reverse_mapping[self._current_source] if media_type.lower() == "radio" and source in DEFAULT_PLAYABLE_SOURCES: - self.command("preset {}".format(media_id)) + self.command(f"preset {media_id}") def select_output(self, output): """Set hdmi-out.""" - self.command("hdmi-output-selector={}".format(output)) + self.command(f"hdmi-output-selector={output}") class OnkyoDeviceZone(OnkyoDevice): @@ -372,7 +378,7 @@ class OnkyoDeviceZone(OnkyoDevice): def update(self): """Get the latest state from the device.""" - status = self.command("zone{}.power=query".format(self._zone)) + status = self.command(f"zone{self._zone}.power=query") if not status: return @@ -382,10 +388,10 @@ class OnkyoDeviceZone(OnkyoDevice): self._pwstate = STATE_OFF return - volume_raw = self.command("zone{}.volume=query".format(self._zone)) - mute_raw = self.command("zone{}.muting=query".format(self._zone)) - current_source_raw = self.command("zone{}.selector=query".format(self._zone)) - + volume_raw = self.command(f"zone{self._zone}.volume=query") + mute_raw = self.command(f"zone{self._zone}.muting=query") + current_source_raw = self.command(f"zone{self._zone}.selector=query") + preset_raw = self.command(f"zone{self._zone}.preset=query") # If we received a source value, but not a volume value # it's likely this zone permanently does not support volume. if current_source_raw and not volume_raw: @@ -411,7 +417,10 @@ class OnkyoDeviceZone(OnkyoDevice): else: self._current_source = "_".join([i for i in current_source_tuples[1]]) self._muted = bool(mute_raw[1] == "on") - + if preset_raw and self._current_source.lower() == "radio": + self._attributes[ATTR_PRESET] = preset_raw[1] + elif ATTR_PRESET in self._attributes: + del self._attributes[ATTR_PRESET] if self._supports_volume: self._volume = volume_raw[1] / 80.0 @@ -424,33 +433,33 @@ class OnkyoDeviceZone(OnkyoDevice): def turn_off(self): """Turn the media player off.""" - self.command("zone{}.power=standby".format(self._zone)) + self.command(f"zone{self._zone}.power=standby") def set_volume_level(self, volume): """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command("zone{}.volume={}".format(self._zone, int(volume * 80))) + self.command(f"zone{self._zone}.volume={int(volume * 80)}") def volume_up(self): """Increase volume by 1 step.""" - self.command("zone{}.volume=level-up".format(self._zone)) + self.command(f"zone{self._zone}.volume=level-up") def volume_down(self): """Decrease volume by 1 step.""" - self.command("zone{}.volume=level-down".format(self._zone)) + self.command(f"zone{self._zone}.volume=level-down") def mute_volume(self, mute): """Mute (true) or unmute (false) media player.""" if mute: - self.command("zone{}.muting=on".format(self._zone)) + self.command(f"zone{self._zone}.muting=on") else: - self.command("zone{}.muting=off".format(self._zone)) + self.command(f"zone{self._zone}.muting=off") def turn_on(self): """Turn the media player on.""" - self.command("zone{}.power=on".format(self._zone)) + self.command(f"zone{self._zone}.power=on") def select_source(self, source): """Set the input source.""" if source in self._source_list: source = self._reverse_mapping[source] - self.command("zone{}.selector={}".format(self._zone, source)) + self.command(f"zone{self._zone}.selector={source}") diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 44270e5e7e9..0635a2d1f11 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -24,6 +24,7 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.service import extract_entity_ids +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -158,19 +159,38 @@ class ONVIFHassCamera(Camera): from aiohttp.client_exceptions import ClientConnectorError from homeassistant.exceptions import PlatformNotReady from zeep.exceptions import Fault - import homeassistant.util.dt as dt_util try: _LOGGER.debug("Updating service addresses") - await self._camera.update_xaddrs() - _LOGGER.debug("Setting up the ONVIF device management service") + await self.async_check_date_and_time() + await self.async_obtain_input_uri() + self.setup_ptz() + except ClientConnectorError as err: + _LOGGER.warning( + "Couldn't connect to camera '%s', but will " "retry later. Error: %s", + self._name, + err, + ) + raise PlatformNotReady + except Fault as err: + _LOGGER.error( + "Couldn't connect to camera '%s', please verify " + "that the credentials are correct. Error: %s", + self._name, + err, + ) - devicemgmt = self._camera.create_devicemgmt_service() + async def async_check_date_and_time(self): + """Warns if camera and system date not synced.""" + from aiohttp.client_exceptions import ServerDisconnectedError - _LOGGER.debug("Retrieving current camera date/time") + _LOGGER.debug("Setting up the ONVIF device management service") + devicemgmt = self._camera.create_devicemgmt_service() + _LOGGER.debug("Retrieving current camera date/time") + try: system_date = dt_util.utcnow() device_time = await devicemgmt.GetSystemDateAndTime() if device_time: @@ -201,33 +221,10 @@ class ONVIFHassCamera(Camera): cam_date, system_date, ) - - _LOGGER.debug("Obtaining input uri") - - await self.async_obtain_input_uri() - - _LOGGER.debug("Setting up the ONVIF PTZ service") - - if self._camera.get_service("ptz", create=False) is None: - _LOGGER.warning("PTZ is not available on this camera") - else: - self._ptz_service = self._camera.create_ptz_service() - _LOGGER.debug("Completed set up of the ONVIF camera component") - except ClientConnectorError as err: + except ServerDisconnectedError as err: _LOGGER.warning( - "Couldn't connect to camera '%s', but will " "retry later. Error: %s", - self._name, - err, + "Couldn't get camera '%s' date/time. Error: %s", self._name, err ) - raise PlatformNotReady - except Fault as err: - _LOGGER.error( - "Couldn't connect to camera '%s', please verify " - "that the credentials are correct. Error: %s", - self._name, - err, - ) - return async def async_obtain_input_uri(self): """Set the input uri for the camera.""" @@ -270,7 +267,7 @@ class ONVIFHassCamera(Camera): uri_no_auth = stream_uri.Uri uri_for_log = uri_no_auth.replace("rtsp://", "rtsp://:@", 1) self._input = uri_no_auth.replace( - "rtsp://", "rtsp://{}:{}@".format(self._username, self._password), 1 + "rtsp://", f"rtsp://{self._username}:{self._password}@", 1 ) _LOGGER.debug( @@ -280,7 +277,15 @@ class ONVIFHassCamera(Camera): ) except exceptions.ONVIFError as err: _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) - return + + def setup_ptz(self): + """Set up PTZ if available.""" + _LOGGER.debug("Setting up the ONVIF PTZ service") + if self._camera.get_service("ptz", create=False) is None: + _LOGGER.warning("PTZ is not available on this camera") + else: + self._ptz_service = self._camera.create_ptz_service() + _LOGGER.debug("Completed set up of the ONVIF camera component") async def async_perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 68f14846af7..e8ebeb102e6 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -3,9 +3,9 @@ "name": "Opencv", "documentation": "https://www.home-assistant.io/components/opencv", "requirements": [ - "numpy==1.17.0", - "opencv-python-headless==4.1.0.25" + "numpy==1.17.1", + "opencv-python-headless==4.1.1.26" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 67f5c93dbc8..1243a9164fd 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -6,19 +6,20 @@ import voluptuous as vol from homeassistant.components.cover import ( CoverDevice, + DEVICE_CLASS_GARAGE, PLATFORM_SCHEMA, SUPPORT_OPEN, SUPPORT_CLOSE, ) from homeassistant.const import ( - CONF_DEVICE, CONF_NAME, - STATE_UNKNOWN, STATE_CLOSED, STATE_OPEN, CONF_COVERS, CONF_HOST, CONF_PORT, + STATE_CLOSING, + STATE_OPENING, ) import homeassistant.helpers.config_validation as cv @@ -28,17 +29,11 @@ ATTR_DISTANCE_SENSOR = "distance_sensor" ATTR_DOOR_STATE = "door_state" ATTR_SIGNAL_STRENGTH = "wifi_signal" -CONF_DEVICE_ID = "device_id" CONF_DEVICE_KEY = "device_key" DEFAULT_NAME = "OpenGarage" DEFAULT_PORT = 80 -STATE_CLOSING = "closing" -STATE_OFFLINE = "offline" -STATE_OPENING = "opening" -STATE_STOPPED = "stopped" - STATES_MAP = {0: STATE_CLOSED, 1: STATE_OPEN} COVER_SCHEMA = vol.Schema( @@ -60,16 +55,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): covers = [] devices = config.get(CONF_COVERS) - for device_id, device_config in devices.items(): + for device_config in devices.values(): args = { CONF_NAME: device_config.get(CONF_NAME), CONF_HOST: device_config.get(CONF_HOST), CONF_PORT: device_config.get(CONF_PORT), - CONF_DEVICE_ID: device_config.get(CONF_DEVICE, device_id), CONF_DEVICE_KEY: device_config.get(CONF_DEVICE_KEY), } - covers.append(OpenGarageCover(hass, args)) + covers.append(OpenGarageCover(args)) add_entities(covers, True) @@ -77,17 +71,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class OpenGarageCover(CoverDevice): """Representation of a OpenGarage cover.""" - def __init__(self, hass, args): + def __init__(self, args): """Initialize the cover.""" self.opengarage_url = "http://{}:{}".format(args[CONF_HOST], args[CONF_PORT]) - self.hass = hass self._name = args[CONF_NAME] - self.device_id = args["device_id"] self._device_key = args[CONF_DEVICE_KEY] self._state = None self._state_before_move = None - self.dist = None - self.signal = None + self._device_state_attributes = {} self._available = True @property @@ -103,93 +94,88 @@ class OpenGarageCover(CoverDevice): @property def device_state_attributes(self): """Return the device state attributes.""" - data = {} - - if self.signal is not None: - data[ATTR_SIGNAL_STRENGTH] = self.signal - - if self.dist is not None: - data[ATTR_DISTANCE_SENSOR] = self.dist - - if self._state is not None: - data[ATTR_DOOR_STATE] = self._state - - return data + return self._device_state_attributes @property def is_closed(self): """Return if the cover is closed.""" - if self._state in [STATE_UNKNOWN, STATE_OFFLINE]: + if self._state is None: return None return self._state in [STATE_CLOSED, STATE_OPENING] def close_cover(self, **kwargs): """Close the cover.""" - if self._state not in [STATE_CLOSED, STATE_CLOSING]: - self._state_before_move = self._state - self._state = STATE_CLOSING - self._push_button() + if self._state in [STATE_CLOSED, STATE_CLOSING]: + return + self._state_before_move = self._state + self._state = STATE_CLOSING + self._push_button() def open_cover(self, **kwargs): """Open the cover.""" - if self._state not in [STATE_OPEN, STATE_OPENING]: - self._state_before_move = self._state - self._state = STATE_OPENING - self._push_button() + if self._state in [STATE_OPEN, STATE_OPENING]: + return + self._state_before_move = self._state + self._state = STATE_OPENING + self._push_button() def update(self): """Get updated status from API.""" try: - status = self._get_status() - if self._name is None: - if status["name"] is not None: - self._name = status["name"] - state = STATES_MAP.get(status.get("door"), STATE_UNKNOWN) - if self._state_before_move is not None: - if self._state_before_move != state: - self._state = state - self._state_before_move = None - else: - self._state = state - - _LOGGER.debug("%s status: %s", self._name, self._state) - self.signal = status.get("rssi") - self.dist = status.get("dist") - self._available = True + status = requests.get(f"{self.opengarage_url}/jc", timeout=10).json() except requests.exceptions.RequestException as ex: _LOGGER.error( "Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex) ) - self._state = STATE_OFFLINE + self._available = False + return - def _get_status(self): - """Get latest status.""" - url = "{}/jc".format(self.opengarage_url) - ret = requests.get(url, timeout=10) - return ret.json() + if self._name is None and status["name"] is not None: + self._name = status["name"] + state = STATES_MAP.get(status.get("door")) + if self._state_before_move is not None: + if self._state_before_move != state: + self._state = state + self._state_before_move = None + else: + self._state = state + + _LOGGER.debug("%s status: %s", self._name, self._state) + if status.get("rssi") is not None: + self._device_state_attributes[ATTR_SIGNAL_STRENGTH] = status.get("rssi") + if status.get("dist") is not None: + self._device_state_attributes[ATTR_DISTANCE_SENSOR] = status.get("dist") + if self._state is not None: + self._device_state_attributes[ATTR_DOOR_STATE] = self._state + + self._available = True def _push_button(self): """Send commands to API.""" - url = "{}/cc?dkey={}&click=1".format(self.opengarage_url, self._device_key) + result = -1 try: - response = requests.get(url, timeout=10).json() - if response["result"] == 2: - _LOGGER.error( - "Unable to control %s: Device key is incorrect", self._name - ) - self._state = self._state_before_move - self._state_before_move = None + result = requests.get( + f"{self.opengarage_url}/cc?dkey={self._device_key}&click=1", timeout=10 + ).json()["result"] except requests.exceptions.RequestException as ex: _LOGGER.error( "Unable to connect to OpenGarage device: %(reason)s", dict(reason=ex) ) - self._state = self._state_before_move - self._state_before_move = None + if result == 1: + return + + if result == 2: + _LOGGER.error("Unable to control %s: Device key is incorrect", self._name) + elif result > 2: + _LOGGER.error("Unable to control %s: Error code %s", self._name, result) + + self._state = self._state_before_move + self._state_before_move = None @property def device_class(self): """Return the class of this device, from component DEVICE_CLASSES.""" - return "garage" + return DEVICE_CLASS_GARAGE @property def supported_features(self): diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index a0cfaf5c2be..0c17daa0ab4 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -36,8 +36,8 @@ DOMAIN = "opensky" DEFAULT_ALTITUDE = 0 -EVENT_OPENSKY_ENTRY = "{}_entry".format(DOMAIN) -EVENT_OPENSKY_EXIT = "{}_exit".format(DOMAIN) +EVENT_OPENSKY_ENTRY = f"{DOMAIN}_entry" +EVENT_OPENSKY_EXIT = f"{DOMAIN}_exit" SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds OPENSKY_ATTRIBUTION = ( diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index b20d97dadce..0c145963653 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -260,7 +260,7 @@ def register_services(hass): gpio_id = call.data[ATTR_ID] gpio_mode = call.data[ATTR_MODE] mode = await gw_dev.gateway.set_gpio_mode(gpio_id, gpio_mode) - gpio_var = getattr(gw_vars, "OTGW_GPIO_{}".format(gpio_id)) + gpio_var = getattr(gw_vars, f"OTGW_GPIO_{gpio_id}") gw_dev.status.update({gpio_var: mode}) async_dispatcher_send(hass, gw_dev.update_signal, gw_dev.status) @@ -274,7 +274,7 @@ def register_services(hass): led_id = call.data[ATTR_ID] led_mode = call.data[ATTR_MODE] mode = await gw_dev.gateway.set_led_mode(led_id, led_mode) - led_var = getattr(gw_vars, "OTGW_LED_{}".format(led_id)) + led_var = getattr(gw_vars, f"OTGW_LED_{led_id}") gw_dev.status.update({led_var: mode}) async_dispatcher_send(hass, gw_dev.update_signal, gw_dev.status) @@ -333,7 +333,7 @@ class OpenThermGatewayDevice: self.name = config.get(CONF_NAME, gw_id) self.climate_config = config[CONF_CLIMATE] self.status = {} - self.update_signal = "{}_{}_update".format(DATA_OPENTHERM_GW, gw_id) + self.update_signal = f"{DATA_OPENTHERM_GW}_{gw_id}_update" self.gateway = pyotgw.pyotgw() async def connect_and_subscribe(self, device_path): diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 2f4206b8e09..614829265e2 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -33,7 +33,7 @@ class OpenThermBinarySensor(BinarySensorDevice): 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, "{}_{}".format(var, gw_dev.gw_id), hass=gw_dev.hass + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass ) self._gateway = gw_dev self._var = var diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 3727d907c9a..1449caf5def 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -34,7 +34,7 @@ class OpenThermSensor(Entity): 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, "{}_{}".format(var, gw_dev.gw_id), hass=gw_dev.hass + ENTITY_ID_FORMAT, f"{var}_{gw_dev.gw_id}", hass=gw_dev.hass ) self._gateway = gw_dev self._var = var @@ -55,7 +55,7 @@ class OpenThermSensor(Entity): """Handle status updates from the component.""" value = status.get(self._var) if isinstance(value, float): - value = "{:2.1f}".format(value) + value = f"{value:2.1f}" self._value = value self.async_schedule_update_ha_state() diff --git a/homeassistant/components/openuv/.translations/pl.json b/homeassistant/components/openuv/.translations/pl.json index 2c4c47e8da4..ee3875c2903 100644 --- a/homeassistant/components/openuv/.translations/pl.json +++ b/homeassistant/components/openuv/.translations/pl.json @@ -12,7 +12,7 @@ "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "OpenUV" diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 339b8900049..62a8c642bc8 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,5 +1,6 @@ """Support for UV data from openuv.io.""" import logging +import asyncio import voluptuous as vol @@ -35,7 +36,7 @@ DEFAULT_ATTRIBUTION = "Data provided by OpenUV" NOTIFICATION_ID = "openuv_notification" NOTIFICATION_TITLE = "OpenUV Component Setup" -TOPIC_UPDATE = "{0}_data_update".format(DOMAIN) +TOPIC_UPDATE = f"{DOMAIN}_data_update" TYPE_CURRENT_OZONE_LEVEL = "current_ozone_level" TYPE_CURRENT_UV_INDEX = "current_uv_index" @@ -198,19 +199,33 @@ async def async_setup_entry(hass, config_entry): @_verify_domain_control async def update_data(service): - """Refresh OpenUV data.""" - _LOGGER.debug("Refreshing OpenUV data") - - try: - await openuv.async_update() - except OpenUvError as err: - _LOGGER.error("Error during data update: %s", err) - return - + """Refresh all OpenUV data.""" + _LOGGER.debug("Refreshing all OpenUV data") + await openuv.async_update() async_dispatcher_send(hass, TOPIC_UPDATE) hass.services.async_register(DOMAIN, "update_data", update_data) + @_verify_domain_control + async def update_uv_index_data(service): + """Refresh OpenUV UV index data.""" + _LOGGER.debug("Refreshing OpenUV UV index data") + await openuv.async_update_uv_index_data() + async_dispatcher_send(hass, TOPIC_UPDATE) + + hass.services.async_register(DOMAIN, "update_uv_index_data", update_uv_index_data) + + @_verify_domain_control + async def update_protection_data(service): + """Refresh OpenUV protection window data.""" + _LOGGER.debug("Refreshing OpenUV protection window data") + await openuv.async_update_protection_data() + async_dispatcher_send(hass, TOPIC_UPDATE) + + hass.services.async_register( + DOMAIN, "update_protection_data", update_protection_data + ) + return True @@ -234,21 +249,36 @@ class OpenUV: self.data = {} self.sensor_conditions = sensor_conditions - async def async_update(self): - """Update sensor/binary sensor data.""" - if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: - resp = await self.client.uv_protection_window() - data = resp["result"] + async def async_update_protection_data(self): + """Update binary sensor (protection window) data.""" + from pyopenuv.errors import OpenUvError - if data.get("from_time") and data.get("to_time"): - self.data[DATA_PROTECTION_WINDOW] = data - else: - _LOGGER.debug("No valid protection window data for this location") + if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: + try: + resp = await self.client.uv_protection_window() + self.data[DATA_PROTECTION_WINDOW] = resp["result"] + except OpenUvError as err: + _LOGGER.error("Error during protection data update: %s", err) self.data[DATA_PROTECTION_WINDOW] = {} + return + + async def async_update_uv_index_data(self): + """Update sensor (uv index, etc) data.""" + from pyopenuv.errors import OpenUvError if any(c in self.sensor_conditions for c in SENSORS): - data = await self.client.uv_index() - self.data[DATA_UV] = data + try: + data = await self.client.uv_index() + self.data[DATA_UV] = data + except OpenUvError as err: + _LOGGER.error("Error during uv index data update: %s", err) + self.data[DATA_UV] = {} + return + + async def async_update(self): + """Update sensor/binary sensor data.""" + tasks = [self.async_update_protection_data(), self.async_update_uv_index_data()] + await asyncio.gather(*tasks) class OpenUvEntity(Entity): diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index d081e09f853..59f6e4d1c67 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -76,7 +76,7 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): @property def unique_id(self) -> str: """Return a unique, HASS-friendly identifier for this entity.""" - return "{0}_{1}_{2}".format(self._latitude, self._longitude, self._sensor_type) + return f"{self._latitude}_{self._longitude}_{self._sensor_type}" async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index e86bfdac35f..de2688ab121 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -98,7 +98,7 @@ class OpenUvSensor(OpenUvEntity): @property def unique_id(self) -> str: """Return a unique, HASS-friendly identifier for this entity.""" - return "{0}_{1}_{2}".format(self._latitude, self._longitude, self._sensor_type) + return f"{self._latitude}_{self._longitude}_{self._sensor_type}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/openuv/services.yaml b/homeassistant/components/openuv/services.yaml index f353c7f4774..be9a7ba522f 100644 --- a/homeassistant/components/openuv/services.yaml +++ b/homeassistant/components/openuv/services.yaml @@ -2,4 +2,10 @@ --- update_data: - description: Request new data from OpenUV. + description: Request new data from OpenUV. Consumes two API calls. + +update_uv_index_data: + description: Request new UV index data from OpenUV. + +update_protection_data: + description: Request new protection window data from OpenUV. diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 85bd1ccb2c6..51dc92623f3 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -108,7 +108,7 @@ class OpenWeatherMapSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py index 1a49d92d1cf..f9543c7fa6e 100644 --- a/homeassistant/components/owlet/__init__.py +++ b/homeassistant/components/owlet/__init__.py @@ -58,7 +58,7 @@ def setup(hass, config): device.update_properties() if not name: - name = "{}'s Owlet".format(device.baby_name) + name = f"{device.baby_name}'s Owlet" hass.data[DOMAIN] = OwletDevice(device, name, SENSOR_TYPES) diff --git a/homeassistant/components/owntracks/.translations/it.json b/homeassistant/components/owntracks/.translations/it.json index 9b66b693c33..03b0c84744f 100644 --- a/homeassistant/components/owntracks/.translations/it.json +++ b/homeassistant/components/owntracks/.translations/it.json @@ -3,6 +3,9 @@ "abort": { "one_instance_allowed": "\u00c8 necessaria una sola istanza." }, + "create_entry": { + "default": "\n\nSu Android, apri l'[app OwnTracks]({android_url}), vai su preferenze -> connessione. Modifica le seguenti impostazioni: \n - Modalit\u00e0: HTTP privato \n - Host: {webhook_url} \n - Identificazione: \n - Nome utente: `` \n - ID dispositivo: ``\n\nSu iOS, apri l'[app OwnTracks]({ios_url}), tocca l'icona (i) in alto a sinistra -> impostazioni. Modifica le seguenti impostazioni: \n - Modalit\u00e0: HTTP \n - URL: {webhook_url} \n - Attiva autenticazione \n - UserID: `` \n\n {secret} \n \n Vedi [la documentazione]({docs_url}) per maggiori informazioni." + }, "step": { "user": { "description": "Sei sicuro di voler configurare OwnTracks?", diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index df9ae27b5ff..7e65ff3d51d 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -169,7 +169,7 @@ async def handle_webhook(hass, webhook_id, request): if user: topic_base = re.sub("/#$", "", context.mqtt_topic) - message["topic"] = "{}/{}/{}".format(topic_base, user, device) + message["topic"] = f"{topic_base}/{user}/{device}" elif message["_type"] != "encrypted": _LOGGER.warning( @@ -264,7 +264,7 @@ class OwnTracksContext: # Mobile beacons should always be set to the location of the # tracking device. I get the device state and make the necessary # changes to kwargs. - device_tracker_state = hass.states.get("device_tracker.{}".format(dev_id)) + device_tracker_state = hass.states.get(f"device_tracker.{dev_id}") if device_tracker_state is not None: acc = device_tracker_state.attributes.get("gps_accuracy") @@ -282,6 +282,6 @@ class OwnTracksContext: # kwargs location is the beacon's configured lat/lon kwargs.pop("battery", None) for beacon in self.mobile_beacons_active[dev_id]: - kwargs["dev_id"] = "{}_{}".format(BEACON_DEV_ID, beacon) + kwargs["dev_id"] = f"{BEACON_DEV_ID}_{beacon}" kwargs["host_name"] = beacon self.async_see(**kwargs) diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 61cfb9e05f9..7ef31be1327 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -60,7 +60,7 @@ def _parse_see_args(message, subscribe_topic): Async friendly. """ user, device = _parse_topic(message["topic"], subscribe_topic) - dev_id = slugify("{}_{}".format(user, device)) + dev_id = slugify(f"{user}_{device}") kwargs = {"dev_id": dev_id, "host_name": user, "attributes": {}} if message["lat"] is not None and message["lon"] is not None: kwargs["gps"] = (message["lat"], message["lon"]) @@ -253,7 +253,7 @@ async def async_handle_transition_message(hass, context, message): async def async_handle_waypoint(hass, name_base, waypoint): """Handle a waypoint.""" name = waypoint["desc"] - pretty_name = "{} - {}".format(name_base, name) + pretty_name = f"{name_base} - {name}" lat = waypoint["lat"] lon = waypoint["lon"] rad = waypoint["rad"] diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index 9ced5fc6cf4..c242670ba48 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -220,7 +220,7 @@ class PandoraMediaPlayer(MediaPlayerDevice): return _LOGGER.debug("Setting station %s, %d", source, station_index) self._send_station_list_command() - self._pianobar.sendline("{}".format(station_index)) + self._pianobar.sendline(f"{station_index}") self._pianobar.expect("\r\n") self._player_state = STATE_PLAYING diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index d18d00ef841..cf861992bd6 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -165,7 +165,7 @@ async def async_setup(hass, config): panel_path = panel.get(CONF_WEBCOMPONENT_PATH) if panel_path is None: - panel_path = hass.config.path(PANEL_DIR, "{}.html".format(name)) + panel_path = hass.config.path(PANEL_DIR, f"{name}.html") if CONF_JS_URL in panel: kwargs["js_url"] = panel[CONF_JS_URL] diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index 496ab9199c3..d026896a7c5 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -17,7 +17,7 @@ dismiss: notification_id: description: Target ID of the notification, which should be removed. [Required] example: 1234 - + mark_read: description: Mark a notification read. fields: diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index c6a2f91bab3..832853c670d 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -441,7 +441,7 @@ def ws_list_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """List persons.""" - manager = hass.data[DOMAIN] # type: PersonManager + manager: PersonManager = hass.data[DOMAIN] connection.send_result( msg["id"], {"storage": manager.storage_persons, "config": manager.config_persons}, @@ -464,7 +464,7 @@ async def ws_create_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """Create a person.""" - manager = hass.data[DOMAIN] # type: PersonManager + manager: PersonManager = hass.data[DOMAIN] try: person = await manager.async_create_person( name=msg["name"], @@ -495,7 +495,7 @@ async def ws_update_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """Update a person.""" - manager = hass.data[DOMAIN] # type: PersonManager + manager: PersonManager = hass.data[DOMAIN] changes = {} for key in ("name", "user_id", "device_trackers"): if key in msg: @@ -519,7 +519,7 @@ async def ws_delete_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """Delete a person.""" - manager = hass.data[DOMAIN] # type: PersonManager + manager: PersonManager = hass.data[DOMAIN] await manager.async_delete_person(msg["person_id"]) connection.send_result(msg["id"]) diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index 10d7fe8009d..579dc253603 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -286,7 +286,7 @@ class PhilipsTV(MediaPlayerDevice): self._tv.update() self._sources = { - srcid: source["name"] or "Source {}".format(srcid) + srcid: source["name"] or f"Source {srcid}" for srcid, source in (self._tv.sources or {}).items() } diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 432e0f3fa11..ffc9827eed4 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1 +1,96 @@ """The pi_hole component.""" +import logging + +import voluptuous as vol +from hole import Hole +from hole.exceptions import HoleError + +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.util import Throttle + +from .const import ( + DOMAIN, + CONF_LOCATION, + DEFAULT_HOST, + DEFAULT_LOCATION, + DEFAULT_NAME, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + MIN_TIME_BETWEEN_UPDATES, +) + +LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the pi_hole integration.""" + + conf = config[DOMAIN] + name = conf[CONF_NAME] + host = conf[CONF_HOST] + use_tls = conf[CONF_SSL] + verify_tls = conf[CONF_VERIFY_SSL] + location = conf[CONF_LOCATION] + + LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + + session = async_get_clientsession(hass, True) + pi_hole = PiHoleData( + Hole( + host, + hass.loop, + session, + location=location, + tls=use_tls, + verify_tls=verify_tls, + ), + name, + ) + + await pi_hole.async_update() + + hass.data[DOMAIN] = pi_hole + + hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) + + return True + + +class PiHoleData: + """Get the latest data and update the states.""" + + def __init__(self, api, name): + """Initialize the data object.""" + self.api = api + self.name = name + self.available = True + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Get the latest data from the Pi-hole.""" + + try: + await self.api.get_data() + self.available = True + except HoleError: + LOGGER.error("Unable to fetch data from Pi-hole") + self.available = False diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py new file mode 100644 index 00000000000..ba83bf1d805 --- /dev/null +++ b/homeassistant/components/pi_hole/const.py @@ -0,0 +1,43 @@ +"""Constants for the pi_hole intergration.""" +from datetime import timedelta + +DOMAIN = "pi_hole" + +CONF_LOCATION = "location" + +DEFAULT_HOST = "pi.hole" +DEFAULT_LOCATION = "admin" +DEFAULT_METHOD = "GET" +DEFAULT_NAME = "Pi-Hole" +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True + +ATTR_BLOCKED_DOMAINS = "domains_blocked" + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) + +SENSOR_DICT = { + "ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"], + "ads_percentage_today": [ + "Ads Percentage Blocked Today", + "%", + "mdi:close-octagon-outline", + ], + "clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"], + "dns_queries_today": [ + "DNS Queries Today", + "queries", + "mdi:comment-question-outline", + ], + "domains_being_blocked": ["Domains Blocked", "domains", "mdi:block-helper"], + "queries_cached": ["DNS Queries Cached", "queries", "mdi:comment-question-outline"], + "queries_forwarded": [ + "DNS Queries Forwarded", + "queries", + "mdi:comment-question-outline", + ], + "unique_clients": ["DNS Unique Clients", "clients", "mdi:account-outline"], + "unique_domains": ["DNS Unique Domains", "domains", "mdi:domain"], +} + +SENSOR_LIST = list(SENSOR_DICT) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index d60392373bc..4e80e9767a6 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -1,100 +1,27 @@ """Support for getting statistical data from a Pi-hole system.""" -from datetime import timedelta import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - CONF_SSL, - CONF_VERIFY_SSL, -) -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -_LOGGER = logging.getLogger(__name__) - -ATTR_BLOCKED_DOMAINS = "domains_blocked" -ATTR_PERCENTAGE_TODAY = "percentage_today" -ATTR_QUERIES_TODAY = "queries_today" - -CONF_LOCATION = "location" -DEFAULT_HOST = "localhost" - -DEFAULT_LOCATION = "admin" -DEFAULT_METHOD = "GET" -DEFAULT_NAME = "Pi-Hole" -DEFAULT_SSL = False -DEFAULT_VERIFY_SSL = True - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -MONITORED_CONDITIONS = { - "ads_blocked_today": ["Ads Blocked Today", "ads", "mdi:close-octagon-outline"], - "ads_percentage_today": [ - "Ads Percentage Blocked Today", - "%", - "mdi:close-octagon-outline", - ], - "clients_ever_seen": ["Seen Clients", "clients", "mdi:account-outline"], - "dns_queries_today": [ - "DNS Queries Today", - "queries", - "mdi:comment-question-outline", - ], - "domains_being_blocked": ["Domains Blocked", "domains", "mdi:block-helper"], - "queries_cached": ["DNS Queries Cached", "queries", "mdi:comment-question-outline"], - "queries_forwarded": [ - "DNS Queries Forwarded", - "queries", - "mdi:comment-question-outline", - ], - "unique_clients": ["DNS Unique Clients", "clients", "mdi:account-outline"], - "unique_domains": ["DNS Unique Domains", "domains", "mdi:domain"], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_LOCATION, default=DEFAULT_LOCATION): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_MONITORED_CONDITIONS, default=["ads_blocked_today"]): vol.All( - cv.ensure_list, [vol.In(MONITORED_CONDITIONS)] - ), - } +from .const import ( + DOMAIN as PIHOLE_DOMAIN, + ATTR_BLOCKED_DOMAINS, + SENSOR_LIST, + SENSOR_DICT, ) +LOGGER = logging.getLogger(__name__) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Pi-hole sensor.""" - from hole import Hole + """Set up the pi-hole sensor.""" + if discovery_info is None: + return - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - use_tls = config.get(CONF_SSL) - location = config.get(CONF_LOCATION) - verify_tls = config.get(CONF_VERIFY_SSL) + pi_hole = hass.data[PIHOLE_DOMAIN] - session = async_get_clientsession(hass, verify_tls) - pi_hole = PiHoleData(Hole(host, hass.loop, session, location=location, tls=use_tls)) - - await pi_hole.async_update() - - if pi_hole.api.data is None: - raise PlatformNotReady - - sensors = [ - PiHoleSensor(pi_hole, name, condition) - for condition in config[CONF_MONITORED_CONDITIONS] - ] + sensors = [] + sensors = [PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST] async_add_entities(sensors, True) @@ -102,13 +29,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class PiHoleSensor(Entity): """Representation of a Pi-hole sensor.""" - def __init__(self, pi_hole, name, condition): + def __init__(self, pi_hole, sensor_name): """Initialize a Pi-hole sensor.""" self.pi_hole = pi_hole - self._name = name - self._condition = condition + self._name = pi_hole.name + self._condition = sensor_name - variable_info = MONITORED_CONDITIONS[condition] + variable_info = SENSOR_DICT[sensor_name] self._condition_name = variable_info[0] self._unit_of_measurement = variable_info[1] self._icon = variable_info[2] @@ -117,7 +44,7 @@ class PiHoleSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._condition_name) + return f"{self._name} {self._condition_name}" @property def icon(self): @@ -151,24 +78,3 @@ class PiHoleSensor(Entity): """Get the latest data from the Pi-hole API.""" await self.pi_hole.async_update() self.data = self.pi_hole.api.data - - -class PiHoleData: - """Get the latest data and update the states.""" - - def __init__(self, api): - """Initialize the data object.""" - self.api = api - self.available = True - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest data from the Pi-hole.""" - from hole.exceptions import HoleError - - try: - await self.api.get_data() - self.available = True - except HoleError: - _LOGGER.error("Unable to fetch data from Pi-hole") - self.available = False diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index 5d4f5dd25b5..2688b15e837 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -92,7 +92,7 @@ def setup(hass, config): try: pilight_client.send_code(message_data) - except IOError: + except OSError: _LOGGER.error("Pilight send failed for %s", str(message_data)) hass.services.register(DOMAIN, SERVICE_NAME, send_code, schema=RF_CODE_SCHEMA) diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 398e77ea511..44b4055e032 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -54,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hass.data["pjlink"] = {} hass_data = hass.data["pjlink"] - device_label = "{}:{}".format(host, port) + device_label = f"{host}:{port}" if device_label in hass_data: return @@ -65,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def format_input_source(input_source_name, input_source_number): """Format input source for display in UI.""" - return "{} {}".format(input_source_name, input_source_number) + return f"{input_source_name} {input_source_number}" class PjLinkDevice(MediaPlayerDevice): diff --git a/homeassistant/components/plaato/.translations/it.json b/homeassistant/components/plaato/.translations/it.json new file mode 100644 index 00000000000..7e7697a339b --- /dev/null +++ b/homeassistant/components/plaato/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Plaato Airlook.", + "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, dovrai impostare la funzione webhook in Plaato Airlock. \n\n Inserisci le seguenti informazioni: \n\n - URL: ` {webhook_url} ` \n - Metodo: POST \n\n Vedi [la documentazione]({docs_url}) per ulteriori dettagli." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Plaato Airlock?", + "title": "Configura il webhook di Plaato" + } + }, + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 7ca5de419e0..49b749b8de6 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -37,8 +37,8 @@ ATTR_ABV = "abv" ATTR_CO2_VOLUME = "co2_volume" ATTR_BATCH_VOLUME = "batch_volume" -SENSOR_UPDATE = "{}_sensor_update".format(DOMAIN) -SENSOR_DATA_KEY = "{}.{}".format(DOMAIN, SENSOR) +SENSOR_UPDATE = f"{DOMAIN}_sensor_update" +SENSOR_DATA_KEY = f"{DOMAIN}.{SENSOR}" WEBHOOK_SCHEMA = vol.Schema( { @@ -121,7 +121,7 @@ async def handle_webhook(hass, webhook_id, request): async_dispatcher_send(hass, SENSOR_UPDATE, device_id) - return web.Response(text="Saving status for {}".format(device_id), status=HTTP_OK) + return web.Response(text=f"Saving status for {device_id}", status=HTTP_OK) def _device_id(data): diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index bf128af931a..f8e6a3e9fa7 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -54,9 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) else: for entity in devices[device_id]: - async_dispatcher_send( - hass, "{}_{}".format(PLAATO_DOMAIN, entity.unique_id) - ) + async_dispatcher_send(hass, f"{PLAATO_DOMAIN}_{entity.unique_id}") hass.data[SENSOR_DATA_KEY] = async_dispatcher_connect( hass, SENSOR_UPDATE, _update_sensor @@ -73,18 +71,18 @@ class PlaatoSensor(Entity): self._device_id = device_id self._type = sensor_type self._state = 0 - self._name = "{} {}".format(device_id, sensor_type) + self._name = f"{device_id} {sensor_type}" self._attributes = None @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(PLAATO_DOMAIN, self._name) + return f"{PLAATO_DOMAIN} {self._name}" @property def unique_id(self): """Return the unique ID of this sensor.""" - return "{}_{}".format(self._device_id, self._type) + return f"{self._device_id}_{self._type}" @property def device_info(self): @@ -157,6 +155,5 @@ class PlaatoSensor(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - "{}_{}".format(PLAATO_DOMAIN, self.unique_id), - self.async_schedule_update_ha_state, + f"{PLAATO_DOMAIN}_{self.unique_id}", self.async_schedule_update_ha_state ) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 5dff1d29e70..a516e06d55b 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -216,7 +216,7 @@ class Plant(Entity): ) else: raise HomeAssistantError( - "Unknown reading from sensor {}: {}".format(entity_id, value) + f"Unknown reading from sensor {entity_id}: {value}" ) if ATTR_UNIT_OF_MEASUREMENT in new_state.attributes: self._unit_of_measurement[reading] = new_state.attributes.get( @@ -229,10 +229,10 @@ class Plant(Entity): result = [] for sensor_name in self._sensormap.values(): params = self.READINGS[sensor_name] - value = getattr(self, "_{}".format(sensor_name)) + value = getattr(self, f"_{sensor_name}") if value is not None: if value == STATE_UNAVAILABLE: - result.append("{} unavailable".format(sensor_name)) + result.append(f"{sensor_name} unavailable") else: if sensor_name == READING_BRIGHTNESS: result.append( @@ -260,14 +260,14 @@ class Plant(Entity): if "min" in params and params["min"] in self._config: min_value = self._config[params["min"]] if value < min_value: - return "{} low".format(sensor_name) + return f"{sensor_name} low" def _check_max(self, sensor_name, value, params): """If configured, check the value against the defined maximum value.""" if "max" in params and params["max"] in self._config: max_value = self._config[params["max"]] if value > max_value: - return "{} high".format(sensor_name) + return f"{sensor_name} high" return None async def async_added_to_hass(self): @@ -352,7 +352,7 @@ class Plant(Entity): } for reading in self._sensormap.values(): - attrib[reading] = getattr(self, "_{}".format(reading)) + attrib[reading] = getattr(self, f"_{reading}") if self._brightness_history.max is not None: attrib[ATTR_MAX_BRIGHTNESS_HISTORY] = self._brightness_history.max diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 6e4e02026ab..69e77c8854f 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1 +1,208 @@ -"""The plex component.""" +"""Support to embed Plex.""" +import logging + +import plexapi.exceptions +import requests.exceptions +import voluptuous as vol + +from homeassistant.components.discovery import SERVICE_PLEX +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_TOKEN, + CONF_URL, + CONF_VERIFY_SSL, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.util.json import load_json, save_json + +from .const import ( + CONF_SERVER, + CONF_USE_EPISODE_ART, + CONF_SHOW_ALL_CONTROLS, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + DOMAIN as PLEX_DOMAIN, + PLATFORMS, + PLEX_CONFIG_FILE, + PLEX_MEDIA_PLAYER_OPTIONS, + SERVERS, +) +from .server import PlexServer + +MEDIA_PLAYER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, + vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, + } +) + +SERVER_CONFIG_SCHEMA = vol.Schema( + vol.All( + { + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_TOKEN): cv.string, + vol.Optional(CONF_SERVER): cv.string, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(MP_DOMAIN, default={}): MEDIA_PLAYER_SCHEMA, + }, + cv.has_at_least_one_key(CONF_HOST, CONF_TOKEN), + ) +) + +CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_EXTRA) + +CONFIGURING = "configuring" +_LOGGER = logging.getLogger(__package__) + + +def setup(hass, config): + """Set up the Plex component.""" + + def server_discovered(service, info): + """Pass back discovered Plex server details.""" + if hass.data[PLEX_DOMAIN][SERVERS]: + _LOGGER.debug("Plex server already configured, ignoring discovery.") + return + _LOGGER.debug("Discovered Plex server: %s:%s", info["host"], info["port"]) + setup_plex(discovery_info=info) + + def setup_plex(config=None, discovery_info=None, configurator_info=None): + """Return assembled server_config dict.""" + json_file = hass.config.path(PLEX_CONFIG_FILE) + file_config = load_json(json_file) + host_and_port = None + + if config: + server_config = config + if CONF_HOST in server_config: + host_and_port = ( + f"{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" + ) + if MP_DOMAIN in server_config: + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) + elif file_config: + _LOGGER.debug("Loading config from %s", json_file) + host_and_port, server_config = file_config.popitem() + server_config[CONF_VERIFY_SSL] = server_config.pop("verify") + elif discovery_info: + server_config = {} + host_and_port = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" + elif configurator_info: + server_config = configurator_info + host_and_port = server_config["host_and_port"] + else: + discovery.listen(hass, SERVICE_PLEX, server_discovered) + return True + + if host_and_port: + use_ssl = server_config.get(CONF_SSL, DEFAULT_SSL) + http_prefix = "https" if use_ssl else "http" + server_config[CONF_URL] = f"{http_prefix}://{host_and_port}" + + plex_server = PlexServer(server_config) + try: + plex_server.connect() + except requests.exceptions.ConnectionError as error: + _LOGGER.error( + "Plex server could not be reached, please verify host and port: [%s]", + error, + ) + return False + except ( + plexapi.exceptions.BadRequest, + plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound, + ) as error: + _LOGGER.error( + "Connection to Plex server failed, please verify token and SSL settings: [%s]", + error, + ) + request_configuration(host_and_port) + return False + else: + hass.data[PLEX_DOMAIN][SERVERS][ + plex_server.machine_identifier + ] = plex_server + + if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: + request_id = hass.data[PLEX_DOMAIN][CONFIGURING].pop(host_and_port) + configurator = hass.components.configurator + configurator.request_done(request_id) + _LOGGER.debug("Discovery configuration done") + if configurator_info: + # Write plex.conf if created via discovery/configurator + save_json( + hass.config.path(PLEX_CONFIG_FILE), + { + host_and_port: { + CONF_TOKEN: server_config[CONF_TOKEN], + CONF_SSL: use_ssl, + "verify": server_config[CONF_VERIFY_SSL], + } + }, + ) + + if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) + + for platform in PLATFORMS: + hass.helpers.discovery.load_platform( + platform, PLEX_DOMAIN, {}, original_config + ) + + return True + + def request_configuration(host_and_port): + """Request configuration steps from the user.""" + configurator = hass.components.configurator + if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: + configurator.notify_errors( + hass.data[PLEX_DOMAIN][CONFIGURING][host_and_port], + "Failed to register, please try again.", + ) + return + + def plex_configuration_callback(data): + """Handle configuration changes.""" + config = { + "host_and_port": host_and_port, + CONF_TOKEN: data.get("token"), + CONF_SSL: cv.boolean(data.get("ssl")), + CONF_VERIFY_SSL: cv.boolean(data.get("verify_ssl")), + } + setup_plex(configurator_info=config) + + hass.data[PLEX_DOMAIN][CONFIGURING][ + host_and_port + ] = configurator.request_config( + "Plex Media Server", + plex_configuration_callback, + description="Enter the X-Plex-Token", + entity_picture="/static/images/logo_plex_mediaserver.png", + submit_caption="Confirm", + fields=[ + {"id": "token", "name": "X-Plex-Token", "type": ""}, + {"id": "ssl", "name": "Use SSL", "type": ""}, + {"id": "verify_ssl", "name": "Verify SSL", "type": ""}, + ], + ) + + # End of inner functions. + + original_config = config + + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, CONFIGURING: {}}) + + if hass.data[PLEX_DOMAIN][SERVERS]: + _LOGGER.debug("Plex server already configured") + return False + + plex_config = config.get(PLEX_DOMAIN, {}) + return setup_plex(config=plex_config) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py new file mode 100644 index 00000000000..6f19623c809 --- /dev/null +++ b/homeassistant/components/plex/const.py @@ -0,0 +1,18 @@ +"""Constants for the Plex component.""" +DOMAIN = "plex" +NAME_FORMAT = "Plex {}" + +DEFAULT_PORT = 32400 +DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True + +PLATFORMS = ["media_player", "sensor"] +SERVERS = "servers" + +PLEX_CONFIG_FILE = "plex.conf" +PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" +PLEX_SERVER_CONFIG = "server_config" + +CONF_SERVER = "server" +CONF_USE_EPISODE_ART = "use_episode_art" +CONF_SHOW_ALL_CONTROLS = "show_all_controls" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 32ddb83476c..4269400dc24 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,5 +6,7 @@ "plexapi==3.0.6" ], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": [ + "@jjlawren" + ] } diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 98137897149..cfc63948bee 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -3,10 +3,12 @@ from datetime import timedelta import json import logging -import requests -import voluptuous as vol +import plexapi.exceptions +import plexapi.playlist +import plexapi.playqueue +import requests.exceptions -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -27,121 +29,33 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_time_interval from homeassistant.util import dt as dt_util -from homeassistant.util.json import load_json, save_json + +from .const import ( + CONF_USE_EPISODE_ART, + CONF_SHOW_ALL_CONTROLS, + DOMAIN as PLEX_DOMAIN, + NAME_FORMAT, + PLEX_MEDIA_PLAYER_OPTIONS, + SERVERS, +) + +SERVER_SETUP = "server_setup" _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -NAME_FORMAT = "Plex {}" -PLEX_CONFIG_FILE = "plex.conf" -PLEX_DATA = "plex" - -CONF_USE_EPISODE_ART = "use_episode_art" -CONF_SHOW_ALL_CONTROLS = "show_all_controls" -CONF_REMOVE_UNAVAILABLE_CLIENTS = "remove_unavailable_clients" -CONF_CLIENT_REMOVE_INTERVAL = "client_remove_interval" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, - vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, - vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean, - vol.Optional( - CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600) - ): vol.All(cv.time_period, cv.positive_timedelta), - } -) - def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the Plex platform.""" - if PLEX_DATA not in hass.data: - hass.data[PLEX_DATA] = {} - - # get config from plex.conf - file_config = load_json(hass.config.path(PLEX_CONFIG_FILE)) - - if file_config: - # Setup a configured PlexServer - host, host_config = file_config.popitem() - token = host_config["token"] - try: - has_ssl = host_config["ssl"] - except KeyError: - has_ssl = False - try: - verify_ssl = host_config["verify"] - except KeyError: - verify_ssl = True - - # Via discovery - elif discovery_info is not None: - # Parse discovery data - host = discovery_info.get("host") - port = discovery_info.get("port") - host = "%s:%s" % (host, port) - _LOGGER.info("Discovered PLEX server: %s", host) - - if host in _CONFIGURING: - return - token = None - has_ssl = False - verify_ssl = True - else: + if discovery_info is None: return - setup_plexserver( - host, token, has_ssl, verify_ssl, hass, config, add_entities_callback - ) + plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] + config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS] - -def setup_plexserver( - host, token, has_ssl, verify_ssl, hass, config, add_entities_callback -): - """Set up a plexserver based on host parameter.""" - import plexapi.server - import plexapi.exceptions - - cert_session = None - http_prefix = "https" if has_ssl else "http" - if has_ssl and (verify_ssl is False): - _LOGGER.info("Ignoring SSL verification") - cert_session = requests.Session() - cert_session.verify = False - try: - plexserver = plexapi.server.PlexServer( - "%s://%s" % (http_prefix, host), token, cert_session - ) - _LOGGER.info("Discovery configuration done (no token needed)") - except ( - plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.info(error) - # No token or wrong token - request_configuration(host, hass, config, add_entities_callback) - return - - # If we came here and configuring this host, mark as done - if host in _CONFIGURING: - request_id = _CONFIGURING.pop(host) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.info("Discovery configuration done") - - # Save config - save_json( - hass.config.path(PLEX_CONFIG_FILE), - {host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}}, - ) - - _LOGGER.info("Connected to: %s://%s", http_prefix, host) - - plex_clients = hass.data[PLEX_DATA] + plex_clients = {} plex_sessions = {} track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) @@ -154,7 +68,9 @@ def setup_plexserver( return except requests.exceptions.RequestException as ex: _LOGGER.warning( - "Could not connect to plex server at http://%s (%s)", host, ex + "Could not connect to Plex server: %s (%s)", + plexserver.friendly_name, + ex, ) return @@ -186,7 +102,9 @@ def setup_plexserver( return except requests.exceptions.RequestException as ex: _LOGGER.warning( - "Could not connect to plex server at http://%s (%s)", host, ex + "Could not connect to Plex server: %s (%s)", + plexserver.friendly_name, + ex, ) return @@ -215,7 +133,6 @@ def setup_plexserver( _LOGGER.debug("Refreshing session: %s", machine_identifier) plex_clients[machine_identifier].refresh(None, session) - clients_to_remove = [] for client in plex_clients.values(): # force devices to idle that do not have a valid session if client.session is None: @@ -229,59 +146,10 @@ def setup_plexserver( if client not in new_plex_clients: client.schedule_update_ha_state() - if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) or client.available: - continue - - if (dt_util.utcnow() - client.marked_unavailable) >= ( - config.get(CONF_CLIENT_REMOVE_INTERVAL) - ): - hass.add_job(client.async_remove()) - clients_to_remove.append(client.machine_identifier) - - while clients_to_remove: - del plex_clients[clients_to_remove.pop()] - if new_plex_clients: add_entities_callback(new_plex_clients) -def request_configuration(host, hass, config, add_entities_callback): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors( - _CONFIGURING[host], "Failed to register, please try again." - ) - - return - - def plex_configuration_callback(data): - """Handle configuration changes.""" - setup_plexserver( - host, - data.get("token"), - cv.boolean(data.get("has_ssl")), - cv.boolean(data.get("do_not_verify_ssl")), - hass, - config, - add_entities_callback, - ) - - _CONFIGURING[host] = configurator.request_config( - "Plex Media Server", - plex_configuration_callback, - description="Enter the X-Plex-Token", - entity_picture="/static/images/logo_plex_mediaserver.png", - submit_caption="Confirm", - fields=[ - {"id": "token", "name": "X-Plex-Token", "type": ""}, - {"id": "has_ssl", "name": "Use SSL", "type": ""}, - {"id": "do_not_verify_ssl", "name": "Do not verify SSL", "type": ""}, - ], - ) - - class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" @@ -354,9 +222,6 @@ class PlexClient(MediaPlayerDevice): def refresh(self, device, session): """Refresh key device data.""" - import plexapi.exceptions - - # new data refresh self._clear_media_details() if session: # Not being triggered by Chrome or FireTablet Plex App @@ -827,8 +692,6 @@ class PlexClient(MediaPlayerDevice): src["video_name"] ) - import plexapi.playlist - if ( media and media_type == "EPISODE" @@ -847,7 +710,7 @@ class PlexClient(MediaPlayerDevice): show = self.device.server.library.section(library_name).get(show_name) if not season_number: - playlist_name = "{} - {} Episodes".format(self.entity_id, show_name) + playlist_name = f"{self.entity_id} - {show_name} Episodes" return self.device.server.createPlaylist(playlist_name, show.episodes()) for season in show.seasons(): @@ -894,8 +757,6 @@ class PlexClient(MediaPlayerDevice): _LOGGER.error("Client cannot play media: %s", self.entity_id) return - import plexapi.playqueue - playqueue = plexapi.playqueue.PlayQueue.create( self.device.server, media, **params ) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index dbd0d9f8578..f469e95da80 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,132 +1,51 @@ """Support for Plex media server monitoring.""" from datetime import timedelta import logging -import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, - CONF_HOST, - CONF_PORT, - CONF_TOKEN, - CONF_SSL, - CONF_VERIFY_SSL, -) +import plexapi.exceptions +import requests.exceptions + from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv +from .const import DOMAIN as PLEX_DOMAIN, SERVERS + +DEFAULT_NAME = "Plex" _LOGGER = logging.getLogger(__name__) -CONF_SERVER = "server" - -DEFAULT_HOST = "localhost" -DEFAULT_NAME = "Plex" -DEFAULT_PORT = 32400 -DEFAULT_SSL = False -DEFAULT_VERIFY_SSL = True - MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_TOKEN): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SERVER): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Plex sensor.""" - name = config.get(CONF_NAME) - plex_user = config.get(CONF_USERNAME) - plex_password = config.get(CONF_PASSWORD) - plex_server = config.get(CONF_SERVER) - plex_host = config.get(CONF_HOST) - plex_port = config.get(CONF_PORT) - plex_token = config.get(CONF_TOKEN) - - plex_url = "{}://{}:{}".format( - "https" if config.get(CONF_SSL) else "http", plex_host, plex_port - ) - - import plexapi.exceptions - - try: - add_entities( - [ - PlexSensor( - name, - plex_url, - plex_user, - plex_password, - plex_server, - plex_token, - config.get(CONF_VERIFY_SSL), - ) - ], - True, - ) - except ( - plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.error(error) + if discovery_info is None: return + plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] + add_entities([PlexSensor(plexserver)], True) + class PlexSensor(Entity): """Representation of a Plex now playing sensor.""" - def __init__( - self, - name, - plex_url, - plex_user, - plex_password, - plex_server, - plex_token, - verify_ssl, - ): + def __init__(self, plex_server): """Initialize the sensor.""" - from plexapi.myplex import MyPlexAccount - from plexapi.server import PlexServer - from requests import Session - - self._name = name - self._state = 0 + self._name = DEFAULT_NAME + self._state = None self._now_playing = [] - - cert_session = None - if not verify_ssl: - _LOGGER.info("Ignoring SSL verification") - cert_session = Session() - cert_session.verify = False - - if plex_token: - self._server = PlexServer(plex_url, plex_token, cert_session) - elif plex_user and plex_password: - user = MyPlexAccount(plex_user, plex_password) - server = plex_server if plex_server else user.resources()[0].name - self._server = user.resource(server).connect() - else: - self._server = PlexServer(plex_url, None, cert_session) + self._server = plex_server + self._unique_id = f"sensor-{plex_server.machine_identifier}" @property def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the id of this plex client.""" + return self._unique_id + @property def state(self): """Return the state of the sensor.""" @@ -145,12 +64,24 @@ class PlexSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update method for Plex sensor.""" - sessions = self._server.sessions() + try: + sessions = self._server.sessions() + except plexapi.exceptions.BadRequest: + _LOGGER.error( + "Error listing current Plex sessions on %s", self._server.friendly_name + ) + return + except requests.exceptions.RequestException as ex: + _LOGGER.warning( + "Temporary error connecting to %s (%s)", self._server.friendly_name, ex + ) + return + now_playing = [] for sess in sessions: user = sess.usernames[0] device = sess.players[0].title - now_playing_user = "{0} - {1}".format(user, device) + now_playing_user = f"{user} - {device}" now_playing_title = "" if sess.TYPE == "episode": @@ -161,7 +92,7 @@ class PlexSensor(Entity): season_title += " ({0})".format(sess.show().year) season_episode = "S{0}".format(sess.parentIndex) if sess.index is not None: - season_episode += " · E{0}".format(sess.index) + season_episode += f" · E{sess.index}" episode_title = sess.title now_playing_title = "{0} - {1} - {2}".format( season_title, season_episode, episode_title @@ -181,7 +112,7 @@ class PlexSensor(Entity): # "The Incredible Hulk (2008)" now_playing_title = sess.title if sess.year is not None: - now_playing_title += " ({0})".format(sess.year) + now_playing_title += f" ({sess.year})" now_playing.append((now_playing_user, now_playing_title)) self._state = len(sessions) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py new file mode 100644 index 00000000000..962e074996f --- /dev/null +++ b/homeassistant/components/plex/server.py @@ -0,0 +1,78 @@ +"""Shared class to maintain Plex server instances.""" +import logging + +import plexapi.myplex +import plexapi.server +from requests import Session + +from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL + +from .const import CONF_SERVER, DEFAULT_VERIFY_SSL + +_LOGGER = logging.getLogger(__package__) + + +class PlexServer: + """Manages a single Plex server connection.""" + + def __init__(self, server_config): + """Initialize a Plex server instance.""" + self._plex_server = None + self._url = server_config.get(CONF_URL) + self._token = server_config.get(CONF_TOKEN) + self._server_name = server_config.get(CONF_SERVER) + self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + + def connect(self): + """Connect to a Plex server directly, obtaining direct URL if necessary.""" + + def _set_missing_url(): + account = plexapi.myplex.MyPlexAccount(token=self._token) + available_servers = [ + x.name for x in account.resources() if "server" in x.provides + ] + server_choice = ( + self._server_name if self._server_name else available_servers[0] + ) + connections = account.resource(server_choice).connections + local_url = [x.httpuri for x in connections if x.local] + remote_url = [x.uri for x in connections if not x.local] + self._url = local_url[0] if local_url else remote_url[0] + + def _connect_with_url(): + session = None + if self._url.startswith("https") and not self._verify_ssl: + session = Session() + session.verify = False + self._plex_server = plexapi.server.PlexServer( + self._url, self._token, session + ) + _LOGGER.debug("Connected to: %s (%s)", self.friendly_name, self.url_in_use) + + if self._token and not self._url: + _set_missing_url() + + _connect_with_url() + + def clients(self): + """Pass through clients call to plexapi.""" + return self._plex_server.clients() + + def sessions(self): + """Pass through sessions call to plexapi.""" + return self._plex_server.sessions() + + @property + def friendly_name(self): + """Return name of connected Plex server.""" + return self._plex_server.friendlyName + + @property + def machine_identifier(self): + """Return unique identifier of connected Plex server.""" + return self._plex_server.machineIdentifier + + @property + def url_in_use(self): + """Return URL used for connected Plex server.""" + return self._plex_server._baseurl # pylint: disable=W0212 diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index ecf423c500b..63fa67f4da5 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -94,7 +94,7 @@ class GlowRing(Light): def __init__(self, lightpad): """Initialize the light.""" self._lightpad = lightpad - self._name = "{} Glow Ring".format(lightpad.friendly_name) + self._name = f"{lightpad.friendly_name} Glow Ring" self._state = lightpad.glow_enabled self._brightness = lightpad.glow_intensity * 255.0 diff --git a/homeassistant/components/point/.translations/it.json b/homeassistant/components/point/.translations/it.json index 324801009ca..3c0ef8306e0 100644 --- a/homeassistant/components/point/.translations/it.json +++ b/homeassistant/components/point/.translations/it.json @@ -16,6 +16,7 @@ }, "step": { "auth": { + "description": "Segui il link qui sotto e Accetta l'accesso al tuo account Minut, quindi torna indietro e premi Invia qui sotto. \n\n [Link] ( {authorization_url} )", "title": "Autenticare Point" }, "user": { diff --git a/homeassistant/components/point/.translations/ko.json b/homeassistant/components/point/.translations/ko.json index d70859c8bde..0dd9cd43ada 100644 --- a/homeassistant/components/point/.translations/ko.json +++ b/homeassistant/components/point/.translations/ko.json @@ -8,7 +8,7 @@ "no_flows": "Point \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Point \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/point/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "create_entry": { - "default": "Point \uae30\uae30\ub294 Minut \ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "default": "Point \uae30\uae30\ub85c Minut \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "follow_link": "Submit \ubc84\ud2bc\uc744 \ub204\ub974\uae30 \uc804\uc5d0 \ub9c1\ud06c\ub97c \ub530\ub77c \uc778\uc99d\uc744 \ubc1b\uc544\uc8fc\uc138\uc694", diff --git a/homeassistant/components/point/.translations/pl.json b/homeassistant/components/point/.translations/pl.json index 66b454e47ff..ca36001cc1a 100644 --- a/homeassistant/components/point/.translations/pl.json +++ b/homeassistant/components/point/.translations/pl.json @@ -16,7 +16,7 @@ }, "step": { "auth": { - "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do swojego konta Minut, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", + "description": "Kliknij poni\u017cszy link i Zaakceptuj dost\u0119p do konta Minut, a nast\u0119pnie wr\u00f3\u0107 i naci\u015bnij Prze\u015blij poni\u017cej. \n\n [Link]({authorization_url})", "title": "Uwierzytelnienie Point" }, "user": { diff --git a/homeassistant/components/point/.translations/zh-Hant.json b/homeassistant/components/point/.translations/zh-Hant.json index 91a86f5e3db..9f688b2e5f9 100644 --- a/homeassistant/components/point/.translations/zh-Hant.json +++ b/homeassistant/components/point/.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", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", - "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Point \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/point/\uff09\u3002" + "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Point \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/point/)\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Minut Point \u88dd\u7f6e\u3002" diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index f0931bc9e8f..e9885891553 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -182,7 +182,7 @@ class MinutPointClient: async def new_device(device_id, component): """Load new device.""" - config_entries_key = "{}.{}".format(component, DOMAIN) + config_entries_key = f"{component}.{DOMAIN}" async with self._hass.data[DATA_CONFIG_ENTRY_LOCK]: if config_entries_key not in self._hass.data[CONFIG_ENTRY_IS_SETUP]: await self._hass.config_entries.async_forward_entry_setup( @@ -247,7 +247,7 @@ class MinutPointEntity(Entity): def __str__(self): """Return string representation of device.""" - return "MinutPoint {}".format(self.name) + return f"MinutPoint {self.name}" async def async_added_to_hass(self): """Call when entity is added to hass.""" @@ -333,7 +333,7 @@ class MinutPointEntity(Entity): @property def unique_id(self): """Return the unique id of the sensor.""" - return "point.{}-{}".format(self._id, self.device_class) + return f"point.{self._id}-{self.device_class}" @property def value(self): diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index 4a0db111b7d..f9e725f6c8e 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -108,7 +108,7 @@ class MinutPointAlarmControl(AlarmControlPanel): @property def unique_id(self): """Return the unique id of the sensor.""" - return "point.{}".format(self._home_id) + return f"point.{self._home_id}" @property def device_info(self): diff --git a/homeassistant/components/point/strings.json b/homeassistant/components/point/strings.json index 642a61a5f9d..e5491a8bbee 100644 --- a/homeassistant/components/point/strings.json +++ b/homeassistant/components/point/strings.json @@ -29,4 +29,4 @@ "authorize_url_fail": "Unknown error generating an authorize url." } } -} +} diff --git a/homeassistant/components/prezzibenzina/sensor.py b/homeassistant/components/prezzibenzina/sensor.py index 420cd448c19..f1f41ba46ba 100644 --- a/homeassistant/components/prezzibenzina/sensor.py +++ b/homeassistant/components/prezzibenzina/sensor.py @@ -77,7 +77,7 @@ class PrezziBenzinaSensor(Entity): self._index = index self._data = None self._station = station - self._name = "{} {} {}".format(name, ft, srv) + self._name = f"{name} {ft} {srv}" @property def name(self): diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index e9b85f79084..1f86958d08e 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -71,7 +71,7 @@ def setup_proximity_component(hass, name, config): zone_id, unit_of_measurement, ) - proximity.entity_id = "{}.{}".format(DOMAIN, proximity_zone) + proximity.entity_id = f"{DOMAIN}.{proximity_zone}" proximity.schedule_update_ha_state() @@ -211,8 +211,8 @@ class Proximity(Entity): # Loop through each of the distances collected and work out the # closest. - closest_device = None # type: str - dist_to_zone = None # type: float + closest_device: str = None + dist_to_zone: float = None for device in distances_to_zone: if not dist_to_zone or distances_to_zone[device] < dist_to_zone: diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index 7d145315748..53a4f620dcc 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -66,7 +66,7 @@ def _precheck_image(image, opts): raise ValueError() try: img = Image.open(io.BytesIO(image)) - except IOError: + except OSError: _LOGGER.warning("Failed to open image") raise ValueError() imgfmt = str(img.format) diff --git a/homeassistant/components/ps4/.translations/it.json b/homeassistant/components/ps4/.translations/it.json index afa32056757..de5eb4e5e6f 100644 --- a/homeassistant/components/ps4/.translations/it.json +++ b/homeassistant/components/ps4/.translations/it.json @@ -4,11 +4,12 @@ "credential_error": "Errore nel recupero delle credenziali.", "devices_configured": "Tutti i dispositivi trovati sono gi\u00e0 configurati.", "no_devices_found": "Nessun dispositivo PlayStation 4 trovato in rete.", - "port_987_bind_error": "Impossibile connettersi alla porta 987.", - "port_997_bind_error": "Impossibile connettersi alla porta 997." + "port_987_bind_error": "Impossibile collegarsi alla porta 987. Per ulteriori informazioni, consultare la [documentazione] (https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", + "port_997_bind_error": "Impossibile collegarsi alla porta 997. Consultare la [documentazione] (https://www.home-assistant.io/components/ps4/) per ulteriori informazioni." }, "error": { - "login_failed": "Accoppiamento alla PlayStation 4 fallito. Verifica che il PIN sia corretto.", + "credential_timeout": "Servizio credenziali scaduto. Premi Invia per riavviare.", + "login_failed": "Impossibile eseguire l'associazione a PlayStation 4. Verificare che il PIN sia corretto.", "no_ipaddress": "Inserisci l'indirizzo IP della PlayStation 4 che desideri configurare.", "not_ready": "La PlayStation 4 non \u00e8 accesa o non \u00e8 collegata alla rete." }, @@ -24,7 +25,7 @@ "name": "Nome", "region": "Area geografica" }, - "description": "Inserisci le informazioni della tua PlayStation 4. Per il \"PIN\", vai su \"Impostazioni\" sulla tua console PlayStation 4. Quindi accedi a \"Impostazioni connessione app mobile\" e seleziona \"Aggiungi dispositivo\". Inserisci il PIN che viene visualizzato.", + "description": "Inserisci le tue informazioni su PlayStation 4. Per \"PIN\", vai a \"Impostazioni\" sulla console PlayStation 4. Quindi vai a 'Impostazioni di connessione app mobile' e seleziona 'Aggiungi dispositivo'. Immettere il PIN visualizzato. Fare riferimento alla [documentazione](https://www.home-assistant.io/components/ps4/) per ulteriori informazioni.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json index f13a66d5e8a..25f64cd21e9 100644 --- a/homeassistant/components/ps4/.translations/ko.json +++ b/homeassistant/components/ps4/.translations/ko.json @@ -3,7 +3,7 @@ "abort": { "credential_error": "\uc790\uaca9 \uc99d\uba85\uc744 \uac00\uc838\uc624\ub294 \uc911 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "devices_configured": "\ubc1c\uacac \ub41c \ubaa8\ub4e0 \uae30\uae30\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "no_devices_found": "PlayStation 4 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "no_devices_found": "PlayStation 4 \uae30\uae30\ub97c \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "port_987_bind_error": "\ud3ec\ud2b8 987 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "port_997_bind_error": "\ud3ec\ud2b8 997 \uc5d0 \ubc14\uc778\ub529 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ucd94\uac00 \uc815\ubcf4\ub294 [\uc548\ub0b4](https://www.home-assistant.io/components/ps4/) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, diff --git a/homeassistant/components/ps4/.translations/pl.json b/homeassistant/components/ps4/.translations/pl.json index 3e36960b12c..9fb4c73f1d0 100644 --- a/homeassistant/components/ps4/.translations/pl.json +++ b/homeassistant/components/ps4/.translations/pl.json @@ -8,7 +8,7 @@ "port_997_bind_error": "Nie mo\u017cna powi\u0105za\u0107 z portem 997." }, "error": { - "credential_timeout": "Up\u0142yn\u0105\u0142 limit czasu us\u0142ugi po\u015bwiadcze\u0144. Naci\u015bnij przycisk Prze\u015blij, aby ponownie uruchomi\u0107.", + "credential_timeout": "Up\u0142yn\u0105\u0142 limit czasu us\u0142ugi po\u015bwiadcze\u0144. Naci\u015bnij przycisk Prze\u015blij, aby ponowi\u0107.", "login_failed": "Nie uda\u0142o si\u0119 sparowa\u0107 z PlayStation 4. Sprawd\u017a, czy PIN jest poprawny.", "no_ipaddress": "Wprowad\u017a adres IP PlayStation 4, kt\u00f3ry chcesz skonfigurowa\u0107.", "not_ready": "PlayStation 4 nie jest w\u0142\u0105czona lub po\u0142\u0105czona z sieci\u0105." diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 9baf1adbcc2..60635bba525 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -156,7 +156,7 @@ async def async_migrate_entry(hass, entry): def format_unique_id(creds, mac_address): """Use last 4 Chars of credential as suffix. Unique ID per PSN user.""" suffix = creds[-4:] - return "{}_{}".format(mac_address, suffix) + return f"{mac_address}_{suffix}" def load_games(hass: HomeAssistantType) -> dict: diff --git a/homeassistant/components/pushetta/notify.py b/homeassistant/components/pushetta/notify.py index 2bdd7d036ce..b8911039f3f 100644 --- a/homeassistant/components/pushetta/notify.py +++ b/homeassistant/components/pushetta/notify.py @@ -61,9 +61,7 @@ class PushettaNotificationService(BaseNotificationService): title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) try: - self.pushetta.pushMessage( - self._channel_name, "{} {}".format(title, message) - ) + self.pushetta.pushMessage(self._channel_name, f"{title} {message}") except exceptions.TokenValidationError: _LOGGER.error("Please check your access token") self.is_valid = False diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index 461b2540bef..758a3390286 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -132,7 +132,7 @@ class PushsaferNotificationService(BaseNotificationService): return None base64_image = base64.b64encode(filebyte).decode("utf8") - return "data:{};base64,{}".format(mimetype, base64_image) + return f"data:{mimetype};base64,{base64_image}" def load_from_url(self, url=None, username=None, password=None, auth=None): """Load image/document/etc from URL.""" diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 07c23cd9e80..8ffe1ece4a2 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -55,7 +55,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) monitored_types = config.get(CONF_MONITORED_VARIABLES) - url = "http{}://{}:{}/api/".format(ssl, host, port) + url = f"http{ssl}://{host}:{port}/api/" try: pyloadapi = PyLoadAPI(api_url=url, username=username, password=password) diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 715c06aca43..af0865bc685 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -113,7 +113,7 @@ def discover_scripts(hass): @bind_hass def execute_script(hass, name, data=None): """Execute a script.""" - filename = "{}.py".format(name) + filename = f"{name}.py" with open(hass.config.path(FOLDER, sanitize_filename(filename))) as fil: source = fil.read() execute(hass, filename, source, data) @@ -166,9 +166,7 @@ def execute(hass, filename, source, data=None): or isinstance(obj, TimeWrapper) and name not in ALLOWED_TIME ): - raise ScriptError( - "Not allowed to access {}.{}".format(obj.__class__.__name__, name) - ) + raise ScriptError(f"Not allowed to access {obj.__class__.__name__}.{name}") return getattr(obj, name, default) @@ -188,7 +186,7 @@ def execute(hass, filename, source, data=None): "_iter_unpack_sequence_": guarded_iter_unpack_sequence, "_unpack_sequence_": guarded_unpack_sequence, } - logger = logging.getLogger("{}.{}".format(__name__, filename)) + logger = logging.getLogger(f"{__name__}.{filename}") local = {"hass": hass, "data": data or {}, "logger": logger} try: diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 2900496a01e..f00b392065c 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -88,7 +88,7 @@ class QBittorrentSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/qld_bushfire/geo_location.py b/homeassistant/components/qld_bushfire/geo_location.py index e8d32c036d5..8ae80ca9027 100644 --- a/homeassistant/components/qld_bushfire/geo_location.py +++ b/homeassistant/components/qld_bushfire/geo_location.py @@ -198,6 +198,11 @@ class QldBushfireLocationEvent(GeolocationEvent): self._updated_date = feed_entry.updated self._status = feed_entry.status + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:fire" + @property def source(self) -> str: """Return source value of this external event.""" diff --git a/homeassistant/components/qnap/sensor.py b/homeassistant/components/qnap/sensor.py index 8ab2ee575bf..efbb1ac26ca 100644 --- a/homeassistant/components/qnap/sensor.py +++ b/homeassistant/components/qnap/sensor.py @@ -215,8 +215,8 @@ class QNAPSensor(Entity): server_name = self._api.data["system_stats"]["system"]["name"] if self.monitor_device is not None: - return "{} {} ({})".format(server_name, self.var_name, self.monitor_device) - return "{} {}".format(server_name, self.var_name) + return f"{server_name} {self.var_name} ({self.monitor_device})" + return f"{server_name} {self.var_name}" @property def icon(self): @@ -270,7 +270,7 @@ class QNAPMemorySensor(QNAPSensor): if self._api.data: data = self._api.data["system_stats"]["memory"] size = round_nicely(float(data["total"]) / 1024) - return {ATTR_MEMORY_SIZE: "{} GB".format(size)} + return {ATTR_MEMORY_SIZE: f"{size} GB"} class QNAPNetworkSensor(QNAPSensor): @@ -331,7 +331,7 @@ class QNAPSystemSensor(QNAPSensor): ATTR_NAME: data["system"]["name"], ATTR_MODEL: data["system"]["model"], ATTR_SERIAL: data["system"]["serial_number"], - ATTR_UPTIME: "{:0>2d}d {:0>2d}h {:0>2d}m".format(days, hours, minutes), + ATTR_UPTIME: f"{days:0>2d}d {hours:0>2d}h {minutes:0>2d}m", } diff --git a/homeassistant/components/qwikswitch/__init__.py b/homeassistant/components/qwikswitch/__init__.py index 9e4c0658358..1ae92b0a18a 100644 --- a/homeassistant/components/qwikswitch/__init__.py +++ b/homeassistant/components/qwikswitch/__init__.py @@ -80,7 +80,7 @@ class QSEntity(Entity): @property def unique_id(self): """Return a unique identifier for this sensor.""" - return "qs{}".format(self.qsid) + return f"qs{self.qsid}" @callback def update_packet(self, packet): diff --git a/homeassistant/components/qwikswitch/binary_sensor.py b/homeassistant/components/qwikswitch/binary_sensor.py index 36e8181cc47..a5b142e19ae 100644 --- a/homeassistant/components/qwikswitch/binary_sensor.py +++ b/homeassistant/components/qwikswitch/binary_sensor.py @@ -61,7 +61,7 @@ class QSBinarySensor(QSEntity, BinarySensorDevice): @property def unique_id(self): """Return a unique identifier for this sensor.""" - return "qs{}:{}".format(self.qsid, self.channel) + return f"qs{self.qsid}:{self.channel}" @property def device_class(self): diff --git a/homeassistant/components/qwikswitch/sensor.py b/homeassistant/components/qwikswitch/sensor.py index 8e9a755d6da..01964fc7831 100644 --- a/homeassistant/components/qwikswitch/sensor.py +++ b/homeassistant/components/qwikswitch/sensor.py @@ -34,7 +34,7 @@ class QSSensor(QSEntity): self._decode, self.unit = SENSORS[sensor_type] if isinstance(self.unit, type): - self.unit = "{}:{}".format(sensor_type, self.channel) + self.unit = f"{sensor_type}:{self.channel}" @callback def update_packet(self, packet): @@ -60,7 +60,7 @@ class QSSensor(QSEntity): @property def unique_id(self): """Return a unique identifier for this sensor.""" - return "qs{}:{}".format(self.qsid, self.channel) + return f"qs{self.qsid}:{self.channel}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 0d582b0c2e9..2030512ab31 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -226,7 +226,7 @@ class RachioIro: def __str__(self) -> str: """Display the controller as a string.""" - return 'Rachio controller "{}"'.format(self.name) + return f'Rachio controller "{self.name}"' @property def controller_id(self) -> str: diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 01d38a931c4..f74e3ca1802 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -87,12 +87,12 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): @property def name(self) -> str: """Return the name of this sensor including the controller name.""" - return "{} online".format(self._controller.name) + return f"{self._controller.name} online" @property def unique_id(self) -> str: """Return a unique id for this entity.""" - return "{}-online".format(self._controller.controller_id) + return f"{self._controller.controller_id}-online" @property def device_class(self) -> str: diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index b65e6bf6044..80c227a6df6 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -72,7 +72,7 @@ class RachioSwitch(SwitchDevice): @property def name(self) -> str: """Get a name for this switch.""" - return "Switch on {}".format(self._controller.name) + return f"Switch on {self._controller.name}" @property def is_on(self) -> bool: @@ -113,12 +113,12 @@ class RachioStandbySwitch(RachioSwitch): @property def name(self) -> str: """Return the name of the standby switch.""" - return "{} in standby mode".format(self._controller.name) + return f"{self._controller.name} in standby mode" @property def unique_id(self) -> str: """Return a unique id by combining controller id and purpose.""" - return "{}-standby".format(self._controller.controller_id) + return f"{self._controller.controller_id}-standby" @property def icon(self) -> str: @@ -183,7 +183,7 @@ class RachioZone(RachioSwitch): @property def unique_id(self) -> str: """Return a unique id by combining controller id and zone number.""" - return "{}-zone-{}".format(self._controller.controller_id, self.zone_id) + return f"{self._controller.controller_id}-zone-{self.zone_id}" @property def icon(self) -> str: diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index f2c3e229c95..a007dd673ac 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,5 +1,4 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" -import datetime import logging import voluptuous as vol @@ -11,6 +10,11 @@ from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + FAN_ON, + FAN_OFF, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_COOL, SUPPORT_TARGET_TEMPERATURE, SUPPORT_FAN_MODE, ) @@ -21,12 +25,12 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, STATE_ON, ) +from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -ATTR_FAN = "fan" -ATTR_MODE = "mode" +ATTR_FAN_ACTION = "fan_action" CONF_HOLD_TEMP = "hold_temp" @@ -55,11 +59,11 @@ FAN_MODE_TO_CODE = {v: k for k, v in CODE_TO_FAN_MODE.items()} # Active thermostat state (is it heating or cooling?). In the future # this should probably made into heat and cool binary sensors. -CODE_TO_TEMP_STATE = {0: HVAC_MODE_OFF, 1: HVAC_MODE_HEAT, 2: HVAC_MODE_COOL} +CODE_TO_TEMP_STATE = {0: CURRENT_HVAC_IDLE, 1: CURRENT_HVAC_HEAT, 2: CURRENT_HVAC_COOL} # Active fan state. This is if the fan is actually on or not. In the # future this should probably made into a binary sensor for the fan. -CODE_TO_FAN_STATE = {0: HVAC_MODE_OFF, 1: STATE_ON} +CODE_TO_FAN_STATE = {0: FAN_OFF, 1: FAN_ON} def round_temp(temperature): @@ -160,7 +164,7 @@ class RadioThermostat(ClimateDevice): @property def device_state_attributes(self): """Return the device specific state attributes.""" - return {ATTR_FAN: self._fstate, ATTR_MODE: self._tstate} + return {ATTR_FAN_ACTION: self._fstate} @property def fan_modes(self): @@ -200,6 +204,13 @@ class RadioThermostat(ClimateDevice): """Return the operation modes list.""" return OPERATION_LIST + @property + def hvac_action(self): + """Return the current running hvac operation if supported.""" + if self.hvac_mode == HVAC_MODE_OFF: + return None + return self._tstate + @property def target_temperature(self): """Return the temperature we try to reach.""" @@ -261,9 +272,9 @@ class RadioThermostat(ClimateDevice): # This doesn't really work - tstate is only set if the HVAC is # active. If it's idle, we don't know what to do with the target # temperature. - if self._tstate == HVAC_MODE_COOL: + if self._tstate == CURRENT_HVAC_COOL: self._target_temperature = data["t_cool"] - elif self._tstate == HVAC_MODE_HEAT: + elif self._tstate == CURRENT_HVAC_HEAT: self._target_temperature = data["t_heat"] else: self._current_operation = HVAC_MODE_OFF @@ -281,9 +292,9 @@ class RadioThermostat(ClimateDevice): elif self._current_operation == HVAC_MODE_HEAT: self.device.t_heat = temperature elif self._current_operation == HVAC_MODE_AUTO: - if self._tstate == HVAC_MODE_COOL: + if self._tstate == CURRENT_HVAC_COOL: self.device.t_cool = temperature - elif self._tstate == HVAC_MODE_HEAT: + elif self._tstate == CURRENT_HVAC_HEAT: self.device.t_heat = temperature # Only change the hold if requested or if hold mode was turned @@ -299,7 +310,7 @@ class RadioThermostat(ClimateDevice): """Set device time.""" # Calling this clears any local temperature override and # reverts to the scheduled temperature. - now = datetime.datetime.now() + now = dt_util.now() self.device.time = { "day": now.weekday(), "hour": now.hour, diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index a1b82bc1af7..868e8ff4c7d 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -53,7 +53,7 @@ class RainBirdSwitch(SwitchDevice): self._rainbird = rb self._devid = dev_id self._zone = int(dev.get(CONF_ZONE)) - self._name = dev.get(CONF_FRIENDLY_NAME, "Sprinkler {}".format(self._zone)) + self._name = dev.get(CONF_FRIENDLY_NAME, f"Sprinkler {self._zone}") self._state = None self._duration = dev.get(CONF_TRIGGER_TIME) self._attributes = {"duration": self._duration, "zone": self._zone} diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index 9891ac50f48..9ab6156549d 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane", "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" }, "step": { @@ -11,7 +11,7 @@ "password": "Has\u0142o", "port": "Port" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "RainMachine" diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index b04384dc81d..183872087a7 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -41,9 +41,9 @@ _LOGGER = logging.getLogger(__name__) DATA_LISTENER = "listener" -PROGRAM_UPDATE_TOPIC = "{0}_program_update".format(DOMAIN) -SENSOR_UPDATE_TOPIC = "{0}_data_update".format(DOMAIN) -ZONE_UPDATE_TOPIC = "{0}_zone_update".format(DOMAIN) +PROGRAM_UPDATE_TOPIC = f"{DOMAIN}_program_update" +SENSOR_UPDATE_TOPIC = f"{DOMAIN}_data_update" +ZONE_UPDATE_TOPIC = f"{DOMAIN}_zone_update" CONF_CONTROLLERS = "controllers" CONF_PROGRAM_ID = "program_id" diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 0c3833528dc..118b6fb3709 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -66,7 +66,7 @@ class RecollectWasteSensor(Entity): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}{}".format(self.client.place_id, self.client.service_id) + return f"{self.client.place_id}{self.client.service_id}" @property def state(self): diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 0d814a5d74b..9d34cc6fb79 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -7,7 +7,7 @@ import logging import queue import threading import time -from typing import Any, Dict, Optional # noqa: F401 +from typing import Any, Dict, Optional import voluptuous as vol @@ -177,12 +177,12 @@ class Recorder(threading.Thread): self.hass = hass self.keep_days = keep_days self.purge_interval = purge_interval - self.queue = queue.Queue() # type: Any + self.queue: Any = queue.Queue() self.recording_start = dt_util.utcnow() self.db_url = uri self.async_db_ready = asyncio.Future() - self.engine = None # type: Any - self.run_info = None # type: Any + self.engine: Any = None + self.run_info: Any = None self.entity_filter = generate_filter( include.get(CONF_DOMAINS, []), diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index c91b910724c..9ecfa88053f 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,7 +3,7 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/components/recorder", "requirements": [ - "sqlalchemy==1.3.7" + "sqlalchemy==1.3.8" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index aee993fa104..3de0430d8f3 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -107,7 +107,7 @@ def _drop_index(engine, table_name, index_name): # Engines like DB2/Oracle try: - engine.execute(text("DROP INDEX {index}".format(index=index_name))) + engine.execute(text(f"DROP INDEX {index_name}")) except SQLAlchemyError: pass else: @@ -170,7 +170,7 @@ def _add_columns(engine, table_name, columns_def): table_name, ) - columns_def = ["ADD {}".format(col_def) for col_def in columns_def] + columns_def = [f"ADD {col_def}" for col_def in columns_def] try: engine.execute( @@ -265,9 +265,7 @@ def _apply_update(engine, new_version, old_version): # 'context_parent_id CHARACTER(36)', # ]) else: - raise ValueError( - "No schema migration defined for version {}".format(new_version) - ) + raise ValueError(f"No schema migration defined for version {new_version}") def _inspect_schema_version(engine, session): diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 0e95bea9091..f9c8140f60d 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -94,7 +94,7 @@ class RedditSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "reddit_{}".format(self._subreddit) + return f"reddit_{self._subreddit}" @property def state(self): diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py old mode 100755 new mode 100644 index 3172e614166..61cb319fd11 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -220,7 +220,7 @@ class PublicTransportData: and due_at_time is not None and route is not None ): - due_at = "{} {}".format(due_at_date, due_at_time) + due_at = f"{due_at_date} {due_at_time}" departure_data = { ATTR_DUE_IN: due_in_minutes(due_at), diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 3d340d9c07e..c92a246da14 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -87,13 +87,13 @@ def _create_instance( component.add_entities([entity]) hass.services.register( DOMAIN, - "{}_create_task".format(account_name), + f"{account_name}_create_task", entity.create_task, schema=SERVICE_SCHEMA_CREATE_TASK, ) hass.services.register( DOMAIN, - "{}_complete_task".format(account_name), + f"{account_name}_complete_task", entity.complete_task, schema=SERVICE_SCHEMA_COMPLETE_TASK, ) @@ -137,7 +137,7 @@ def _register_new_account( configurator.request_done(request_id) request_id = configurator.async_request_config( - "{} - {}".format(DOMAIN, account_name), + f"{DOMAIN} - {account_name}", callback=register_account_callback, description="You need to log in to Remember The Milk to" + "connect your account. \n\n" diff --git a/homeassistant/components/remote_rpi_gpio/__init__.py b/homeassistant/components/remote_rpi_gpio/__init__.py index ccefd00c723..33356d0e3b8 100644 --- a/homeassistant/components/remote_rpi_gpio/__init__.py +++ b/homeassistant/components/remote_rpi_gpio/__init__.py @@ -47,7 +47,7 @@ def setup_input(address, port, pull_mode, bouncetime): bounce_time=bouncetime, pin_factory=PiGPIOFactory(address), ) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return None diff --git a/homeassistant/components/remote_rpi_gpio/binary_sensor.py b/homeassistant/components/remote_rpi_gpio/binary_sensor.py index 8c7d7b7d023..e12d83324fd 100644 --- a/homeassistant/components/remote_rpi_gpio/binary_sensor.py +++ b/homeassistant/components/remote_rpi_gpio/binary_sensor.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): button = remote_rpi_gpio.setup_input( address, port_num, pull_mode, bouncetime ) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return new_sensor = RemoteRPiGPIOBinarySensor(port_name, button, invert_logic) devices.append(new_sensor) diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index aa20a2909d2..8240de7951d 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -36,7 +36,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for port, name in ports.items(): try: led = remote_rpi_gpio.setup_output(address, port, invert_logic) - except (ValueError, IndexError, KeyError, IOError): + except (ValueError, IndexError, KeyError, OSError): return new_switch = RemoteRPiGPIOSwitch(name, led, invert_logic) devices.append(new_switch) diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py old mode 100755 new mode 100644 index 1643966b33e..6f72a6b7ddc --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -239,7 +239,7 @@ class PrinterAPI: info["name"] = printer.slug info["printer_name"] = self.conf_name - known = "{}-{}".format(printer.slug, sensor_type) + known = f"{printer.slug}-{sensor_type}" if known in self._known_entities: continue diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json old mode 100755 new mode 100644 diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/rflink/cover.py b/homeassistant/components/rflink/cover.py index 7e6de0ec03b..f41c4cde2f7 100644 --- a/homeassistant/components/rflink/cover.py +++ b/homeassistant/components/rflink/cover.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice -from homeassistant.const import CONF_NAME, STATE_OPEN +from homeassistant.const import CONF_NAME, CONF_TYPE, STATE_OPEN import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -23,6 +23,8 @@ from . import ( _LOGGER = logging.getLogger(__name__) +TYPE_STANDARD = "standard" +TYPE_INVERTED = "inverted" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -33,6 +35,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { cv.string: { vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_TYPE): vol.Any(TYPE_STANDARD, TYPE_INVERTED), vol.Optional(CONF_ALIASES, default=[]): vol.All( cv.ensure_list, [cv.string] ), @@ -52,12 +55,51 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +def entity_type_for_device_id(device_id): + """Return entity class for protocol of a given device_id. + + Async friendly. + """ + entity_type_mapping = { + # KlikAanKlikUit cover have the controls inverted + "newkaku": TYPE_INVERTED + } + protocol = device_id.split("_")[0] + return entity_type_mapping.get(protocol, TYPE_STANDARD) + + +def entity_class_for_type(entity_type): + """Translate entity type to entity class. + + Async friendly. + """ + entity_device_mapping = { + # default cover implementation + TYPE_STANDARD: RflinkCover, + # cover with open/close commands inverted + # like KAKU/COCO ASUN-650 + TYPE_INVERTED: InvertedRflinkCover, + } + + return entity_device_mapping.get(entity_type, RflinkCover) + + def devices_from_config(domain_config): """Parse configuration and add Rflink cover devices.""" devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): + # Determine what kind of entity to create, RflinkCover + # or InvertedRflinkCover + if CONF_TYPE in config: + # Remove type from config to not pass it as and argument + # to entity instantiation + entity_type = config.pop(CONF_TYPE) + else: + entity_type = entity_type_for_device_id(device_id) + + entity_class = entity_class_for_type(entity_type) device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - device = RflinkCover(device_id, **device_config) + device = entity_class(device_id, **device_config) devices.append(device) return devices @@ -115,3 +157,13 @@ class RflinkCover(RflinkCommand, CoverDevice, RestoreEntity): def async_stop_cover(self, **kwargs): """Turn the device stop.""" return self._async_handle_command("stop_cover") + + +class InvertedRflinkCover(RflinkCover): + """Rflink cover that has inverted open/close commands.""" + + async def _async_send_command(self, cmd, repetitions): + """Will invert only the UP/DOWN commands.""" + _LOGGER.debug("Getting command: %s for Rflink device: %s", cmd, self._device_id) + cmd_inv = {"UP": "DOWN", "DOWN": "UP"} + await super()._async_send_command(cmd_inv.get(cmd, cmd), repetitions) diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 56ae6f8675f..682d45f8f42 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -300,7 +300,7 @@ class ToggleRflinkLight(SwitchableRflinkDevice, Light): @property def entity_id(self): """Return entity id.""" - return "light.{}".format(self.name) + return f"light.{self.name}" def _handle_event(self, event): """Adjust state if Rflink picks up a remote command for this device.""" diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 0c9a98143c8..79b3054ecf2 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -106,7 +106,7 @@ def setup(hass, config): slugify(event.device.id_string.lower()), event.device.__class__.__name__, event.device.subtype, - "".join("{0:02x}".format(x) for x in event.data), + "".join(f"{x:02x}" for x in event.data), ) # Callback to HA registered components. @@ -270,7 +270,7 @@ def get_new_device(event, config, device): if not config[ATTR_AUTOMATIC_ADD]: return - pkt_id = "".join("{0:02x}".format(x) for x in event.data) + pkt_id = "".join(f"{x:02x}" for x in event.data) _LOGGER.debug( "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)", device_id, diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index d4ed874156e..8f1c7e6fa55 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -116,7 +116,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): poss_id = slugify(poss_dev.event.device.id_string.lower()) _LOGGER.debug("Found possible matching device ID: %s", poss_id) - pkt_id = "".join("{0:02x}".format(x) for x in event.data) + pkt_id = "".join(f"{x:02x}" for x in event.data) sensor = RfxtrxBinarySensor(event, pkt_id) sensor.hass = hass rfxtrx.RFX_DEVICES[device_id] = sensor diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 69397263a62..5941b00764b 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -98,7 +98,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not config[CONF_AUTOMATIC_ADD]: return - pkt_id = "".join("{0:02x}".format(x) for x in event.data) + pkt_id = "".join(f"{x:02x}" for x in event.data) _LOGGER.info("Automatic add rfxtrx.sensor: %s", pkt_id) data_type = "" @@ -141,7 +141,7 @@ class RfxtrxSensor(Entity): @property def name(self): """Get the name of the sensor.""" - return "{} {}".format(self._name, self.data_type) + return f"{self._name} {self.data_type}" @property def device_state_attributes(self): diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 1b06a1d47d1..6806df0408f 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -73,7 +73,7 @@ class RingBinarySensor(BinarySensorDevice): ) self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] self._state = None - self._unique_id = "{}-{}".format(self._data.id, self._sensor_type) + self._unique_id = f"{self._data.id}-{self._sensor_type}" @property def name(self): diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index bd7ea3a3679..5805114252e 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -56,7 +56,7 @@ class RingLight(Light): @property def name(self): """Name of the light.""" - return "{} light".format(self._device.name) + return f"{self._device.name} light" @property def unique_id(self): diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index 9950609c10f..af661f4571c 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -121,7 +121,7 @@ class RingSensor(Entity): ) self._state = None self._tz = str(hass.config.time_zone) - self._unique_id = "{}-{}".format(self._data.id, self._sensor_type) + self._unique_id = f"{self._data.id}-{self._sensor_type}" async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 3b6bd4ea024..cbbecb1a403 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -38,7 +38,7 @@ class BaseRingSwitch(SwitchDevice): """Initialize the switch.""" self._device = device self._device_type = device_type - self._unique_id = "{}-{}".format(self._device.id, self._device_type) + self._unique_id = f"{self._device.id}-{self._device_type}" async def async_added_to_hass(self): """Register callbacks.""" @@ -53,7 +53,7 @@ class BaseRingSwitch(SwitchDevice): @property def name(self): """Name of the device.""" - return "{} {}".format(self._device.name, self._device_type) + return f"{self._device.name} {self._device_type}" @property def unique_id(self): diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index e6dd05b9328..aa13814ee6b 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -78,7 +78,7 @@ def scan_for_rokus(hass): "Name: {0}
Host: {1}
".format( r_info.userdevicename if r_info.userdevicename - else "{} {}".format(r_info.modelname, r_info.serial_num), + else f"{r_info.modelname} {r_info.serial_num}", roku.host, ) ) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 03060361020..d69b0eddb71 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -92,7 +92,7 @@ class RokuDevice(MediaPlayerDevice): """Return the name of the device.""" if self._device_info.user_device_name: return self._device_info.user_device_name - return "Roku {}".format(self._device_info.serial_num) + return f"Roku {self._device_info.serial_num}" @property def state(self): diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index 0bb840e9531..f443b7e8e74 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -36,7 +36,7 @@ class RokuRemote(remote.RemoteDevice): """Return the name of the device.""" if self._device_info.user_device_name: return self._device_info.user_device_name - return "Roku {}".format(self._device_info.serial_num) + return f"Roku {self._device_info.serial_num}" @property def unique_id(self): diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index 766fd72cdba..291658e19f4 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -327,7 +327,7 @@ class RoombaVacuum(VacuumDevice): pos_y = pos_state.get("point", {}).get("y") theta = pos_state.get("theta") if all(item is not None for item in [pos_x, pos_y, theta]): - position = "({}, {}, {})".format(pos_x, pos_y, theta) + position = f"({pos_x}, {pos_y}, {theta})" self._state_attrs[ATTR_POSITION] = position # Not all Roombas have a bin full sensor diff --git a/homeassistant/components/route53/__init__.py b/homeassistant/components/route53/__init__.py index 906f09a5649..3dffc3ffd9e 100644 --- a/homeassistant/components/route53/__init__.py +++ b/homeassistant/components/route53/__init__.py @@ -104,7 +104,7 @@ def _update_route53( { "Action": "UPSERT", "ResourceRecordSet": { - "Name": "{}.{}".format(record, domain), + "Name": f"{record}.{domain}", "Type": "A", "TTL": ttl, "ResourceRecords": [{"Value": ipaddress}], diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index c39bf5ca4f3..fe0b5dead84 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -96,7 +96,7 @@ class RovaSensor(Entity): @property def name(self): """Return the name.""" - return "{}_{}".format(self.platform_name, self.sensor_key) + return f"{self.platform_name}_{self.sensor_key}" @property def icon(self): diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index f2533e3dc86..ed16331e912 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -79,7 +79,7 @@ class RTorrentSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 642dd27b1d8..58624c758d9 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -42,7 +42,7 @@ class SabnzbdSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._client_name, self._name) + return f"{self._client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 934ee94e65d..2821a05261b 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): tv_name = discovery_info.get("name") model = discovery_info.get("model_name") host = discovery_info.get("host") - name = "{} ({})".format(tv_name, model) + name = f"{tv_name} ({model})" port = DEFAULT_PORT timeout = DEFAULT_TIMEOUT mac = None diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index d810d50cfbf..5a3223a8508 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -209,7 +209,7 @@ class ScriptEntity(ToggleEntity): await self.script.async_run(kwargs.get(ATTR_VARIABLES), context) except Exception as err: # pylint: disable=broad-except self.script.async_log_exception( - _LOGGER, "Error executing script {}".format(self.entity_id), err + _LOGGER, f"Error executing script {self.entity_id}", err ) raise err diff --git a/homeassistant/components/scsgate/__init__.py b/homeassistant/components/scsgate/__init__.py index acb7f78a2aa..739a2949d17 100644 --- a/homeassistant/components/scsgate/__init__.py +++ b/homeassistant/components/scsgate/__init__.py @@ -77,11 +77,11 @@ class SCSGate: """Handle a messages seen on the bus.""" from scsgate.messages import StateMessage, ScenarioTriggeredMessage - self._logger.debug("Received message {}".format(message)) + self._logger.debug(f"Received message {message}") if not isinstance(message, StateMessage) and not isinstance( message, ScenarioTriggeredMessage ): - msg = "Ignored message {} - not relevant type".format(message) + msg = f"Ignored message {message} - not relevant type" self._logger.debug(msg) return @@ -97,7 +97,7 @@ class SCSGate: try: self._devices[message.entity].process_event(message) except Exception as exception: # pylint: disable=broad-except - msg = "Exception while processing event: {}".format(exception) + msg = f"Exception while processing event: {exception}" self._logger.error(msg) else: self._logger.info( diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 8ad289b9200..36474620b03 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -80,7 +80,7 @@ class Sense(Entity): def __init__(self, data, name, sensor_type, is_production, update_call): """Initialize the Sense sensor.""" name_type = PRODUCTION_NAME if is_production else CONSUMPTION_NAME - self._name = "{} {}".format(name, name_type) + self._name = f"{name} {name_type}" self._data = data self._sensor_type = sensor_type self.update_sensor = update_call diff --git a/homeassistant/components/serial_pm/sensor.py b/homeassistant/components/serial_pm/sensor.py index 80952672487..1d46b05d46e 100644 --- a/homeassistant/components/serial_pm/sensor.py +++ b/homeassistant/components/serial_pm/sensor.py @@ -51,7 +51,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if config.get(CONF_NAME) is not None: name = "{} PM{}".format(config.get(CONF_NAME), pmname) else: - name = "PM{}".format(pmname) + name = f"PM{pmname}" dev.append(ParticulateMatterSensor(coll, name, pmname)) add_entities(dev) diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 14539e342f1..33abe2f1f86 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -120,7 +120,7 @@ class SeventeenTrackSummarySensor(Entity): @property def name(self): """Return the name.""" - return "Seventeentrack Packages {0}".format(self._status) + return f"Seventeentrack Packages {self._status}" @property def state(self): @@ -203,7 +203,7 @@ class SeventeenTrackPackageSensor(Entity): name = self._friendly_name if not name: name = self._tracking_number - return "Seventeentrack Package: {0}".format(name) + return f"Seventeentrack Package: {name}" @property def state(self): diff --git a/homeassistant/components/shiftr/__init__.py b/homeassistant/components/shiftr/__init__.py index a7e82ef66cf..8e698d283cf 100644 --- a/homeassistant/components/shiftr/__init__.py +++ b/homeassistant/components/shiftr/__init__.py @@ -69,10 +69,7 @@ def setup(hass, config): if state.attributes: for attribute, data in state.attributes.items(): mqttc.publish( - "/{}/{}".format(topic, attribute), - str(data), - qos=0, - retain=False, + f"/{topic}/{attribute}", str(data), qos=0, retain=False ) except RuntimeError: pass diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 075d819655b..3c9cb4391a7 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -196,7 +196,7 @@ class AddItemIntent(intent.IntentHandler): intent_obj.hass.data[DOMAIN].async_add(item) response = intent_obj.create_response() - response.async_set_speech("I've added {} to your shopping list".format(item)) + response.async_set_speech(f"I've added {item} to your shopping list") intent_obj.hass.bus.async_fire(EVENT) return response diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index 0961017b65c..b890880389c 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -90,7 +90,7 @@ class SigfoxAPI: """Get the device_id of each device registered.""" devices = [] for unique_type in device_types: - location_url = "devicetypes/{}/devices".format(unique_type) + location_url = f"devicetypes/{unique_type}/devices" url = urljoin(API_URL, location_url) response = requests.get(url, auth=self._auth, timeout=10) devices_data = json.loads(response.text)["data"] @@ -117,12 +117,12 @@ class SigfoxDevice(Entity): self._device_id = device_id self._auth = auth self._message_data = {} - self._name = "{}_{}".format(name, device_id) + self._name = f"{name}_{device_id}" self._state = None def get_last_message(self): """Return the last message from a device.""" - device_url = "devices/{}/messages?limit=1".format(self._device_id) + device_url = f"devices/{self._device_id}/messages?limit=1" url = urljoin(API_URL, device_url) response = requests.get(url, auth=self._auth, timeout=10) data = json.loads(response.text)["data"][0] diff --git a/homeassistant/components/simplisafe/.translations/it.json b/homeassistant/components/simplisafe/.translations/it.json index 134bfae3668..6f0e403a356 100644 --- a/homeassistant/components/simplisafe/.translations/it.json +++ b/homeassistant/components/simplisafe/.translations/it.json @@ -9,7 +9,7 @@ "data": { "code": "Codice (Home Assistant)", "password": "Password", - "username": "Indirizzo email" + "username": "Indirizzo E-mail" }, "title": "Inserisci i tuoi dati" } diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index 0b83ba8cbed..c4d616600f5 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "identifier_exists": "Konto jest ju\u017c zarejestrowane", "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" }, "step": { @@ -11,7 +11,7 @@ "password": "Has\u0142o", "username": "Adres e-mail" }, - "title": "Wprowad\u017a swoje dane" + "title": "Wprowad\u017a dane" } }, "title": "SimpliSafe" diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index eea97fb37fb..109c410c16d 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -34,7 +34,7 @@ class SkyHubDeviceScanner(DeviceScanner): _LOGGER.info("Initialising Sky Hub") self.host = config.get(CONF_HOST, "192.168.1.254") self.last_results = {} - self.url = "http://{}/".format(self.host) + self.url = f"http://{self.host}/" # Test the router is accessible data = _get_skyhub_data(self.url) @@ -94,7 +94,7 @@ def _parse_skyhub_response(data_str): """Parse the Sky Hub data format.""" pattmatch = re.search("attach_dev = '(.*)'", data_str) if pattmatch is None: - raise IOError( + raise OSError( "Error: Impossible to fetch data from" + " Sky Hub. Try to reboot the router." ) diff --git a/homeassistant/components/skybell/camera.py b/homeassistant/components/skybell/camera.py index 87cfdce6dfc..87dc3c0bf8d 100644 --- a/homeassistant/components/skybell/camera.py +++ b/homeassistant/components/skybell/camera.py @@ -57,7 +57,7 @@ class SkybellCamera(SkybellDevice, Camera): SkybellDevice.__init__(self, device) Camera.__init__(self) if name is not None: - self._name = "{} {}".format(self._device.name, name) + self._name = f"{self._device.name} {name}" else: self._name = self._device.name self._url = None diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py new file mode 100644 index 00000000000..54154ae863e --- /dev/null +++ b/homeassistant/components/slide/__init__.py @@ -0,0 +1,157 @@ +"""Component for the Go Slide API.""" + +import logging +from datetime import timedelta + +import voluptuous as vol +from goslideapi import GoSlideCloud, goslideapi + +from homeassistant.const import ( + CONF_USERNAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.event import async_track_time_interval, async_call_later +from .const import DOMAIN, SLIDES, API, COMPONENT, DEFAULT_RETRY + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the Slide platform.""" + + async def update_slides(now=None): + """Update slide information.""" + result = await hass.data[DOMAIN][API].slides_overview() + + if result is None: + _LOGGER.error("Slide API does not work or returned an error") + return + + if result: + _LOGGER.debug("Slide API returned %d slide(s)", len(result)) + else: + _LOGGER.warning("Slide API returned 0 slides") + + for slide in result: + if "device_id" not in slide: + _LOGGER.error( + "Found invalid Slide entry, device_id is " "missing. Entry=%s", + slide, + ) + continue + + uid = slide["device_id"].replace("slide_", "") + slidenew = hass.data[DOMAIN][SLIDES].setdefault(uid, {}) + slidenew["mac"] = uid + slidenew["id"] = slide["id"] + slidenew["name"] = slide["device_name"] + slidenew["state"] = None + oldpos = slidenew.get("pos") + slidenew["pos"] = None + slidenew["online"] = False + + if "device_info" not in slide: + _LOGGER.error( + "Slide %s (%s) has no device_info Entry=%s", + slide["id"], + slidenew["mac"], + slide, + ) + continue + + # Check if we have pos (OK) or code (NOK) + if "pos" in slide["device_info"]: + slidenew["online"] = True + slidenew["pos"] = slide["device_info"]["pos"] + slidenew["pos"] = max(0, min(1, slidenew["pos"])) + + if oldpos is None or oldpos == slidenew["pos"]: + slidenew["state"] = ( + STATE_CLOSED if slidenew["pos"] > 0.95 else STATE_OPEN + ) + elif oldpos < slidenew["pos"]: + slidenew["state"] = ( + STATE_CLOSED if slidenew["pos"] >= 0.95 else STATE_CLOSING + ) + else: + slidenew["state"] = ( + STATE_OPEN if slidenew["pos"] <= 0.05 else STATE_OPENING + ) + elif "code" in slide["device_info"]: + _LOGGER.warning( + "Slide %s (%s) is offline with " "code=%s", + slide["id"], + slidenew["mac"], + slide["device_info"]["code"], + ) + else: + _LOGGER.error( + "Slide %s (%s) has invalid device_info %s", + slide["id"], + slidenew["mac"], + slide["device_info"], + ) + + _LOGGER.debug("Updated entry=%s", slidenew) + + async def retry_setup(now): + """Retry setup if a connection/timeout happens on Slide API.""" + await async_setup(hass, config) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][SLIDES] = {} + + username = config[DOMAIN][CONF_USERNAME] + password = config[DOMAIN][CONF_PASSWORD] + scaninterval = config[DOMAIN][CONF_SCAN_INTERVAL] + + hass.data[DOMAIN][API] = GoSlideCloud(username, password) + + try: + result = await hass.data[DOMAIN][API].login() + except (goslideapi.ClientConnectionError, goslideapi.ClientTimeoutError) as err: + _LOGGER.error( + "Error connecting to Slide Cloud: %s, going to retry in %s seconds", + err, + DEFAULT_RETRY, + ) + async_call_later(hass, DEFAULT_RETRY, retry_setup) + return True + + if not result: + _LOGGER.error("Slide API returned unknown error during authentication") + return False + + _LOGGER.debug("Slide API successfully authenticated") + + await update_slides() + + hass.async_create_task(async_load_platform(hass, COMPONENT, DOMAIN, {}, config)) + + async_track_time_interval(hass, update_slides, scaninterval) + + return True diff --git a/homeassistant/components/slide/const.py b/homeassistant/components/slide/const.py new file mode 100644 index 00000000000..de3d2e560c1 --- /dev/null +++ b/homeassistant/components/slide/const.py @@ -0,0 +1,7 @@ +"""Define constants for the Go Slide component.""" + +API = "api" +COMPONENT = "cover" +DOMAIN = "slide" +SLIDES = "slides" +DEFAULT_RETRY = 120 diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py new file mode 100644 index 00000000000..1c4e6da5aac --- /dev/null +++ b/homeassistant/components/slide/cover.py @@ -0,0 +1,124 @@ +"""Support for Go Slide slides.""" + +import logging + +from homeassistant.const import ATTR_ID +from homeassistant.components.cover import ( + ATTR_POSITION, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, + DEVICE_CLASS_CURTAIN, + CoverDevice, +) +from .const import API, DOMAIN, SLIDES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up cover(s) for Go Slide platform.""" + + if discovery_info is None: + return + + entities = [] + + for slide in hass.data[DOMAIN][SLIDES].values(): + _LOGGER.debug("Setting up Slide entity: %s", slide) + entities.append(SlideCover(hass.data[DOMAIN][API], slide)) + + async_add_entities(entities) + + +class SlideCover(CoverDevice): + """Representation of a Go Slide cover.""" + + def __init__(self, api, slide): + """Initialize the cover.""" + self._api = api + self._slide = slide + self._id = slide["id"] + self._unique_id = slide["mac"] + self._name = slide["name"] + + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + + @property + def name(self): + """Return the device name.""" + return self._name + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + return {ATTR_ID: self._id} + + @property + def is_opening(self): + """Return if the cover is opening or not.""" + return self._slide["state"] == STATE_OPENING + + @property + def is_closing(self): + """Return if the cover is closing or not.""" + return self._slide["state"] == STATE_CLOSING + + @property + def is_closed(self): + """Return None if status is unknown, True if closed, else False.""" + if self._slide["state"] is None: + return None + return self._slide["state"] == STATE_CLOSED + + @property + def available(self): + """Return False if state is not available.""" + return self._slide["online"] + + @property + def assumed_state(self): + """Let HA know the integration is assumed state.""" + return True + + @property + def device_class(self): + """Return the device class of the cover.""" + return DEVICE_CLASS_CURTAIN + + @property + def current_cover_position(self): + """Return the current position of cover shutter.""" + pos = self._slide["pos"] + if pos is not None: + pos = int(pos * 100) + return pos + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._slide["state"] = STATE_OPENING + await self._api.slide_open(self._id) + + async def async_close_cover(self, **kwargs): + """Close the cover.""" + self._slide["state"] = STATE_CLOSING + await self._api.slide_close(self._id) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._api.slide_stop(self._id) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + position = kwargs[ATTR_POSITION] / 100 + + if self._slide["pos"] is not None: + if position > self._slide["pos"]: + self._slide["state"] = STATE_CLOSING + else: + self._slide["state"] = STATE_OPENING + + await self._api.slide_set_position(self._id, position) diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json new file mode 100644 index 00000000000..f9fd7f242b6 --- /dev/null +++ b/homeassistant/components/slide/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "slide", + "name": "Slide", + "documentation": "https://www.home-assistant.io/components/slide", + "requirements": [ + "goslide-api==0.5.1" + ], + "dependencies": [], + "codeowners": [ + "@ualex73" + ] +} diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 34aed146cf0..56e10b03d2a 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -19,7 +19,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.const import MINOR_VERSION, MAJOR_VERSION _LOGGER = logging.getLogger(__name__) @@ -31,7 +30,6 @@ CONF_SENSORS = "sensors" CONF_UNIT = "unit" GROUPS = ["user", "installer"] -OLD_CONFIG_DEPRECATED = MAJOR_VERSION > 0 or MINOR_VERSION > 98 def _check_sensor_schema(conf): @@ -45,29 +43,6 @@ def _check_sensor_schema(conf): customs = list(conf[CONF_CUSTOM].keys()) - if isinstance(conf[CONF_SENSORS], dict): - msg = '"sensors" should be a simple list from 0.99' - if OLD_CONFIG_DEPRECATED: - raise vol.Invalid(msg) - _LOGGER.warning(msg) - valid.extend(customs) - - for sname, attrs in conf[CONF_SENSORS].items(): - if sname not in valid: - raise vol.Invalid("{} does not exist".format(sname)) - if attrs: - _LOGGER.warning( - "Attributes on sensors will be deprecated in 0.99. Start using only individual sensors: %s: %s", - sname, - ", ".join(attrs), - ) - for attr in attrs: - if attr in valid: - continue - raise vol.Invalid("{} does not exist [{}]".format(attr, sname)) - return conf - - # Sensors is a list (only option from from 0.99) for sensor in conf[CONF_SENSORS]: if sensor in customs: _LOGGER.warning( @@ -75,7 +50,7 @@ def _check_sensor_schema(conf): sensor, ) elif sensor not in valid: - raise vol.Invalid("{} does not exist".format(sensor)) + raise vol.Invalid(f"{sensor} does not exist") return conf @@ -242,7 +217,7 @@ class SMAsensor(Entity): update = False for sens in self._sub_sensors: # Can be remove from 0.99 - newval = "{} {}".format(sens.value, sens.unit) + newval = f"{sens.value} {sens.unit}" if self._attr[sens.name] != newval: update = True self._attr[sens.name] = newval @@ -256,4 +231,4 @@ class SMAsensor(Entity): @property def unique_id(self): """Return a unique identifier for this sensor.""" - return "sma-{}-{}".format(self._sensor.key, self._sensor.name) + return f"sma-{self._sensor.key}-{self._sensor.name}" diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index cdbd1d18c29..28abf759d09 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -139,7 +139,7 @@ class SmappeeSensor(Entity): else: location_name = "Local" - return "{} {} {}".format(SENSOR_PREFIX, location_name, self._name) + return f"{SENSOR_PREFIX} {location_name} {self._name}" @property def icon(self): diff --git a/homeassistant/components/smartthings/.translations/es.json b/homeassistant/components/smartthings/.translations/es.json index 9ae98bcb9f1..513b8ba3ffe 100644 --- a/homeassistant/components/smartthings/.translations/es.json +++ b/homeassistant/components/smartthings/.translations/es.json @@ -5,7 +5,7 @@ "app_setup_error": "No se pudo configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", "base_url_not_https": "La 'base_url' del componente 'http' debe empezar por 'https://'.", "token_already_setup": "El token ya ha sido configurado.", - "token_forbidden": "El token no tiene los alcances necesarios de OAuth.", + "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", "webhook_error": "SmartThings no ha podido validar el endpoint configurado en 'base_url'. Por favor, revisa los requisitos del componente." diff --git a/homeassistant/components/smartthings/.translations/it.json b/homeassistant/components/smartthings/.translations/it.json index 486a61847a7..c2b17eed04d 100644 --- a/homeassistant/components/smartthings/.translations/it.json +++ b/homeassistant/components/smartthings/.translations/it.json @@ -5,6 +5,7 @@ "app_setup_error": "Impossibile configurare SmartApp. Riprovare.", "base_url_not_https": "Il `base_url` per il componente `http` deve essere configurato e deve iniziare con `https://`.", "token_already_setup": "Il token \u00e8 gi\u00e0 stato configurato.", + "token_forbidden": "Il token non dispone degli ambiti OAuth necessari.", "token_invalid_format": "Il token deve essere nel formato UID/GUID", "token_unauthorized": "Il token non \u00e8 valido o non \u00e8 pi\u00f9 autorizzato.", "webhook_error": "SmartThings non ha potuto convalidare l'endpoint configurato in `base_url`. Si prega di rivedere i requisiti del componente." @@ -18,6 +19,7 @@ "title": "Inserisci il Token di Accesso Personale" }, "wait_install": { + "description": "Si prega di installare l'Home Assistant SmartApp in almeno una posizione e fare clic su Invia.", "title": "Installa SmartApp" } }, diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 555fc6ec765..93f7cbb8f32 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -78,8 +78,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Initialize config entry which represents an installed SmartApp.""" if not validate_webhook_requirements(hass): _LOGGER.warning( - "The 'base_url' of the 'http' integration must be " - "configured and start with 'https://'" + "The 'base_url' of the 'http' integration must be configured and start with 'https://'" ) return False @@ -121,8 +120,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): await device.status.refresh() except ClientResponseError: _LOGGER.debug( - "Unable to update status for device: %s (%s), " - "the device will be excluded", + "Unable to update status for device: %s (%s), the device will be excluded", device.label, device.device_id, exc_info=True, @@ -148,8 +146,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): except ClientResponseError as ex: if ex.status in (401, 403): _LOGGER.exception( - "Unable to setup config entry '%s' - please " - "reconfigure the integration", + "Unable to setup config entry '%s' - please reconfigure the integration", entry.title, ) remove_entry = True @@ -186,9 +183,7 @@ async def async_get_entry_scenes(entry: ConfigEntry, api): except ClientResponseError as ex: if ex.status == 403: _LOGGER.exception( - "Unable to load scenes for config entry '%s' " - "because the access token does not have the " - "required access", + "Unable to load scenes for config entry '%s' because the access token does not have the required access", entry.title, ) else: @@ -235,7 +230,7 @@ async def async_remove_entry(hass: HomeAssistantType, entry: ConfigEntry) -> Non app_count = sum(1 for entry in all_entries if entry.data[CONF_APP_ID] == app_id) if app_count > 1: _LOGGER.debug( - "App %s was not removed because it is in use by other" "config entries", + "App %s was not removed because it is in use by other config entries", app_id, ) return diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index 1ddbee6b827..1e90709fc82 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -66,12 +66,12 @@ class SmartThingsBinarySensor(SmartThingsEntity, BinarySensorDevice): @property def name(self) -> str: """Return the name of the binary sensor.""" - return "{} {}".format(self._device.label, self._attribute) + return f"{self._device.label} {self._attribute}" @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}.{}".format(self._device.device_id, self._attribute) + return f"{self._device.device_id}.{self._attribute}" @property def is_on(self): diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index bb307523e97..4f005a326cd 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -406,7 +406,7 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice): modes.add(state) else: _LOGGER.debug( - "Device %s (%s) returned an invalid supported " "AC mode: %s", + "Device %s (%s) returned an invalid supported AC mode: %s", self._device.label, self._device.device_id, mode, diff --git a/homeassistant/components/smartthings/const.py b/homeassistant/components/smartthings/const.py index cd9fc1ccdf8..c258101da70 100644 --- a/homeassistant/components/smartthings/const.py +++ b/homeassistant/components/smartthings/const.py @@ -37,8 +37,5 @@ SUPPORTED_PLATFORMS = [ "scene", ] TOKEN_REFRESH_INTERVAL = timedelta(days=14) -VAL_UID = ( - "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]" - "{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" -) +VAL_UID = "^(?:([0-9a-fA-F]{32})|([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}))$" VAL_UID_MATCHER = re.compile(VAL_UID) diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 423c141e4da..3a6f9167054 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -283,12 +283,12 @@ class SmartThingsSensor(SmartThingsEntity): @property def name(self) -> str: """Return the name of the binary sensor.""" - return "{} {}".format(self._device.label, self._name) + return f"{self._device.label} {self._name}" @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}.{}".format(self._device.device_id, self._attribute) + return f"{self._device.device_id}.{self._attribute}" @property def state(self): diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index b152ba3328f..d205c1d245c 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -77,8 +77,7 @@ async def validate_installed_app(api, installed_app_id: str): installed_app = await api.installed_app(installed_app_id) if installed_app.installed_app_status != InstalledAppStatus.AUTHORIZED: raise RuntimeWarning( - "Installed SmartApp instance '{}' ({}) is not " - "AUTHORIZED but instead {}".format( + "Installed SmartApp instance '{}' ({}) is not AUTHORIZED but instead {}".format( installed_app.display_name, installed_app.installed_app_id, installed_app.installed_app_status, @@ -113,7 +112,7 @@ def _get_app_template(hass: HomeAssistantType): cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] if cloudhook_url is not None: endpoint = "via Nabu Casa" - description = "{} {}".format(hass.config.location_name, endpoint) + description = f"{hass.config.location_name} {endpoint}" return { "app_name": APP_NAME_PREFIX + str(uuid4()), @@ -321,7 +320,7 @@ async def smartapp_sync_subscriptions( ) except Exception as error: # pylint:disable=broad-except _LOGGER.error( - "Failed to create subscription for '%s' under app " "'%s': %s", + "Failed to create subscription for '%s' under app '%s': %s", target, installed_app_id, error, @@ -331,8 +330,7 @@ async def smartapp_sync_subscriptions( try: await api.delete_subscription(installed_app_id, sub.subscription_id) _LOGGER.debug( - "Removed subscription for '%s' under app '%s' " - "because it was no longer needed", + "Removed subscription for '%s' under app '%s' because it was no longer needed", sub.capability, installed_app_id, ) diff --git a/homeassistant/components/smarty/binary_sensor.py b/homeassistant/components/smarty/binary_sensor.py index 2d79700db78..8723f0248d3 100644 --- a/homeassistant/components/smarty/binary_sensor.py +++ b/homeassistant/components/smarty/binary_sensor.py @@ -69,9 +69,7 @@ class BoostSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Alarm Sensor Init.""" - super().__init__( - name="{} Boost State".format(name), device_class=None, smarty=smarty - ) + super().__init__(name=f"{name} Boost State", device_class=None, smarty=smarty) def update(self) -> None: """Update state.""" @@ -84,9 +82,7 @@ class AlarmSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Alarm Sensor Init.""" - super().__init__( - name="{} Alarm".format(name), device_class="problem", smarty=smarty - ) + super().__init__(name=f"{name} Alarm", device_class="problem", smarty=smarty) def update(self) -> None: """Update state.""" @@ -99,9 +95,7 @@ class WarningSensor(SmartyBinarySensor): def __init__(self, name, smarty): """Warning Sensor Init.""" - super().__init__( - name="{} Warning".format(name), device_class="problem", smarty=smarty - ) + super().__init__(name=f"{name} Warning", device_class="problem", smarty=smarty) def update(self) -> None: """Update state.""" diff --git a/homeassistant/components/smarty/sensor.py b/homeassistant/components/smarty/sensor.py index 16d910beeb5..bf647777b52 100644 --- a/homeassistant/components/smarty/sensor.py +++ b/homeassistant/components/smarty/sensor.py @@ -88,7 +88,7 @@ class SupplyAirTemperatureSensor(SmartySensor): def __init__(self, name, smarty): """Supply Air Temperature Init.""" super().__init__( - name="{} Supply Air Temperature".format(name), + name=f"{name} Supply Air Temperature", device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, @@ -106,7 +106,7 @@ class ExtractAirTemperatureSensor(SmartySensor): def __init__(self, name, smarty): """Supply Air Temperature Init.""" super().__init__( - name="{} Extract Air Temperature".format(name), + name=f"{name} Extract Air Temperature", device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, @@ -124,7 +124,7 @@ class OutdoorAirTemperatureSensor(SmartySensor): def __init__(self, name, smarty): """Outdoor Air Temperature Init.""" super().__init__( - name="{} Outdoor Air Temperature".format(name), + name=f"{name} Outdoor Air Temperature", device_class=DEVICE_CLASS_TEMPERATURE, unit_of_measurement=TEMP_CELSIUS, smarty=smarty, @@ -142,7 +142,7 @@ class SupplyFanSpeedSensor(SmartySensor): def __init__(self, name, smarty): """Supply Fan Speed RPM Init.""" super().__init__( - name="{} Supply Fan Speed".format(name), + name=f"{name} Supply Fan Speed", device_class=None, unit_of_measurement=None, smarty=smarty, @@ -160,7 +160,7 @@ class ExtractFanSpeedSensor(SmartySensor): def __init__(self, name, smarty): """Extract Fan Speed RPM Init.""" super().__init__( - name="{} Extract Fan Speed".format(name), + name=f"{name} Extract Fan Speed", device_class=None, unit_of_measurement=None, smarty=smarty, @@ -178,7 +178,7 @@ class FilterDaysLeftSensor(SmartySensor): def __init__(self, name, smarty): """Filter Days Left Init.""" super().__init__( - name="{} Filter Days Left".format(name), + name=f"{name} Filter Days Left", device_class=DEVICE_CLASS_TIMESTAMP, unit_of_measurement=None, smarty=smarty, diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 29a8c300944..5f6722b72a6 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -102,7 +102,7 @@ class SmhiWeather(WeatherEntity): @property def unique_id(self) -> str: """Return a unique id.""" - return "{}, {}".format(self._latitude, self._longitude) + return f"{self._latitude}, {self._longitude}" @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 7336b4577fa..8a96865ab8d 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -182,7 +182,7 @@ class MailNotificationService(BaseNotificationService): msg["Subject"] = subject msg["To"] = ",".join(self.recipients) if self._sender_name: - msg["From"] = "{} <{}>".format(self._sender_name, self._sender) + msg["From"] = f"{self._sender_name} <{self._sender}>" else: msg["From"] = self._sender msg["X-Mailer"] = "HomeAssistant" @@ -225,18 +225,18 @@ def _build_multipart_msg(message, images): msg.attach(msg_alt) body_txt = MIMEText(message) msg_alt.attach(body_txt) - body_text = ["

{}


".format(message)] + body_text = [f"

{message}


"] for atch_num, atch_name in enumerate(images): - cid = "image{}".format(atch_num) - body_text.append('
'.format(cid)) + cid = f"image{atch_num}" + body_text.append(f'
') try: with open(atch_name, "rb") as attachment_file: file_bytes = attachment_file.read() try: attachment = MIMEImage(file_bytes) msg.attach(attachment) - attachment.add_header("Content-ID", "<{}>".format(cid)) + attachment.add_header("Content-ID", f"<{cid}>") except TypeError: _LOGGER.warning( "Attachment %s has an unknown MIME type. " @@ -271,7 +271,7 @@ def _build_html_msg(text, html, images): with open(atch_name, "rb") as attachment_file: attachment = MIMEImage(attachment_file.read(), filename=name) msg.attach(attachment) - attachment.add_header("Content-ID", "<{}>".format(name)) + attachment.add_header("Content-ID", f"<{name}>") except FileNotFoundError: _LOGGER.warning( "Attachment %s [#%s] not found. Skipping", atch_name, atch_num diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 454201319ad..81cd6538578 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -98,7 +98,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= return # Note: Host part is needed, when using multiple snapservers - hpid = "{}:{}".format(host, port) + hpid = f"{host}:{port}" groups = [SnapcastGroupDevice(group, hpid) for group in server.groups] clients = [SnapcastClientDevice(client, hpid) for client in server.clients] @@ -114,7 +114,7 @@ class SnapcastGroupDevice(MediaPlayerDevice): """Initialize the Snapcast group device.""" group.set_callback(self.schedule_update_ha_state) self._group = group - self._uid = "{}{}_{}".format(GROUP_PREFIX, uid_part, self._group.identifier) + self._uid = f"{GROUP_PREFIX}{uid_part}_{self._group.identifier}" @property def state(self): @@ -133,7 +133,7 @@ class SnapcastGroupDevice(MediaPlayerDevice): @property def name(self): """Return the name of the device.""" - return "{}{}".format(GROUP_PREFIX, self._group.identifier) + return f"{GROUP_PREFIX}{self._group.identifier}" @property def source(self): @@ -163,7 +163,7 @@ class SnapcastGroupDevice(MediaPlayerDevice): @property def device_state_attributes(self): """Return the state attributes.""" - name = "{} {}".format(self._group.friendly_name, GROUP_SUFFIX) + name = f"{self._group.friendly_name} {GROUP_SUFFIX}" return {"friendly_name": name} @property @@ -204,7 +204,7 @@ class SnapcastClientDevice(MediaPlayerDevice): """Initialize the Snapcast client device.""" client.set_callback(self.schedule_update_ha_state) self._client = client - self._uid = "{}{}_{}".format(CLIENT_PREFIX, uid_part, self._client.identifier) + self._uid = f"{CLIENT_PREFIX}{uid_part}_{self._client.identifier}" @property def unique_id(self): @@ -223,7 +223,7 @@ class SnapcastClientDevice(MediaPlayerDevice): @property def name(self): """Return the name of the device.""" - return "{}{}".format(CLIENT_PREFIX, self._client.identifier) + return f"{CLIENT_PREFIX}{self._client.identifier}" @property def source(self): @@ -260,7 +260,7 @@ class SnapcastClientDevice(MediaPlayerDevice): @property def device_state_attributes(self): """Return the state attributes.""" - name = "{} {}".format(self._client.friendly_name, CLIENT_SUFFIX) + name = f"{self._client.friendly_name} {CLIENT_SUFFIX}" return {"friendly_name": name} @property diff --git a/homeassistant/components/solaredge/.translations/ca.json b/homeassistant/components/solaredge/.translations/ca.json new file mode 100644 index 00000000000..fd3707af3dd --- /dev/null +++ b/homeassistant/components/solaredge/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Aquest site_id ja est\u00e0 configurat" + }, + "error": { + "site_exists": "Aquest site_id ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API d\u2019aquest lloc", + "name": "Nom d\u2019aquesta instal\u00b7laci\u00f3", + "site_id": "SolarEdge site_id" + }, + "title": "Configuraci\u00f3 dels par\u00e0metres de l'API per aquesta instal\u00b7laci\u00f3" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/da.json b/homeassistant/components/solaredge/.translations/da.json new file mode 100644 index 00000000000..7ed64f51083 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Dette site_id er allerede konfigureret" + }, + "error": { + "site_exists": "Dette site_id er allerede konfigureret" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8glen til dette websted", + "name": "Navnet p\u00e5 denne installation", + "site_id": "SolarEdge site-id" + }, + "title": "Definer API-parametre til denne installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/en.json b/homeassistant/components/solaredge/.translations/en.json new file mode 100644 index 00000000000..7b06c110397 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "This site_id is already configured" + }, + "error": { + "site_exists": "This site_id is already configured" + }, + "step": { + "user": { + "data": { + "api_key": "The API key for this site", + "name": "The name of this installation", + "site_id": "The SolarEdge site-id" + }, + "title": "Define the API parameters for this installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/it.json b/homeassistant/components/solaredge/.translations/it.json new file mode 100644 index 00000000000..6523f393628 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato" + }, + "error": { + "site_exists": "Questo site_id \u00e8 gi\u00e0 configurato" + }, + "step": { + "user": { + "data": { + "api_key": "La chiave API per questo sito", + "name": "Il nome di questa installazione", + "site_id": "Il sito-id di SolarEdge" + }, + "title": "Definire i parametri API per questa installazione" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/ko.json b/homeassistant/components/solaredge/.translations/ko.json new file mode 100644 index 00000000000..3d4b3448252 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "site_exists": "\uc774 site_id \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "\uc774 \uc0ac\uc774\ud2b8\uc758 API \ud0a4", + "name": "\uc774 \uc124\uce58\uc758 \uc774\ub984", + "site_id": "SolarEdge site-id" + }, + "title": "\uc774 \uc124\uce58\uc5d0 \ub300\ud55c API \ub9e4\uac1c\ubcc0\uc218\ub97c \uc815\uc758\ud574\uc8fc\uc138\uc694" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/nl.json b/homeassistant/components/solaredge/.translations/nl.json new file mode 100644 index 00000000000..3cc52b43a63 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Deze site_id is al geconfigureerd" + }, + "error": { + "site_exists": "Deze site_id is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "api_key": "De API-sleutel voor deze site", + "name": "De naam van deze installatie", + "site_id": "De SolarEdge site-id" + }, + "title": "Definieer de API-parameters voor deze installatie" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/pl.json b/homeassistant/components/solaredge/.translations/pl.json new file mode 100644 index 00000000000..376a81219b0 --- /dev/null +++ b/homeassistant/components/solaredge/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + }, + "error": { + "site_exists": "Ten site_id jest ju\u017c skonfigurowany" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API dla tej strony", + "name": "Nazwa tej instalacji", + "site_id": "SolarEdge site-id" + }, + "title": "Zdefiniuj parametry API dla tej instalacji" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json new file mode 100644 index 00000000000..fe36e4296fe --- /dev/null +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + }, + "error": { + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0430\u0439\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "site_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0430\u0439\u0442\u0430 SolarEdge" + }, + "title": "SolarEdge" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index b675126c5fd..8909b970aaf 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -1 +1,43 @@ """The solaredge component.""" +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DEFAULT_NAME, DOMAIN, CONF_SITE_ID + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SITE_ID): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Platform setup, do nothing.""" + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config[DOMAIN]) + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Load the saved entities.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + return True diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py new file mode 100644 index 00000000000..67f05d83aa0 --- /dev/null +++ b/homeassistant/components/solaredge/config_flow.py @@ -0,0 +1,98 @@ +"""Config flow for the SolarEdge platform.""" +import solaredge +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectTimeout + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DOMAIN, DEFAULT_NAME, CONF_SITE_ID + + +@callback +def solaredge_entries(hass: HomeAssistant): + """Return the site_ids for the domain.""" + return set( + (entry.data[CONF_SITE_ID]) + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class SolarEdgeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Initialize the config flow.""" + self._errors = {} + + def _site_in_configuration_exists(self, site_id) -> bool: + """Return True if site_id exists in configuration.""" + if site_id in solaredge_entries(self.hass): + return True + return False + + def _check_site(self, site_id, api_key) -> bool: + """Check if we can connect to the soleredge api service.""" + api = solaredge.Solaredge(api_key) + try: + response = api.get_details(site_id) + except (ConnectTimeout, HTTPError): + self._errors[CONF_SITE_ID] = "could_not_connect" + return False + try: + if response["details"]["status"].lower() != "active": + self._errors[CONF_SITE_ID] = "site_not_active" + return False + except KeyError: + self._errors[CONF_SITE_ID] = "api_failure" + return False + return True + + async def async_step_user(self, user_input=None): + """Step when user intializes a integration.""" + self._errors = {} + if user_input is not None: + name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) + if self._site_in_configuration_exists(user_input[CONF_SITE_ID]): + self._errors[CONF_SITE_ID] = "site_exists" + else: + site = user_input[CONF_SITE_ID] + api = user_input[CONF_API_KEY] + can_connect = await self.hass.async_add_executor_job( + self._check_site, site, api + ) + if can_connect: + return self.async_create_entry( + title=name, data={CONF_SITE_ID: site, CONF_API_KEY: api} + ) + + else: + user_input = {} + user_input[CONF_NAME] = DEFAULT_NAME + user_input[CONF_SITE_ID] = "" + user_input[CONF_API_KEY] = "" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required(CONF_SITE_ID, default=user_input[CONF_SITE_ID]): str, + vol.Required(CONF_API_KEY, default=user_input[CONF_API_KEY]): str, + } + ), + errors=self._errors, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + if self._site_in_configuration_exists(user_input[CONF_SITE_ID]): + return self.async_abort(reason="site_exists") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/solaredge/const.py b/homeassistant/components/solaredge/const.py new file mode 100644 index 00000000000..0d3d1a0cb5f --- /dev/null +++ b/homeassistant/components/solaredge/const.py @@ -0,0 +1,68 @@ +"""Constants for the SolarEdge Monitoring API.""" +from datetime import timedelta + +from homeassistant.const import POWER_WATT, ENERGY_WATT_HOUR + +DOMAIN = "solaredge" + +# Config for solaredge monitoring api requests. +CONF_SITE_ID = "site_id" + +DEFAULT_NAME = "SolarEdge" + +OVERVIEW_UPDATE_DELAY = timedelta(minutes=10) +DETAILS_UPDATE_DELAY = timedelta(hours=12) +INVENTORY_UPDATE_DELAY = timedelta(hours=12) +POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10) + +SCAN_INTERVAL = timedelta(minutes=10) + +# Supported overview sensor types: +# Key: ['json_key', 'name', unit, icon, default] +SENSOR_TYPES = { + "lifetime_energy": [ + "lifeTimeData", + "Lifetime energy", + ENERGY_WATT_HOUR, + "mdi:solar-power", + False, + ], + "energy_this_year": [ + "lastYearData", + "Energy this year", + ENERGY_WATT_HOUR, + "mdi:solar-power", + False, + ], + "energy_this_month": [ + "lastMonthData", + "Energy this month", + ENERGY_WATT_HOUR, + "mdi:solar-power", + False, + ], + "energy_today": [ + "lastDayData", + "Energy today", + ENERGY_WATT_HOUR, + "mdi:solar-power", + False, + ], + "current_power": [ + "currentPower", + "Current Power", + POWER_WATT, + "mdi:solar-power", + True, + ], + "site_details": [None, "Site details", None, None, False], + "meters": ["meters", "Meters", None, None, False], + "sensors": ["sensors", "Sensors", None, None, False], + "gateways": ["gateways", "Gateways", None, None, False], + "batteries": ["batteries", "Batteries", None, None, False], + "inverters": ["inverters", "Inverters", None, None, False], + "power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash", False], + "solar_power": ["PV", "Solar Power", None, "mdi:solar-power", False], + "grid_power": ["GRID", "Grid Power", None, "mdi:power-plug", False], + "storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery", False], +} diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index b2707a0a937..7452790cd60 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -6,6 +6,7 @@ "solaredge==0.0.2", "stringcase==1.2.0" ], + "config_flow": true, "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index cad81c3c338..896596a2a34 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -1,102 +1,39 @@ """Support for SolarEdge Monitoring API.""" - -from datetime import timedelta import logging - -import voluptuous as vol +import solaredge from requests.exceptions import HTTPError, ConnectTimeout -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_API_KEY, - CONF_MONITORED_CONDITIONS, - CONF_NAME, - POWER_WATT, - ENERGY_WATT_HOUR, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_API_KEY from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -# Config for solaredge monitoring api requests. -CONF_SITE_ID = "site_id" - -OVERVIEW_UPDATE_DELAY = timedelta(minutes=10) -DETAILS_UPDATE_DELAY = timedelta(hours=12) -INVENTORY_UPDATE_DELAY = timedelta(hours=12) -POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10) - -SCAN_INTERVAL = timedelta(minutes=10) - -# Supported overview sensor types: -# Key: ['json_key', 'name', unit, icon] -SENSOR_TYPES = { - "lifetime_energy": [ - "lifeTimeData", - "Lifetime energy", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], - "energy_this_year": [ - "lastYearData", - "Energy this year", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], - "energy_this_month": [ - "lastMonthData", - "Energy this month", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], - "energy_today": [ - "lastDayData", - "Energy today", - ENERGY_WATT_HOUR, - "mdi:solar-power", - ], - "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], - "site_details": [None, "Site details", None, None], - "meters": ["meters", "Meters", None, None], - "sensors": ["sensors", "Sensors", None, None], - "gateways": ["gateways", "Gateways", None, None], - "batteries": ["batteries", "Batteries", None, None], - "inverters": ["inverters", "Inverters", None, None], - "power_consumption": ["LOAD", "Power Consumption", None, "mdi:flash"], - "solar_power": ["PV", "Solar Power", None, "mdi:solar-power"], - "grid_power": ["GRID", "Grid Power", None, "mdi:power-plug"], - "storage_power": ["STORAGE", "Storage Power", None, "mdi:car-battery"], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_SITE_ID): cv.string, - vol.Optional(CONF_NAME, default="SolarEdge"): cv.string, - vol.Optional(CONF_MONITORED_CONDITIONS, default=["current_power"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } +from .const import ( + CONF_SITE_ID, + OVERVIEW_UPDATE_DELAY, + DETAILS_UPDATE_DELAY, + INVENTORY_UPDATE_DELAY, + POWER_FLOW_UPDATE_DELAY, + SENSOR_TYPES, ) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Create the SolarEdge Monitoring API sensor.""" - import solaredge +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old configuration.""" + pass - api_key = config[CONF_API_KEY] - site_id = config[CONF_SITE_ID] - platform_name = config[CONF_NAME] - # Create new SolarEdge object to retrieve data - api = solaredge.Solaredge(api_key) +async def async_setup_entry(hass, entry, async_add_entities): + """Add an solarEdge entry.""" + # Add the needed sensors to hass + api = solaredge.Solaredge(entry.data[CONF_API_KEY]) # Check if api can be reached and site is active try: - response = api.get_details(site_id) - + response = await hass.async_add_executor_job( + api.get_details, entry.data[CONF_SITE_ID] + ) if response["details"]["status"].lower() != "active": _LOGGER.error("SolarEdge site is not active") return @@ -108,17 +45,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Could not retrieve details from SolarEdge API") return - # Create sensor factory that will create sensors based on sensor_key. - sensor_factory = SolarEdgeSensorFactory(platform_name, site_id, api) - - # Create a new sensor for each sensor type. + sensor_factory = SolarEdgeSensorFactory(entry.title, entry.data[CONF_SITE_ID], api) entities = [] - for sensor_key in config[CONF_MONITORED_CONDITIONS]: + for sensor_key in SENSOR_TYPES: sensor = sensor_factory.create_sensor(sensor_key) if sensor is not None: entities.append(sensor) - - add_entities(entities, True) + async_add_entities(entities) class SolarEdgeSensorFactory: diff --git a/homeassistant/components/solaredge/strings.json b/homeassistant/components/solaredge/strings.json new file mode 100644 index 00000000000..3265e3bb1b0 --- /dev/null +++ b/homeassistant/components/solaredge/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "SolarEdge", + "step": { + "user": { + "title": "Define the API parameters for this installation", + "data": { + "name": "The name of this installation", + "site_id": "The SolarEdge site-id", + "api_key": "The API key for this site" + } + } + }, + "error": { + "site_exists": "This site_id is already configured" + }, + "abort": { + "site_exists": "This site_id is already configured" + } + } +} diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 4bf015a7489..8586d950e39 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -67,7 +67,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): platform_name = config[CONF_NAME] # Create new SolarEdge object to retrieve data - api = SolarEdge("http://{}/".format(ip_address)) + api = SolarEdge(f"http://{ip_address}/") # Check if api can be reached and site is active try: @@ -149,25 +149,19 @@ class SolarEdgeData: try: response = self.api.get_status() _LOGGER.debug("response from SolarEdge: %s", response) + except (ConnectTimeout): + _LOGGER.error("Connection timeout, skipping update") + return + except (HTTPError): + _LOGGER.error("Could not retrieve data, skipping update") + return + try: self.data["energyTotal"] = response.energy.total self.data["energyThisYear"] = response.energy.thisYear self.data["energyThisMonth"] = response.energy.thisMonth self.data["energyToday"] = response.energy.today self.data["currentPower"] = response.powerWatt - _LOGGER.debug("Updated SolarEdge overview data: %s", self.data) except AttributeError: - _LOGGER.error("Missing details data in solaredge response") - _LOGGER.debug("Response is: %s", response) - return - except (ConnectTimeout, HTTPError): - _LOGGER.error("Could not retrieve data, skipping update") - return - - self.data["energyTotal"] = response.energy.total - self.data["energyThisYear"] = response.energy.thisYear - self.data["energyThisMonth"] = response.energy.thisMonth - self.data["energyToday"] = response.energy.today - self.data["currentPower"] = response.powerWatt - _LOGGER.debug("Updated SolarEdge overview data: %s", self.data) + _LOGGER.error("Missing details data in SolarEdge response") diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 14598607ada..52e50ab4799 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -8,4 +8,3 @@ "dependencies": [], "codeowners": ["@squishykid"] } - \ No newline at end of file diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index 62c6a2a3a51..0c1cfcf21da 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -35,7 +35,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= idx, unit = solax.INVERTER_SENSORS[sensor] if unit == "C": unit = TEMP_CELSIUS - uid = "{}-{}".format(serial, idx) + uid = f"{serial}-{idx}" devices.append(Inverter(uid, serial, sensor, unit)) endpoint.sensors = devices async_add_entities(devices) @@ -97,7 +97,7 @@ class Inverter(Entity): @property def name(self): """Name of this inverter attribute.""" - return "Solax {} {}".format(self.serial, self.key) + return f"Solax {self.serial} {self.key}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/somfy/.translations/it.json b/homeassistant/components/somfy/.translations/it.json new file mode 100644 index 00000000000..06fc8bed40f --- /dev/null +++ b/homeassistant/components/somfy/.translations/it.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 possibile configurare un solo account Somfy.", + "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", + "missing_configuration": "Il componente Somfy non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticato con successo con Somfy." + }, + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index 7e6645c31e2..9f3c58c8ffb 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -73,7 +73,7 @@ class SomfyFlowHandler(config_entries.ConfigFlow): client_id = self.hass.data[DOMAIN][CLIENT_ID] client_secret = self.hass.data[DOMAIN][CLIENT_SECRET] - redirect_uri = "{}{}".format(self.hass.config.api.base_url, AUTH_CALLBACK_PATH) + redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" api = SomfyApi(client_id, client_secret, redirect_uri) self.hass.http.register_view(SomfyAuthCallbackView()) @@ -95,7 +95,7 @@ class SomfyFlowHandler(config_entries.ConfigFlow): code = self.code from pymfy.api.somfy_api import SomfyApi - redirect_uri = "{}{}".format(self.hass.config.api.base_url, AUTH_CALLBACK_PATH) + redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" api = SomfyApi(client_id, client_secret, redirect_uri) token = await self.hass.async_add_executor_job(api.request_token, None, code) _LOGGER.info("Successfully authenticated Somfy") diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 70461ad15d2..41472413a07 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -606,7 +606,7 @@ class SonosEntity(MediaPlayerDevice): # media_artist = "Station - Artist - Title" # detect this case and trim from the front of # media_artist for cosmetics - trim = "{title} - ".format(title=self._media_title) + trim = f"{self._media_title} - " chars = min(len(self._media_artist), len(trim)) if self._media_artist[:chars].upper() == trim[:chars].upper(): diff --git a/homeassistant/components/speedtestdotnet/const.py b/homeassistant/components/speedtestdotnet/const.py index 90a89fcda2c..69aadb7ac6c 100644 --- a/homeassistant/components/speedtestdotnet/const.py +++ b/homeassistant/components/speedtestdotnet/const.py @@ -1,7 +1,7 @@ """Consts used by Speedtest.net.""" DOMAIN = "speedtestdotnet" -DATA_UPDATED = "{}_data_updated".format(DOMAIN) +DATA_UPDATED = f"{DOMAIN}_data_updated" SENSOR_TYPES = { "ping": ["Ping", "ms"], diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py index 4498fd47e69..fc3a7592af3 100644 --- a/homeassistant/components/spotcrime/sensor.py +++ b/homeassistant/components/spotcrime/sensor.py @@ -29,7 +29,7 @@ CONF_DAYS = "days" DEFAULT_DAYS = 1 NAME = "spotcrime" -EVENT_INCIDENT = "{}_incident".format(NAME) +EVENT_INCIDENT = f"{NAME}_incident" SCAN_INTERVAL = timedelta(minutes=30) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 74a0dc0c9c0..31fdc09af80 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -99,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Spotify platform.""" import spotipy.oauth2 - callback_url = "{}{}".format(hass.config.api.base_url, AUTH_CALLBACK_PATH) + callback_url = f"{hass.config.api.base_url}{AUTH_CALLBACK_PATH}" cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) oauth = spotipy.oauth2.SpotifyOAuth( config.get(CONF_CLIENT_ID), diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index a489e3fd736..38a320543a9 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,7 +3,7 @@ "name": "Sql", "documentation": "https://www.home-assistant.io/components/sql", "requirements": [ - "sqlalchemy==1.3.7" + "sqlalchemy==1.3.8" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 5f3a91bcf2f..9e62e7ee0db 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -208,7 +208,7 @@ class LogitechMediaServer: if self._username is None else aiohttp.BasicAuth(self._username, self._password) ) - url = "http://{}:{}/jsonrpc.js".format(self.host, self.port) + url = f"http://{self.host}:{self.port}/jsonrpc.js" data = json.dumps( {"id": "1", "method": "slim.request", "params": [player, command]} ) @@ -288,9 +288,7 @@ class SqueezeBoxDevice(MediaPlayerDevice): async def async_update(self): """Retrieve the current state of the player.""" tags = "adKl" - response = await self.async_query( - "status", "-", "1", "tags:{tags}".format(tags=tags) - ) + response = await self.async_query("status", "-", "1", f"tags:{tags}") if response is False: return diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index a9873c76afe..f1d1787b7b4 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -87,7 +87,7 @@ class SrpEnergy(Entity): if self._state is None: return None - return "{0:.2f}".format(self._state) + return f"{self._state:.2f}" @property def name(self): diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index d4104dd3dcf..1b567c58b45 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -18,8 +18,8 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Start.ca" CONF_TOTAL_BANDWIDTH = "total_bandwidth" -GIGABYTES = "GB" # type: str -PERCENT = "%" # type: str +GIGABYTES = "GB" +PERCENT = "%" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) REQUEST_TIMEOUT = 5 # seconds @@ -86,7 +86,7 @@ class StartcaSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 924afedfbd5..6c9c5ac6079 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -80,7 +80,7 @@ class SteamSensor(Entity): @property def entity_id(self): """Return the entity ID.""" - return "sensor.steam_{}".format(self._account) + return f"sensor.steam_{self._account}" @property def state(self): diff --git a/homeassistant/components/stiebel_eltron/climate.py b/homeassistant/components/stiebel_eltron/climate.py index f2e6209d21d..ce16b10f548 100644 --- a/homeassistant/components/stiebel_eltron/climate.py +++ b/homeassistant/components/stiebel_eltron/climate.py @@ -139,7 +139,7 @@ class StiebelEltron(ClimateDevice): @property def current_humidity(self): """Return the current humidity.""" - return float("{0:.1f}".format(self._current_humidity)) + return float(f"{self._current_humidity:.1f}") @property def hvac_modes(self): diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 50cc1d8169d..2ae8dd5f714 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -217,9 +217,7 @@ async def async_handle_record_service(hass, call): # Check for file access if not hass.config.is_allowed_path(video_path): - raise HomeAssistantError( - "Can't write {}, no access to path!".format(video_path) - ) + raise HomeAssistantError(f"Can't write {video_path}, no access to path!") # Check for active stream streams = hass.data[DOMAIN][ATTR_STREAMS] @@ -231,9 +229,7 @@ async def async_handle_record_service(hass, call): # Add recorder recorder = stream.outputs.get("recorder") if recorder: - raise HomeAssistantError( - "Stream already recording to {}!".format(recorder.video_path) - ) + raise HomeAssistantError(f"Stream already recording to {recorder.video_path}!") recorder = stream.add_provider("recorder") recorder.video_path = video_path diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index ab877915158..c9e62f53a57 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -64,10 +64,7 @@ class M3U8Renderer: @staticmethod def render_preamble(track): """Render preamble.""" - return [ - "#EXT-X-VERSION:3", - "#EXT-X-TARGETDURATION:{}".format(track.target_duration), - ] + return ["#EXT-X-VERSION:3", f"#EXT-X-TARGETDURATION:{track.target_duration}"] @staticmethod def render_playlist(track, start_time): @@ -84,7 +81,7 @@ class M3U8Renderer: playlist.extend( [ "#EXTINF:{:.04f},".format(float(segment.duration)), - "./segment/{}.ts".format(segment.sequence), + f"./segment/{segment.sequence}.ts", ] ) diff --git a/homeassistant/components/streamlabswater/binary_sensor.py b/homeassistant/components/streamlabswater/binary_sensor.py index fd0ccb57aa6..78b2ceb4044 100644 --- a/homeassistant/components/streamlabswater/binary_sensor.py +++ b/homeassistant/components/streamlabswater/binary_sensor.py @@ -58,7 +58,7 @@ class StreamlabsAwayMode(BinarySensorDevice): @property def name(self): """Return the name for away mode.""" - return "{} {}".format(self._location_name, NAME_AWAY_MODE) + return f"{self._location_name} {NAME_AWAY_MODE}" @property def is_on(self): diff --git a/homeassistant/components/streamlabswater/sensor.py b/homeassistant/components/streamlabswater/sensor.py index 69196c288f6..e7168f8ec0b 100644 --- a/homeassistant/components/streamlabswater/sensor.py +++ b/homeassistant/components/streamlabswater/sensor.py @@ -79,7 +79,7 @@ class StreamLabsDailyUsage(Entity): @property def name(self): """Return the name for daily usage.""" - return "{} {}".format(self._location_name, NAME_DAILY_USAGE) + return f"{self._location_name} {NAME_DAILY_USAGE}" @property def icon(self): @@ -107,7 +107,7 @@ class StreamLabsMonthlyUsage(StreamLabsDailyUsage): @property def name(self): """Return the name for monthly usage.""" - return "{} {}".format(self._location_name, NAME_MONTHLY_USAGE) + return f"{self._location_name} {NAME_MONTHLY_USAGE}" @property def state(self): @@ -121,7 +121,7 @@ class StreamLabsYearlyUsage(StreamLabsDailyUsage): @property def name(self): """Return the name for yearly usage.""" - return "{} {}".format(self._location_name, NAME_YEARLY_USAGE) + return f"{self._location_name} {NAME_YEARLY_USAGE}" @property def state(self): diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 50b8e29f88e..86e763142e6 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -17,7 +17,10 @@ DOMAIN = "supla" CONF_SERVER = "server" CONF_SERVERS = "servers" -SUPLA_FUNCTION_HA_CMP_MAP = {"CONTROLLINGTHEROLLERSHUTTER": "cover"} +SUPLA_FUNCTION_HA_CMP_MAP = { + "CONTROLLINGTHEROLLERSHUTTER": "cover", + "LIGHTSWITCH": "switch", +} SUPLA_CHANNELS = "supla_channels" SUPLA_SERVERS = "supla_servers" @@ -62,7 +65,7 @@ def setup(hass, base_config): srv_info, ) return False - except IOError: + except OSError: _LOGGER.exception( "Server: %s not configured. Error on Supla API access: ", server_address ) diff --git a/homeassistant/components/supla/cover.py b/homeassistant/components/supla/cover.py index 0b842bd181c..3182aa8c136 100644 --- a/homeassistant/components/supla/cover.py +++ b/homeassistant/components/supla/cover.py @@ -5,8 +5,6 @@ from pprint import pformat from homeassistant.components.cover import ATTR_POSITION, CoverDevice from homeassistant.components.supla import SuplaChannel -DEPENDENCIES = ["supla"] - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/supla/switch.py b/homeassistant/components/supla/switch.py new file mode 100644 index 00000000000..5e7a5469950 --- /dev/null +++ b/homeassistant/components/supla/switch.py @@ -0,0 +1,38 @@ +"""Support for Supla cover - curtains, rollershutters etc.""" +import logging +from pprint import pformat + +from homeassistant.components.switch import SwitchDevice +from homeassistant.components.supla import SuplaChannel + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Supla switches.""" + if discovery_info is None: + return + + _LOGGER.debug("Discovery: %s", pformat(discovery_info)) + + add_entities([SuplaSwitch(device) for device in discovery_info]) + + +class SuplaSwitch(SuplaChannel, SwitchDevice): + """Representation of a Supla Switch.""" + + def turn_on(self, **kwargs): + """Turn on the switch.""" + self.action("TURN_ON") + + def turn_off(self, **kwargs): + """Turn off the switch.""" + self.action("TURN_OFF") + + @property + def is_on(self): + """Return true if switch is on.""" + state = self.channel_data.get("state") + if state: + return state["on"] + return False diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index bbac046d62e..2d5d0e8de3f 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -101,7 +101,7 @@ class SwissHydrologicalDataSensor(Entity): @property def unique_id(self) -> str: """Return a unique, friendly identifier for this entity.""" - return "{0}_{1}".format(self._station, self._condition) + return f"{self._station}_{self._condition}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index fedcb3003b0..3cf8babf554 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -110,7 +110,7 @@ class SwissPublicTransportSensor(Entity): ATTR_DEPARTURE_TIME2: self._opendata.connections[2]["departure"], ATTR_START: self._opendata.from_name, ATTR_TARGET: self._opendata.to_name, - ATTR_REMAINING_TIME: "{}".format(self._remaining_time), + ATTR_REMAINING_TIME: f"{self._remaining_time}", ATTR_ATTRIBUTION: ATTRIBUTION, } return attr diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 3775854fade..98965af1513 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -74,7 +74,7 @@ class SwisscomDeviceScanner(DeviceScanner): def get_swisscom_data(self): """Retrieve data from Swisscom and return parsed result.""" - url = "http://{}/ws".format(self.host) + url = f"http://{self.host}/ws" headers = {CONTENT_TYPE: "application/x-sah-ws-4-call+json"} data = """ {"service":"Devices", "method":"get", diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json new file mode 100644 index 00000000000..c97565ddfe6 --- /dev/null +++ b/homeassistant/components/switch/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Commuta {entity_name}", + "turn_off": "Desactiva {entity_name}", + "turn_on": "Activa {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} desactivat", + "turn_on": "{entity_name} activat" + }, + "trigger_type": { + "turn_off": "{entity_name} desactivat", + "turn_on": "{entity_name} activat" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json new file mode 100644 index 00000000000..5be333cbf13 --- /dev/null +++ b/homeassistant/components/switch/.translations/en.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_off": "Turn off {entity_name}", + "turn_on": "Turn on {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} turned off", + "turn_on": "{entity_name} turned on" + }, + "trigger_type": { + "turn_off": "{entity_name} turned off", + "turn_on": "{entity_name} turned on" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json new file mode 100644 index 00000000000..c51ce8c6ee5 --- /dev/null +++ b/homeassistant/components/switch/.translations/it.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Attivare / Disattivare {entity_name}", + "turn_off": "Disattivare {entity_name}", + "turn_on": "Attivare {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} disattivato", + "turn_on": "{entity_name} attivato" + }, + "trigger_type": { + "turn_off": "{entity_name} disattivato", + "turn_on": "{entity_name} attivato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json new file mode 100644 index 00000000000..2156ea04e01 --- /dev/null +++ b/homeassistant/components/switch/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} \ud1a0\uae00", + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" + }, + "condition_type": { + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + }, + "trigger_type": { + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/nl.json b/homeassistant/components/switch/.translations/nl.json new file mode 100644 index 00000000000..1d8355d2158 --- /dev/null +++ b/homeassistant/components/switch/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Omschakelen {entity_name}", + "turn_off": "Zet {entity_name} uit.", + "turn_on": "Zet {entity_name} aan." + }, + "condition_type": { + "turn_off": "{entity_name} uitgeschakeld", + "turn_on": "{entity_name} ingeschakeld" + }, + "trigger_type": { + "turn_off": "{entity_name} uitgeschakeld", + "turn_on": "{entity_name} ingeschakeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json new file mode 100644 index 00000000000..f564d1424ea --- /dev/null +++ b/homeassistant/components/switch/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Prze\u0142\u0105cz {entity_name}", + "turn_off": "Wy\u0142\u0105cz {entity_name}", + "turn_on": "W\u0142\u0105cz {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} wy\u0142\u0105czone", + "turn_on": "{entity_name} w\u0142\u0105czone" + }, + "trigger_type": { + "turn_off": "{entity_name} wy\u0142\u0105czone", + "turn_on": "{entity_name} w\u0142\u0105czone" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json new file mode 100644 index 00000000000..1b0658cd174 --- /dev/null +++ b/homeassistant/components/switch/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" + }, + "condition_type": { + "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + }, + "trigger_type": { + "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 348c2a8616b..aa7459d1d3c 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -10,7 +10,6 @@ from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, - ENTITY_SERVICE_SCHEMA, ) from homeassistant.const import ( STATE_ON, @@ -68,17 +67,9 @@ async def async_setup(hass, config): ) await component.async_setup(config) - component.async_register_entity_service( - SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, "async_turn_off" - ) - - component.async_register_entity_service( - SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, "async_turn_on" - ) - - component.async_register_entity_service( - SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, "async_toggle" - ) + component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") + component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") return True diff --git a/homeassistant/components/switch/device_automation.py b/homeassistant/components/switch/device_automation.py new file mode 100644 index 00000000000..61292d47449 --- /dev/null +++ b/homeassistant/components/switch/device_automation.py @@ -0,0 +1,56 @@ +"""Provides device automations for lights.""" +import voluptuous as vol + +from homeassistant.components.device_automation import toggle_entity +from homeassistant.const import CONF_DOMAIN +from . import DOMAIN + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) + +CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + +TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( + {vol.Required(CONF_DOMAIN): DOMAIN} +) + + +async def async_call_action_from_config(hass, config, variables, context): + """Change state based on configuration.""" + config = ACTION_SCHEMA(config) + await toggle_entity.async_call_action_from_config( + hass, config, variables, context, DOMAIN + ) + + +def async_condition_from_config(config, config_validation): + """Evaluate state based on configuration.""" + config = CONDITION_SCHEMA(config) + return toggle_entity.async_condition_from_config(config, config_validation) + + +async def async_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + config = TRIGGER_SCHEMA(config) + return await toggle_entity.async_attach_trigger( + hass, config, action, automation_info + ) + + +async def async_get_actions(hass, device_id): + """List device actions.""" + return await toggle_entity.async_get_actions(hass, device_id, DOMAIN) + + +async def async_get_conditions(hass, device_id): + """List device conditions.""" + return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 0b1094c0dd9..2027a8fc458 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -48,10 +48,10 @@ class LightSwitch(Light): def __init__(self, name: str, switch_entity_id: str) -> None: """Initialize Light Switch.""" - self._name = name # type: str - self._switch_entity_id = switch_entity_id # type: str - self._is_on = False # type: bool - self._available = False # type: bool + self._name = name + self._switch_entity_id = switch_entity_id + self._is_on = False + self._available = False self._async_unsub_state_changed = None @property diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json new file mode 100644 index 00000000000..77b842ba078 --- /dev/null +++ b/homeassistant/components/switch/strings.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Toggle {entity_name}", + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}" + }, + "condition_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off" + }, + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off" + } + } +} diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index f52935c02ec..454baca4eef 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -65,7 +65,7 @@ class SwitcherControl(SwitchDevice): @property def unique_id(self) -> str: """Return a unique ID.""" - return "{}-{}".format(self._device_data.device_id, self._device_data.mac_addr) + return f"{self._device_data.device_id}-{self._device_data.mac_addr}" @property def is_on(self) -> bool: @@ -143,7 +143,7 @@ class SwitcherControl(SwitchDevice): STATE_ON as SWITCHER_STATE_ON, ) - response = None # type: SwitcherV2ControlResponseMSG + response: "SwitcherV2ControlResponseMSG" = None async with SwitcherV2Api( self.hass.loop, self._device_data.ip_addr, diff --git a/homeassistant/components/syncthru/sensor.py b/homeassistant/components/syncthru/sensor.py index 0a50eec75c2..1258732223b 100644 --- a/homeassistant/components/syncthru/sensor.py +++ b/homeassistant/components/syncthru/sensor.py @@ -18,12 +18,10 @@ TONER_COLORS = COLORS TRAYS = range(1, 6) OUTPUT_TRAYS = range(0, 6) DEFAULT_MONITORED_CONDITIONS = [] -DEFAULT_MONITORED_CONDITIONS.extend(["toner_{}".format(key) for key in TONER_COLORS]) -DEFAULT_MONITORED_CONDITIONS.extend(["drum_{}".format(key) for key in DRUM_COLORS]) -DEFAULT_MONITORED_CONDITIONS.extend(["tray_{}".format(key) for key in TRAYS]) -DEFAULT_MONITORED_CONDITIONS.extend( - ["output_tray_{}".format(key) for key in OUTPUT_TRAYS] -) +DEFAULT_MONITORED_CONDITIONS.extend([f"toner_{key}" for key in TONER_COLORS]) +DEFAULT_MONITORED_CONDITIONS.extend([f"drum_{key}" for key in DRUM_COLORS]) +DEFAULT_MONITORED_CONDITIONS.extend([f"tray_{key}" for key in TRAYS]) +DEFAULT_MONITORED_CONDITIONS.extend([f"output_tray_{key}" for key in OUTPUT_TRAYS]) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -81,16 +79,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= devices = [SyncThruMainSensor(printer, name)] for key in supp_toner: - if "toner_{}".format(key) in monitored: + if f"toner_{key}" in monitored: devices.append(SyncThruTonerSensor(printer, name, key)) for key in supp_drum: - if "drum_{}".format(key) in monitored: + if f"drum_{key}" in monitored: devices.append(SyncThruDrumSensor(printer, name, key)) for key in supp_tray: - if "tray_{}".format(key) in monitored: + if f"tray_{key}" in monitored: devices.append(SyncThruInputTraySensor(printer, name, key)) for key in supp_output_tray: - if "output_tray_{}".format(key) in monitored: + if f"output_tray_{key}" in monitored: devices.append(SyncThruOutputTraySensor(printer, name, key)) async_add_entities(devices, True) @@ -173,10 +171,10 @@ class SyncThruTonerSensor(SyncThruSensor): def __init__(self, syncthru, name, color): """Initialize the sensor.""" super().__init__(syncthru, name) - self._name = "{} Toner {}".format(name, color) + self._name = f"{name} Toner {color}" self._color = color self._unit_of_measurement = "%" - self._id_suffix = "_toner_{}".format(color) + self._id_suffix = f"_toner_{color}" def update(self): """Get the latest data from SyncThru and update the state.""" @@ -193,10 +191,10 @@ class SyncThruDrumSensor(SyncThruSensor): def __init__(self, syncthru, name, color): """Initialize the sensor.""" super().__init__(syncthru, name) - self._name = "{} Drum {}".format(name, color) + self._name = f"{name} Drum {color}" self._color = color self._unit_of_measurement = "%" - self._id_suffix = "_drum_{}".format(color) + self._id_suffix = f"_drum_{color}" def update(self): """Get the latest data from SyncThru and update the state.""" @@ -213,9 +211,9 @@ class SyncThruInputTraySensor(SyncThruSensor): def __init__(self, syncthru, name, number): """Initialize the sensor.""" super().__init__(syncthru, name) - self._name = "{} Tray {}".format(name, number) + self._name = f"{name} Tray {number}" self._number = number - self._id_suffix = "_tray_{}".format(number) + self._id_suffix = f"_tray_{number}" def update(self): """Get the latest data from SyncThru and update the state.""" @@ -234,9 +232,9 @@ class SyncThruOutputTraySensor(SyncThruSensor): def __init__(self, syncthru, name, number): """Initialize the sensor.""" super().__init__(syncthru, name) - self._name = "{} Output Tray {}".format(name, number) + self._name = f"{name} Output Tray {number}" self._number = number - self._id_suffix = "_output_tray_{}".format(number) + self._id_suffix = f"_output_tray_{number}" def update(self): """Get the latest data from SyncThru and update the state.""" diff --git a/homeassistant/components/synologydsm/sensor.py b/homeassistant/components/synologydsm/sensor.py index 17295f15250..e19f6ada809 100644 --- a/homeassistant/components/synologydsm/sensor.py +++ b/homeassistant/components/synologydsm/sensor.py @@ -184,7 +184,7 @@ class SynoNasSensor(Entity): def name(self): """Return the name of the sensor, if any.""" if self.monitor_device is not None: - return "{} ({})".format(self.var_name, self.monitor_device) + return f"{self.var_name} ({self.monitor_device})" return self.var_name @property diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index c9bd486053e..68561d45f8f 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -198,7 +198,7 @@ async def async_setup(hass, config): return if service.service == "write": logger = logging.getLogger( - service.data.get(CONF_LOGGER, "{}.external".format(__name__)) + service.data.get(CONF_LOGGER, f"{__name__}.external") ) level = service.data[CONF_LEVEL] getattr(logger, level)(service.data[CONF_MESSAGE]) diff --git a/homeassistant/components/sytadin/sensor.py b/homeassistant/components/sytadin/sensor.py index 4296f2d5b05..b7c94933a39 100644 --- a/homeassistant/components/sytadin/sensor.py +++ b/homeassistant/components/sytadin/sensor.py @@ -86,7 +86,7 @@ class SytadinSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self._option) + return f"{self._name} {self._option}" @property def state(self): diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 15e01db4082..1108b32af4e 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -124,7 +124,7 @@ def create_climate_device(tado, hass, zone, name, zone_id): max_temp = float(temperatures["celsius"]["max"]) step = temperatures["celsius"].get("step", PRECISION_TENTHS) - data_id = "zone {} {}".format(name, zone_id) + data_id = f"zone {name} {zone_id}" device = TadoClimate( tado, name, diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 5cfdbd1f30c..7b4bd643f3d 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -80,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def create_zone_sensor(tado, zone, name, zone_id, variable): """Create a zone sensor.""" - data_id = "zone {} {}".format(name, zone_id) + data_id = f"zone {name} {zone_id}" tado.add_sensor( data_id, @@ -92,7 +92,7 @@ def create_zone_sensor(tado, zone, name, zone_id, variable): def create_device_sensor(tado, device, name, device_id, variable): """Create a device sensor.""" - data_id = "device {} {}".format(name, device_id) + data_id = f"device {name} {device_id}" tado.add_sensor( data_id, @@ -118,7 +118,7 @@ class TadoSensor(Entity): self.zone_id = zone_id self.zone_variable = zone_variable - self._unique_id = "{} {}".format(zone_variable, zone_id) + self._unique_id = f"{zone_variable} {zone_id}" self._data_id = data_id self._state = None @@ -132,7 +132,7 @@ class TadoSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.zone_name, self.zone_variable) + return f"{self.zone_name} {self.zone_variable}" @property def state(self): diff --git a/homeassistant/components/tapsaff/binary_sensor.py b/homeassistant/components/tapsaff/binary_sensor.py index 3b4bcfa8ea5..fe6b01ced4e 100644 --- a/homeassistant/components/tapsaff/binary_sensor.py +++ b/homeassistant/components/tapsaff/binary_sensor.py @@ -45,7 +45,7 @@ class TapsAffSensor(BinarySensorDevice): @property def name(self): """Return the name of the sensor.""" - return "{}".format(self._name) + return f"{self._name}" @property def is_on(self): diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index 922d88d44bf..ea0963a092e 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -32,7 +32,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) port = config.get(CONF_PORT) name = config.get(CONF_NAME) - url = "http://{}:{}/api/LiveData.xml".format(host, port) + url = f"http://{host}:{port}/api/LiveData.xml" gateway = Ted5000Gateway(url) diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index 51914d7a4fc..dc8b16b8ce1 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -17,8 +17,8 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "TekSavvy" CONF_TOTAL_BANDWIDTH = "total_bandwidth" -GIGABYTES = "GB" # type: str -PERCENT = "%" # type: str +GIGABYTES = "GB" +PERCENT = "%" MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) REQUEST_TIMEOUT = 5 # seconds @@ -90,7 +90,7 @@ class TekSavvySensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index e73c25203e0..a36f41edf3b 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -554,7 +554,7 @@ class TelegramNotificationService: def send_message(self, message="", target=None, **kwargs): """Send a message to one or multiple pre-allowed chat IDs.""" title = kwargs.get(ATTR_TITLE) - text = "{}\n{}".format(title, message) if title else message + text = f"{title}\n{message}" if title else message params = self._get_msg_kwargs(kwargs) for chat_id in self._get_target_chat_ids(target): _LOGGER.debug("Send message in chat ID %s with params: %s", chat_id, params) @@ -590,7 +590,7 @@ class TelegramNotificationService: if type_edit == SERVICE_EDIT_MESSAGE: message = kwargs.get(ATTR_MESSAGE) title = kwargs.get(ATTR_TITLE) - text = "{}\n{}".format(title, message) if title else message + text = f"{title}\n{message}" if title else message _LOGGER.debug( "Editing message with ID %s.", message_id or inline_message_id ) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index 166f48c4961..c71510eddd9 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -45,7 +45,7 @@ async def async_setup_platform(hass, config): else: _LOGGER.debug("telegram webhook Status: %s", current_status) - handler_url = "{0}{1}".format(base_url, TELEGRAM_HANDLER_URL) + handler_url = f"{base_url}{TELEGRAM_HANDLER_URL}" if not handler_url.startswith("https"): _LOGGER.error("Invalid telegram webhook %s must be https", handler_url) return False diff --git a/homeassistant/components/tellduslive/.translations/it.json b/homeassistant/components/tellduslive/.translations/it.json index 3baa307de51..ce152285e75 100644 --- a/homeassistant/components/tellduslive/.translations/it.json +++ b/homeassistant/components/tellduslive/.translations/it.json @@ -11,12 +11,14 @@ }, "step": { "auth": { + "description": "Per collegare il tuo account TelldusLive:\n 1. Clicca sul link sottostante\n 2. Accedi a Telldus Live\n 3. Autorizzare **{app_name}**** (cliccare **S\u00ec**).\n 4. Torna qui e clicca su **SUBMIT**.\n\n [Collega account TelldusLive]({auth_url})", "title": "Autenticati con TelldusLive" }, "user": { "data": { "host": "Host" }, + "description": "Vuoto", "title": "Scegli l'endpoint." } }, diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 05662cc2b23..7234127a152 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -46,7 +46,7 @@ DATA_CONFIG_ENTRY_LOCK = "tellduslive_config_entry_lock" CONFIG_ENTRY_IS_SETUP = "telldus_config_entry_is_setup" NEW_CLIENT_TASK = "telldus_new_client_task" -INTERVAL_TRACKER = "{}_INTERVAL".format(DOMAIN) +INTERVAL_TRACKER = f"{DOMAIN}_INTERVAL" async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 24c038f870a..98d162d6d81 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -5,7 +5,7 @@ from collections import namedtuple import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME +from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME, CONF_PROTOCOL from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -16,6 +16,7 @@ DatatypeDescription = namedtuple("DatatypeDescription", ["name", "unit"]) CONF_DATATYPE_MASK = "datatype_mask" CONF_ONLY_NAMED = "only_named" CONF_TEMPERATURE_SCALE = "temperature_scale" +CONF_MODEL = "model" DEFAULT_DATATYPE_MASK = 127 DEFAULT_TEMPERATURE_SCALE = TEMP_CELSIUS @@ -35,6 +36,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_PROTOCOL): cv.string, + vol.Optional(CONF_MODEL): cv.string, } ) ], @@ -74,18 +77,36 @@ def setup_platform(hass, config, add_entities, discovery_info=None): datatype_mask = config.get(CONF_DATATYPE_MASK) if config[CONF_ONLY_NAMED]: - named_sensors = { - named_sensor[CONF_ID]: named_sensor[CONF_NAME] - for named_sensor in config[CONF_ONLY_NAMED] - } + named_sensors = {} + for named_sensor in config[CONF_ONLY_NAMED]: + name = named_sensor[CONF_NAME] + proto = named_sensor.get(CONF_PROTOCOL) + model = named_sensor.get(CONF_MODEL) + id_ = named_sensor[CONF_ID] + if proto is not None: + if model is not None: + named_sensors["{}{}{}".format(proto, model, id_)] = name + else: + named_sensors["{}{}".format(proto, id_)] = name + else: + named_sensors[id_] = name for tellcore_sensor in tellcore_lib.sensors(): if not config[CONF_ONLY_NAMED]: sensor_name = str(tellcore_sensor.id) else: - if tellcore_sensor.id not in named_sensors: + proto_id = "{}{}".format(tellcore_sensor.protocol, tellcore_sensor.id) + proto_model_id = "{}{}{}".format( + tellcore_sensor.protocol, tellcore_sensor.model, tellcore_sensor.id + ) + if tellcore_sensor.id in named_sensors: + sensor_name = named_sensors[tellcore_sensor.id] + elif proto_id in named_sensors: + sensor_name = named_sensors[proto_id] + elif proto_model_id in named_sensors: + sensor_name = named_sensors[proto_model_id] + else: continue - sensor_name = named_sensors[tellcore_sensor.id] for datatype in sensor_value_descriptions: if datatype & datatype_mask and tellcore_sensor.has_value(datatype): @@ -107,7 +128,7 @@ class TellstickSensor(Entity): self._unit_of_measurement = sensor_info.unit or None self._value = None - self._name = "{} {}".format(name, sensor_info.name) + self._name = f"{name} {sensor_info.name}" @property def name(self): diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index a4777af5457..87fb70bb888 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -117,7 +117,7 @@ class TelnetSwitch(SwitchDevice): response = telnet.read_until(b"\r", timeout=self._timeout) _LOGGER.debug("telnet response: %s", response.decode("ASCII").strip()) return response.decode("ASCII").strip() - except IOError as error: + except OSError as error: _LOGGER.error( 'Command "%s" failed with exception: %s', command, repr(error) ) diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index c5e5c4af978..a32de3da10f 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -96,7 +96,7 @@ class TemperSensor(Entity): ) sensor_value = self.temper_device.get_temperature(format_str) self.current_value = round(sensor_value, 1) - except IOError: + except OSError: _LOGGER.error( "Failed to get temperature. The device address may" "have changed. Attempting to reset device" diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index a5397e0ea7d..b77528e0c32 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -1,6 +1,7 @@ """Allows the creation of a sensor that breaks out state_attributes.""" import logging from typing import Optional +from itertools import chain import voluptuous as vol @@ -28,6 +29,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.event import async_track_state_change +CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" + _LOGGER = logging.getLogger(__name__) SENSOR_SCHEMA = vol.Schema( @@ -36,6 +39,9 @@ SENSOR_SCHEMA = vol.Schema( vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, vol.Optional(CONF_FRIENDLY_NAME_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES, default={}): vol.Schema( + {cv.string: cv.template} + ), vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -60,17 +66,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= friendly_name_template = device_config.get(CONF_FRIENDLY_NAME_TEMPLATE) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) device_class = device_config.get(CONF_DEVICE_CLASS) + attribute_templates = device_config[CONF_ATTRIBUTE_TEMPLATES] entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) invalid_templates = [] - for tpl_name, template in ( - (CONF_VALUE_TEMPLATE, state_template), - (CONF_ICON_TEMPLATE, icon_template), - (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template), - (CONF_FRIENDLY_NAME_TEMPLATE, friendly_name_template), - ): + templates = { + CONF_VALUE_TEMPLATE: state_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + CONF_FRIENDLY_NAME_TEMPLATE: friendly_name_template, + } + + for tpl_name, template in chain(templates.items(), attribute_templates.items()): if template is None: continue template.hass = hass @@ -82,7 +91,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if template_entity_ids == MATCH_ALL: entity_ids = MATCH_ALL # Cut off _template from name - invalid_templates.append(tpl_name[:-9]) + invalid_templates.append(tpl_name.replace("_template", "")) elif entity_ids != MATCH_ALL: entity_ids |= set(template_entity_ids) @@ -113,6 +122,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_picture_template, entity_ids, device_class, + attribute_templates, ) ) if not sensors: @@ -138,6 +148,7 @@ class SensorTemplate(Entity): entity_picture_template, entity_ids, device_class, + attribute_templates, ): """Initialize the sensor.""" self.hass = hass @@ -155,6 +166,8 @@ class SensorTemplate(Entity): self._entity_picture = None self._entities = entity_ids self._device_class = device_class + self._attribute_templates = attribute_templates + self._attributes = {} async def async_added_to_hass(self): """Register callbacks.""" @@ -209,6 +222,11 @@ class SensorTemplate(Entity): """Return the unit_of_measurement of the device.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + @property def should_poll(self): """No polling needed.""" @@ -229,11 +247,23 @@ class SensorTemplate(Entity): else: self._state = None _LOGGER.error("Could not render template %s: %s", self._name, ex) - for property_name, template in ( - ("_icon", self._icon_template), - ("_entity_picture", self._entity_picture_template), - ("_name", self._friendly_name_template), - ): + + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + "_name": self._friendly_name_template, + } + + attrs = {} + for key, value in self._attribute_templates.items(): + try: + attrs[key] = value.async_render() + except TemplateError as err: + _LOGGER.error("Error rendering attribute %s: %s", key, err) + + self._attributes = attrs + + for property_name, template in templates.items(): if template is None: continue diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index f5bd981bad1..9419cbaaefb 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/components/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.17.0", + "numpy==1.17.1", "pillow==6.1.0", "protobuf==3.6.1" ], diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 98cf5e47fd9..c737b2f0bba 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -43,13 +43,13 @@ class TeslaSensor(TeslaDevice, Entity): super().__init__(tesla_device, controller) if self.type: - self._name = "{} ({})".format(self.tesla_device.name, self.type) + self._name = f"{self.tesla_device.name} ({self.type})" @property def unique_id(self) -> str: """Return a unique ID.""" if self.type: - return "{}_{}".format(self.tesla_id, self.type) + return f"{self.tesla_id}_{self.type}" return self.tesla_id @property diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 08e6afc3e56..70a16287fcc 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -86,7 +86,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) except HTTPError as error: - msg = "{}".format(error.strerror) + msg = f"{error.strerror}" if "EMAIL_NOT_FOUND" in msg or "INVALID_PASSWORD" in msg: _LOGGER.error("Invalid email and password combination") else: @@ -105,7 +105,7 @@ class ThermoworksSmokeSensor(Entity): self._state = None self._attributes = {} self._unit_of_measurement = TEMP_FAHRENHEIT - self._unique_id = "{serial}-{type}".format(serial=serial, type=sensor_type) + self._unique_id = f"{serial}-{sensor_type}" self.serial = serial self.mgr = mgr self.update_unit() diff --git a/homeassistant/components/thethingsnetwork/sensor.py b/homeassistant/components/thethingsnetwork/sensor.py index ccba2bc4b38..3ba58a688fe 100644 --- a/homeassistant/components/thethingsnetwork/sensor.py +++ b/homeassistant/components/thethingsnetwork/sensor.py @@ -65,7 +65,7 @@ class TtnDataSensor(Entity): self._device_id = device_id self._unit_of_measurement = unit_of_measurement self._value = value - self._name = "{} {}".format(self._device_id, self._value) + self._name = f"{self._device_id} {self._value}" @property def name(self): @@ -116,10 +116,7 @@ class TtnDataStorage: self._url = TTN_DATA_STORAGE_URL.format( app_id=app_id, endpoint="api/v2/query", device_id=device_id ) - self._headers = { - ACCEPT: CONTENT_TYPE_JSON, - AUTHORIZATION: "key {}".format(access_key), - } + self._headers = {ACCEPT: CONTENT_TYPE_JSON, AUTHORIZATION: f"key {access_key}"} async def async_update(self): """Get the current state from The Things Network Data Storage.""" diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 1985a85999b..d0f358c5902 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -3,7 +3,7 @@ "name": "Tibber", "documentation": "https://www.home-assistant.io/components/tibber", "requirements": [ - "pyTibber==0.11.6" + "pyTibber==0.11.7" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index aba6499ca6f..3dfe0265bde 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -149,7 +149,7 @@ class TibberSensorRT(Entity): self._device_state_attributes = {} self._unit_of_measurement = "W" nickname = tibber_home.info["viewer"]["home"]["appNickname"] - self._name = "Real time consumption {}".format(nickname) + self._name = f"Real time consumption {nickname}" async def async_added_to_hass(self): """Start unavailability tracking.""" @@ -215,4 +215,4 @@ class TibberSensorRT(Entity): """Return a unique ID.""" home = self._tibber_home.info["viewer"]["home"] _id = home["meteringPointData"]["consumptionEan"] - return "{}_rt_consumption".format(_id) + return f"{_id}_rt_consumption" diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index 02cde06d763..cbe4c85ace3 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -118,15 +118,15 @@ class TimeDateSensor(Entity): elif self.type == "date": self._state = date elif self.type == "date_time": - self._state = "{}, {}".format(date, time) + self._state = f"{date}, {time}" elif self.type == "time_date": - self._state = "{}, {}".format(time, date) + self._state = f"{time}, {date}" elif self.type == "time_utc": self._state = time_utc elif self.type == "beat": - self._state = "@{0:03d}".format(beat) + self._state = f"@{beat:03d}" elif self.type == "date_time_iso": - self._state = dt_util.parse_datetime("{} {}".format(date, time)).isoformat() + self._state = dt_util.parse_datetime(f"{date} {time}").isoformat() @callback def point_in_time_listener(self, time_date): diff --git a/homeassistant/components/toon/.translations/it.json b/homeassistant/components/toon/.translations/it.json index 696c770f130..79349135581 100644 --- a/homeassistant/components/toon/.translations/it.json +++ b/homeassistant/components/toon/.translations/it.json @@ -2,6 +2,7 @@ "config": { "abort": { "client_id": "L'ID client dalla configurazione non \u00e8 valido.", + "client_secret": "Il client segreto della configurazione non \u00e8 valido.", "no_agreements": "Questo account non ha display Toon.", "no_app": "\u00c8 necessario configurare Toon prima di poter eseguire l'autenticazione con esso. [Si prega di leggere le istruzioni] (https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Si \u00e8 verificato un errore imprevisto durante l'autenticazione." @@ -14,6 +15,7 @@ "authenticate": { "data": { "password": "Password", + "tenant": "Inquilino", "username": "Nome utente" }, "description": "Autenticati con il tuo account Eneco Toon (non l'account sviluppatore).", diff --git a/homeassistant/components/toon/.translations/pl.json b/homeassistant/components/toon/.translations/pl.json index 26627389ddd..403be9bc067 100644 --- a/homeassistant/components/toon/.translations/pl.json +++ b/homeassistant/components/toon/.translations/pl.json @@ -18,8 +18,8 @@ "tenant": "Najemca", "username": "Nazwa u\u017cytkownika" }, - "description": "Uwierzytelnij swoje konto Eneco Toon (nie konto programisty).", - "title": "Po\u0142\u0105cz swoje konto Toon" + "description": "Uwierzytelnij konto Eneco Toon (nie konto programisty).", + "title": "Po\u0142\u0105cz konto Toon" }, "display": { "data": { diff --git a/homeassistant/components/toon/.translations/zh-Hant.json b/homeassistant/components/toon/.translations/zh-Hant.json index b09d921268c..0156b58c9ac 100644 --- a/homeassistant/components/toon/.translations/zh-Hant.json +++ b/homeassistant/components/toon/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "client_id": "\u8a2d\u5b9a\u5167\u7528\u6236\u7aef ID \u7121\u6548\u3002", "client_secret": "\u8a2d\u5b9a\u5167\u5ba2\u6236\u7aef\u5bc6\u78bc\u7121\u6548\u3002", "no_agreements": "\u6b64\u5e33\u865f\u4e26\u672a\u64c1\u6709 Toon \u88dd\u7f6e\u3002", - "no_app": "\u5fc5\u9808\u5148\u8a2d\u5b9a Toon \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15]\uff08https://www.home-assistant.io/components/toon/\uff09\u3002", + "no_app": "\u5fc5\u9808\u5148\u8a2d\u5b9a Toon \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002[\u8acb\u53c3\u95b1\u6559\u5b78\u6307\u5f15](https://www.home-assistant.io/components/toon/(\u3002", "unknown_auth_fail": "\u9a57\u8b49\u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" }, "error": { diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index 6f1d9761fdf..60d49573833 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -168,8 +168,8 @@ class Tplink1DeviceScanner(DeviceScanner): """ _LOGGER.info("Loading wireless clients...") - url = "http://{}/userRpm/WlanStationRpm.htm".format(self.host) - referer = "http://{}".format(self.host) + url = f"http://{self.host}/userRpm/WlanStationRpm.htm" + referer = f"http://{self.host}" page = requests.get( url, auth=(self.username, self.password), @@ -205,16 +205,16 @@ class Tplink2DeviceScanner(Tplink1DeviceScanner): """ _LOGGER.info("Loading wireless clients...") - url = "http://{}/data/map_access_wireless_client_grid.json".format(self.host) - referer = "http://{}".format(self.host) + url = f"http://{self.host}/data/map_access_wireless_client_grid.json" + referer = f"http://{self.host}" # Router uses Authorization cookie instead of header # Let's create the cookie - username_password = "{}:{}".format(self.username, self.password) + username_password = f"{self.username}:{self.password}" b64_encoded_username_password = base64.b64encode( username_password.encode("ascii") ).decode("ascii") - cookie = "Authorization=Basic {}".format(b64_encoded_username_password) + cookie = f"Authorization=Basic {b64_encoded_username_password}" response = requests.post( url, headers={REFERER: referer, COOKIE: cookie}, timeout=4 @@ -264,8 +264,8 @@ class Tplink3DeviceScanner(Tplink1DeviceScanner): """Retrieve auth tokens from the router.""" _LOGGER.info("Retrieving auth tokens...") - url = "http://{}/cgi-bin/luci/;stok=/login?form=login".format(self.host) - referer = "http://{}/webpages/login.html".format(self.host) + url = f"http://{self.host}/cgi-bin/luci/;stok=/login?form=login" + referer = f"http://{self.host}/webpages/login.html" # If possible implement RSA encryption of password here. response = requests.post( @@ -303,7 +303,7 @@ class Tplink3DeviceScanner(Tplink1DeviceScanner): url = ( "http://{}/cgi-bin/luci/;stok={}/admin/wireless?" "form=statistics" ).format(self.host, self.stok) - referer = "http://{}/webpages/index.html".format(self.host) + referer = f"http://{self.host}/webpages/index.html" response = requests.post( url, @@ -346,7 +346,7 @@ class Tplink3DeviceScanner(Tplink1DeviceScanner): url = ("http://{}/cgi-bin/luci/;stok={}/admin/system?" "form=logout").format( self.host, self.stok ) - referer = "http://{}/webpages/index.html".format(self.host) + referer = f"http://{self.host}/webpages/index.html" requests.post( url, @@ -379,19 +379,19 @@ class Tplink4DeviceScanner(Tplink1DeviceScanner): def _get_auth_tokens(self): """Retrieve auth tokens from the router.""" _LOGGER.info("Retrieving auth tokens...") - url = "http://{}/userRpm/LoginRpm.htm?Save=Save".format(self.host) + url = f"http://{self.host}/userRpm/LoginRpm.htm?Save=Save" # Generate md5 hash of password. The C7 appears to use the first 15 # characters of the password only, so we truncate to remove additional # characters from being hashed. password = hashlib.md5(self.password.encode("utf")[:15]).hexdigest() - credentials = "{}:{}".format(self.username, password).encode("utf") + credentials = f"{self.username}:{password}".encode("utf") # Encode the credentials to be sent as a cookie. self.credentials = base64.b64encode(credentials).decode("utf") # Create the authorization cookie. - cookie = "Authorization=Basic {}".format(self.credentials) + cookie = f"Authorization=Basic {self.credentials}" response = requests.get(url, headers={COOKIE: cookie}) @@ -423,9 +423,9 @@ class Tplink4DeviceScanner(Tplink1DeviceScanner): # Check both the 2.4GHz and 5GHz client list URLs for clients_url in ("WlanStationRpm.htm", "WlanStationRpm_5g.htm"): - url = "http://{}/{}/userRpm/{}".format(self.host, self.token, clients_url) - referer = "http://{}".format(self.host) - cookie = "Authorization=Basic {}".format(self.credentials) + url = f"http://{self.host}/{self.token}/userRpm/{clients_url}" + referer = f"http://{self.host}" + cookie = f"Authorization=Basic {self.credentials}" page = requests.get(url, headers={COOKIE: cookie, REFERER: referer}) mac_results.extend(self.parse_macs.findall(page.text)) @@ -456,7 +456,7 @@ class Tplink5DeviceScanner(Tplink1DeviceScanner): """ _LOGGER.info("Loading wireless clients...") - base_url = "http://{}".format(self.host) + base_url = f"http://{self.host}" header = { USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;" @@ -466,7 +466,7 @@ class Tplink5DeviceScanner(Tplink1DeviceScanner): ACCEPT_ENCODING: "gzip, deflate", CONTENT_TYPE: "application/x-www-form-urlencoded; charset=UTF-8", HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest", - REFERER: "http://{}/".format(self.host), + REFERER: f"http://{self.host}/", CONNECTION: KEEP_ALIVE, PRAGMA: HTTP_HEADER_NO_CACHE, CACHE_CONTROL: HTTP_HEADER_NO_CACHE, @@ -484,7 +484,7 @@ class Tplink5DeviceScanner(Tplink1DeviceScanner): # A timestamp is required to be sent as get parameter timestamp = int(datetime.now().timestamp() * 1e3) - client_list_url = "{}/data/monitor.client.client.json".format(base_url) + client_list_url = f"{base_url}/data/monitor.client.client.json" get_params = {"operation": "load", "_": timestamp} diff --git a/homeassistant/components/traccar/.translations/ca.json b/homeassistant/components/traccar/.translations/ca.json new file mode 100644 index 00000000000..0cfb9738d5d --- /dev/null +++ b/homeassistant/components/traccar/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per rebre missatges de Traccar.", + "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + }, + "create_entry": { + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Traccar.\n\nUtilitza el seg\u00fcent enlla\u00e7: `{webhook_url}`\n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + }, + "step": { + "user": { + "description": "Est\u00e0s segur que vols configurar Traccar?", + "title": "Configura Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/da.json b/homeassistant/components/traccar/.translations/da.json new file mode 100644 index 00000000000..af3963f8c0f --- /dev/null +++ b/homeassistant/components/traccar/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Traccar meddelelser.", + "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + }, + "create_entry": { + "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Traccar.\n\n Brug f\u00f8lgende URL: `{webhook_url}`\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil konfigurere Traccar?", + "title": "Konfigurer Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/de.json b/homeassistant/components/traccar/.translations/de.json new file mode 100644 index 00000000000..c835ddf76b2 --- /dev/null +++ b/homeassistant/components/traccar/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Ihre Home Assistant-Instanz muss \u00fcber das Internet zug\u00e4nglich sein, um Nachrichten von Traccar zu empfangen.", + "one_instance_allowed": "Es ist nur eine einzelne Instanz erforderlich." + }, + "create_entry": { + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]({docs_url}) f\u00fcr weitere Details." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie Traccar wirklich einrichten?", + "title": "Traccar einrichten" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/es.json b/homeassistant/components/traccar/.translations/es.json new file mode 100644 index 00000000000..ab8c0e70cd4 --- /dev/null +++ b/homeassistant/components/traccar/.translations/es.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Su instancia de Home Assistant debe ser accesible desde Internet para recibir mensajes de Traccar." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/it.json b/homeassistant/components/traccar/.translations/it.json new file mode 100644 index 00000000000..a0980644a71 --- /dev/null +++ b/homeassistant/components/traccar/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La tua istanza di Home Assistant deve essere accessibile da Internet per ricevere messaggi da Traccar.", + "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." + }, + "create_entry": { + "default": "Per inviare eventi a Home Assistant, \u00e8 necessario configurare la funzionalit\u00e0 webhook in Traccar.\n\nUtilizzare l'URL seguente: `{webhook_url}`\n\nPer ulteriori dettagli, vedere [la documentazione]({docs_url}) ." + }, + "step": { + "user": { + "description": "Sei sicuro di voler configurare Traccar?", + "title": "Imposta Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/ko.json b/homeassistant/components/traccar/.translations/ko.json new file mode 100644 index 00000000000..d9f31967e68 --- /dev/null +++ b/homeassistant/components/traccar/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Traccar \uba54\uc2dc\uc9c0\ub97c \ubc1b\uc73c\ub824\uba74 \uc778\ud130\ub137\uc5d0\uc11c Home Assistant \uc778\uc2a4\ud134\uc2a4\uc5d0 \uc561\uc138\uc2a4 \ud560 \uc218 \uc788\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "create_entry": { + "default": "Home Assistant \ub85c \uc774\ubca4\ud2b8\ub97c \ubcf4\ub0b4\ub824\uba74 Traccar \uc5d0\uc11c Webhook \uc744 \uc124\uc815\ud574\uc57c\ud569\ub2c8\ub2e4. \n\n\ub2e4\uc74c URL \uc815\ubcf4\ub97c \uc0ac\uc6a9\ud569\ub2c8\ub2e4: `{webhook_url}`\n \n\uc790\uc138\ud55c \uc815\ubcf4\ub294 [\uc548\ub0b4]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + }, + "step": { + "user": { + "description": "Traccar \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Traccar \uc124\uc815" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/nl.json b/homeassistant/components/traccar/.translations/nl.json new file mode 100644 index 00000000000..c4ee0544a2e --- /dev/null +++ b/homeassistant/components/traccar/.translations/nl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Uw Home Assistant-exemplaar moet toegankelijk zijn vanaf internet om berichten van Traccar te ontvangen.", + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, + "create_entry": { + "default": "Voor het verzenden van gebeurtenissen naar Home Assistant, moet u de webhook-functie in Traccar instellen.\n\nGebruik de volgende URL: ' {webhook_url} '\n\nZie [de documentatie] ({docs_url}) voor meer informatie." + }, + "step": { + "user": { + "description": "Weet u zeker dat u Traccar wilt instellen?", + "title": "Traccar instellen" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/no.json b/homeassistant/components/traccar/.translations/no.json new file mode 100644 index 00000000000..dea146b649a --- /dev/null +++ b/homeassistant/components/traccar/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Din Home Assistant-forekomst m\u00e5 v\u00e6re tilgjengelig fra Internett for \u00e5 motta meldinger fra Traccar.", + "one_instance_allowed": "Kun en enkelt forekomst er n\u00f8dvendig." + }, + "create_entry": { + "default": "Hvis du vil sende hendelser til Home Assistant, m\u00e5 du konfigurere webhook-funksjonen i Traccar.\n\nBruk f\u00f8lgende URL-adresse: ' {webhook_url} '\n\nSe [dokumentasjonen] ({docs_url}) for mer informasjon." + }, + "step": { + "user": { + "description": "Er du sikker p\u00e5 at du vil sette opp Traccar?", + "title": "Sett opp Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/pl.json b/homeassistant/components/traccar/.translations/pl.json new file mode 100644 index 00000000000..66ddbaaa3fd --- /dev/null +++ b/homeassistant/components/traccar/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Twoja instancja Home Assistant musi by\u0107 dost\u0119pna z Internetu, aby otrzymywa\u0107 wiadomo\u015bci z Traccar.", + "one_instance_allowed": "Niezb\u0119dna jest tylko jedna instancja." + }, + "create_entry": { + "default": "Aby wys\u0142a\u0107 wydarzenia do Home Assistant, musisz skonfigurowa\u0107 funkcj\u0119 webhook w Traccar. \n\n U\u017cyj nast\u0119puj\u0105cego URL: ` {webhook_url} ` \n\n Zobacz [dokumentacj\u0119] ( {docs_url} ) w celu uzyskania dalszych szczeg\u00f3\u0142\u00f3w." + }, + "step": { + "user": { + "description": "Czy na pewno chcesz skonfigurowa\u0107 Traccar?", + "title": "Skonfiguruj Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/ru.json b/homeassistant/components/traccar/.translations/ru.json new file mode 100644 index 00000000000..afaab87efe4 --- /dev/null +++ b/homeassistant/components/traccar/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u0437 \u0438\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 Traccar.", + "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Traccar.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\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 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Traccar?", + "title": "Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/sl.json b/homeassistant/components/traccar/.translations/sl.json new file mode 100644 index 00000000000..95aaca7e67d --- /dev/null +++ b/homeassistant/components/traccar/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostopen prek interneta, da boste lahko prejemali Traccar sporo\u010dila.", + "one_instance_allowed": "Potrebna je samo ena instanca." + }, + "create_entry": { + "default": "\u010ce \u017eelite poslati dogodke v Home Assistant, boste morali nastaviti funkcijo \"webhook\" v traccar.\n\nUporabite naslednji URL: ' {webhook_url} '\n\nZa podrobnej\u0161e informacije glejte [dokumentacijo] ({docs_url})." + }, + "step": { + "user": { + "description": "Ali ste prepri\u010dani, da \u017eelite nastaviti Traccar?", + "title": "Nastavite Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/zh-Hant.json b/homeassistant/components/traccar/.translations/zh-Hant.json new file mode 100644 index 00000000000..f5402454294 --- /dev/null +++ b/homeassistant/components/traccar/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Home Assistant \u5be6\u4f8b\u5fc5\u9808\u80fd\u5920\u7531\u7db2\u969b\u7db2\u8def\u5b58\u53d6\uff0c\u65b9\u80fd\u63a5\u53d7 Traccar \u8a0a\u606f\u3002", + "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + }, + "create_entry": { + "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Traccar \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u4f7f\u7528 url: `{webhook_url}`\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Traccar\uff1f", + "title": "\u8a2d\u5b9a Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 8e3f90fb66f..5eb87de0db2 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -24,7 +24,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" DEFAULT_ACCURACY = 200 @@ -83,7 +83,7 @@ async def handle_webhook(hass, webhook_id, request): attrs, ) - return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + return web.Response(text=f"Setting location for {device}", status=HTTP_OK) async def async_setup_entry(hass, entry): diff --git a/homeassistant/components/tradfri/.translations/it.json b/homeassistant/components/tradfri/.translations/it.json index 4c114492336..99ba9053d79 100644 --- a/homeassistant/components/tradfri/.translations/it.json +++ b/homeassistant/components/tradfri/.translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Il bridge \u00e8 gi\u00e0 configurato" + "already_configured": "Bridge gi\u00e0 configurato.", + "already_in_progress": "La configurazione del Bridge \u00e8 gi\u00e0 in corso." }, "error": { "cannot_connect": "Impossibile connettersi al gateway.", diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index e3fcfc89c5b..3a1798e66d9 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -15,7 +15,7 @@ "host": "Host", "security_code": "Kod bezpiecze\u0144stwa" }, - "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramy.", + "description": "Mo\u017cesz znale\u017a\u0107 kod bezpiecze\u0144stwa z ty\u0142u bramki.", "title": "Wprowad\u017a kod bezpiecze\u0144stwa" } }, diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 7992bf459db..97fdfd9d36d 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -57,7 +57,7 @@ class TradfriGroup(Light): def __init__(self, group, api, gateway_id): """Initialize a Group.""" self._api = api - self._unique_id = "group-{}-{}".format(gateway_id, group.id) + self._unique_id = f"group-{gateway_id}-{group.id}" self._group = group self._name = group.name @@ -152,7 +152,7 @@ class TradfriLight(Light): def __init__(self, light, api, gateway_id): """Initialize a Light.""" self._api = api - self._unique_id = "light-{}-{}".format(gateway_id, light.id) + self._unique_id = f"light-{gateway_id}-{light.id}" self._light = None self._light_control = None self._light_data = None diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 2b1bb0d5c54..4be72eb7359 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -34,7 +34,7 @@ class TradfriSwitch(SwitchDevice): def __init__(self, switch, api, gateway_id): """Initialize a switch.""" self._api = api - self._unique_id = "{}-{}".format(gateway_id, switch.id) + self._unique_id = f"{gateway_id}-{switch.id}" self._switch = None self._socket_control = None self._switch_data = None diff --git a/homeassistant/components/trafikverket_weatherstation/sensor.py b/homeassistant/components/trafikverket_weatherstation/sensor.py index 9c79aa5cda6..cb80e8d441b 100644 --- a/homeassistant/components/trafikverket_weatherstation/sensor.py +++ b/homeassistant/components/trafikverket_weatherstation/sensor.py @@ -147,7 +147,7 @@ class TrafikverketWeatherStation(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._client, self._name) + return f"{self._client} {self._name}" @property def icon(self): diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 9e5397dd9fb..ac2e64ce92f 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -63,7 +63,7 @@ class TransmissionSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): diff --git a/homeassistant/components/travisci/sensor.py b/homeassistant/components/travisci/sensor.py index 546fe7606e7..b86b62fc1e9 100644 --- a/homeassistant/components/travisci/sensor.py +++ b/homeassistant/components/travisci/sensor.py @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for repo in repositories: if "/" not in repo: - repo = "{0}/{1}".format(user.login, repo) + repo = f"{user.login}/{repo}" for sensor_type in config.get(CONF_MONITORED_CONDITIONS): sensors.append(TravisCISensor(travis, repo, user, branch, sensor_type)) diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index b9c01c15d20..8719138f3ac 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -3,8 +3,8 @@ "name": "Trend", "documentation": "https://www.home-assistant.io/components/trend", "requirements": [ - "numpy==1.17.0" + "numpy==1.17.1" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 77d24fd7aab..3e7900502d6 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -165,9 +165,7 @@ async def async_setup(hass, config): DOMAIN_MP, SERVICE_PLAY_MEDIA, data, blocking=True ) - service_name = p_config.get( - CONF_SERVICE_NAME, "{}_{}".format(p_type, SERVICE_SAY) - ) + service_name = p_config.get(CONF_SERVICE_NAME, f"{p_type}_{SERVICE_SAY}") hass.services.async_register( DOMAIN, service_name, async_say_handle, schema=SCHEMA_SERVICE_SAY ) @@ -229,7 +227,7 @@ class SpeechManager: init_tts_cache_dir, cache_dir ) except OSError as err: - raise HomeAssistantError("Can't init cache dir {}".format(err)) + raise HomeAssistantError(f"Can't init cache dir {err}") def get_cache_files(): """Return a dict of given engine files.""" @@ -251,7 +249,7 @@ class SpeechManager: try: cache_files = await self.hass.async_add_job(get_cache_files) except OSError as err: - raise HomeAssistantError("Can't read cache dir {}".format(err)) + raise HomeAssistantError(f"Can't read cache dir {err}") if cache_files: self.file_cache.update(cache_files) @@ -293,7 +291,7 @@ class SpeechManager: # Languages language = language or provider.default_language if language is None or language not in provider.supported_languages: - raise HomeAssistantError("Not supported language {0}".format(language)) + raise HomeAssistantError(f"Not supported language {language}") # Options if provider.default_options and options: @@ -308,9 +306,7 @@ class SpeechManager: if opt_name not in (provider.supported_options or []) ] if invalid_opts: - raise HomeAssistantError( - "Invalid options found: {}".format(invalid_opts) - ) + raise HomeAssistantError(f"Invalid options found: {invalid_opts}") options_key = ctypes.c_size_t(hash(frozenset(options))).value else: options_key = "-" @@ -330,7 +326,7 @@ class SpeechManager: engine, key, message, use_cache, language, options ) - return "{}/api/tts_proxy/{}".format(self.base_url, filename) + return f"{self.base_url}/api/tts_proxy/{filename}" async def async_get_tts_audio(self, engine, key, message, cache, language, options): """Receive TTS and store for view in cache. @@ -341,10 +337,10 @@ class SpeechManager: extension, data = await provider.async_get_tts_audio(message, language, options) if data is None or extension is None: - raise HomeAssistantError("No TTS from {} for '{}'".format(engine, message)) + raise HomeAssistantError(f"No TTS from {engine} for '{message}'") # Create file infos - filename = ("{}.{}".format(key, extension)).lower() + filename = (f"{key}.{extension}").lower() data = self.write_tags(filename, data, provider, message, language, options) @@ -381,7 +377,7 @@ class SpeechManager: """ filename = self.file_cache.get(key) if not filename: - raise HomeAssistantError("Key {} not in file cache!".format(key)) + raise HomeAssistantError(f"Key {key} not in file cache!") voice_file = os.path.join(self.cache_dir, filename) @@ -394,7 +390,7 @@ class SpeechManager: data = await self.hass.async_add_job(load_speech) except OSError: del self.file_cache[key] - raise HomeAssistantError("Can't read {}".format(voice_file)) + raise HomeAssistantError(f"Can't read {voice_file}") self._async_store_to_memcache(key, filename, data) @@ -425,7 +421,7 @@ class SpeechManager: if key not in self.mem_cache: if key not in self.file_cache: - raise HomeAssistantError("{} not in cache!".format(key)) + raise HomeAssistantError(f"{key} not in cache!") await self.async_file_to_mem(key) content, _ = mimetypes.guess_type(filename) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 2c72dd60490..9ac72419612 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -40,6 +40,8 @@ class TuyaLight(TuyaDevice, Light): @property def brightness(self): """Return the brightness of the light.""" + if self.tuya.brightness() is None: + return None return int(self.tuya.brightness()) @property diff --git a/homeassistant/components/twentemilieu/.translations/ca.json b/homeassistant/components/twentemilieu/.translations/ca.json new file mode 100644 index 00000000000..27ab8e8a8b2 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adre\u00e7a ja configurada." + }, + "error": { + "connection_error": "No s'ha pogut connectar.", + "invalid_address": "No s'ha trobat l'adre\u00e7a a l'\u00e0rea de servei de Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Lletra/addicional de casa", + "house_number": "N\u00famero de casa", + "post_code": "Codi postal" + }, + "description": "Configura Twente Milieu amb informaci\u00f3 de la recollida de residus a la teva adre\u00e7a.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/da.json b/homeassistant/components/twentemilieu/.translations/da.json new file mode 100644 index 00000000000..1e3ca933e38 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse er allerede konfigureret." + }, + "error": { + "connection_error": "Forbindelse mislykkedes.", + "invalid_address": "Adresse ikke fundet i Twente Milieu serviceomr\u00e5de." + }, + "step": { + "user": { + "data": { + "house_letter": "Hus nummer/yderligere", + "house_number": "Husnummer", + "post_code": "Postnummer" + }, + "description": "Konfigurer Twente Milieu, der leverer oplysninger om indsamling af affald p\u00e5 din adresse.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/de.json b/homeassistant/components/twentemilieu/.translations/de.json new file mode 100644 index 00000000000..502a54a8a3d --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adresse bereits eingerichtet." + }, + "error": { + "connection_error": "Fehler beim Herstellen einer Verbindung.", + "invalid_address": "Adresse nicht im Einzugsgebiet von Twente Milieu gefunden." + }, + "step": { + "user": { + "data": { + "house_letter": "Hausbrief/zusatz", + "house_number": "Hausnummer", + "post_code": "Postleitzahl" + }, + "description": "Richten Sie Twente Milieu mit Informationen zur Abfallsammlung unter Ihrer Adresse ein.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/es.json b/homeassistant/components/twentemilieu/.translations/es.json new file mode 100644 index 00000000000..02dcb71f54e --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "connection_error": "No se conect\u00f3." + }, + "step": { + "user": { + "data": { + "house_letter": "Letra de la casa/adicional", + "house_number": "N\u00famero de casa", + "post_code": "C\u00f3digo postal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/hu.json b/homeassistant/components/twentemilieu/.translations/hu.json new file mode 100644 index 00000000000..439e02d1027 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "address_exists": "A c\u00edm m\u00e1r be lett \u00e1ll\u00edtva." + }, + "error": { + "connection_error": "Nem siker\u00fclt csatlakozni." + }, + "step": { + "user": { + "data": { + "house_number": "h\u00e1zsz\u00e1m", + "post_code": "ir\u00e1ny\u00edt\u00f3sz\u00e1m" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/it.json b/homeassistant/components/twentemilieu/.translations/it.json new file mode 100644 index 00000000000..27850d207b0 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Indirizzo gi\u00e0 impostato." + }, + "error": { + "connection_error": "Impossibile connettersi.", + "invalid_address": "Indirizzo non trovato nell'area di servizio di Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Edificio, Scala, Interno, ecc. / Informazioni aggiuntive", + "house_number": "Numero civico", + "post_code": "Codice di Avviamento Postale" + }, + "description": "Imposta Twente Milieu fornendo le informazioni sulla raccolta dei rifiuti al tuo indirizzo.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/ko.json b/homeassistant/components/twentemilieu/.translations/ko.json new file mode 100644 index 00000000000..a78867d86a8 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\uc8fc\uc18c\uac00 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "invalid_address": "Twente Milieu \uc11c\ube44\uc2a4 \uc9c0\uc5ed\uc5d0\uc11c \uc8fc\uc18c\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "house_letter": "\uc9d1 \uc8fc\uc18c/\ucd94\uac00\uc815\ubcf4", + "house_number": "\uc9d1 \ubc88\ud638", + "post_code": "\uc6b0\ud3b8\ubc88\ud638" + }, + "description": "\uc8fc\uc18c\uc5d0 \uc4f0\ub808\uae30 \uc218\uac70 \uc815\ubcf4\ub97c \ub123\uc5b4 Twente Milieu \ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/nl.json b/homeassistant/components/twentemilieu/.translations/nl.json new file mode 100644 index 00000000000..a420133f464 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adres al ingesteld." + }, + "error": { + "connection_error": "Kon niet verbinden.", + "invalid_address": "Adres niet gevonden in servicegebied Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "Huisnummer / toevoeging", + "house_number": "Huisnummer", + "post_code": "Postcode" + }, + "description": "Stel Twente Milieu in voor het inzamelen van afval op uw adres.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/no.json b/homeassistant/components/twentemilieu/.translations/no.json new file mode 100644 index 00000000000..1d4395bb2c8 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adressen er allerede konfigurert." + }, + "error": { + "connection_error": "Tilkobling mislyktes.", + "invalid_address": "Adresse ble ikke funnet i Twente Milieu tjenesteomr\u00e5de." + }, + "step": { + "user": { + "data": { + "house_letter": "Hus brev/ekstra", + "house_number": "Husnummer", + "post_code": "Postnummer" + }, + "description": "Sett opp Twente Milieu som gir informasjon om innsamling av avfall p\u00e5 adressen din.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/pl.json b/homeassistant/components/twentemilieu/.translations/pl.json new file mode 100644 index 00000000000..042fcf0dda6 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Adres ju\u017c skonfigurowany." + }, + "error": { + "connection_error": "Po\u0142\u0105czenie nieudane.", + "invalid_address": "Nie znaleziono adresu w obszarze us\u0142ugi Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "List domowy / dodatkowy", + "house_number": "Numer domu", + "post_code": "Kod pocztowy" + }, + "description": "Skonfiguruj Twente Milieu, dostarczaj\u0105c informacji o zbieraniu odpad\u00f3w pod swoim adresem.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/ru.json b/homeassistant/components/twentemilieu/.translations/ru.json new file mode 100644 index 00000000000..5d964604a77 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\u0410\u0434\u0440\u0435\u0441 \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d." + }, + "error": { + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_address": "\u0410\u0434\u0440\u0435\u0441 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d \u0432 \u0437\u043e\u043d\u0435 \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u044f Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "\u041b\u0438\u0442\u0435\u0440 \u0434\u043e\u043c\u0430 / \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435", + "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0434\u043e\u043c\u0430", + "post_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 Twente Milieu \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0432\u044b\u0432\u043e\u0437\u0435 \u043c\u0443\u0441\u043e\u0440\u0430 \u043f\u043e \u0412\u0430\u0448\u0435\u043c\u0443 \u0430\u0434\u0440\u0435\u0441\u0443.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/sl.json b/homeassistant/components/twentemilieu/.translations/sl.json new file mode 100644 index 00000000000..7b74b96d057 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "Naslov je \u017ee nastavljen." + }, + "error": { + "connection_error": "Povezava ni uspela.", + "invalid_address": "V storitvenem obmo\u010dju Twente Milieu ni mogo\u010de najti naslova." + }, + "step": { + "user": { + "data": { + "house_letter": "Hi\u0161na \u0161tevilka -\u010drka/dodatno", + "house_number": "Hi\u0161na \u0161tevilka", + "post_code": "Po\u0161tna \u0161tevilka" + }, + "description": "Nastavite Twente milieu, ki zagotavlja informacije o zbiranju odpadkov na va\u0161em naslovu.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/zh-Hant.json b/homeassistant/components/twentemilieu/.translations/zh-Hant.json new file mode 100644 index 00000000000..0e0083ec5c1 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "address_exists": "\u5730\u5740\u5df2\u8a2d\u5b9a\u3002" + }, + "error": { + "connection_error": "\u9023\u7dda\u5931\u6557\u3002", + "invalid_address": "Twente Milieu \u670d\u52d9\u5340\u57df\u5167\u627e\u4e0d\u5230\u6b64\u5730\u5740\u3002" + }, + "step": { + "user": { + "data": { + "house_letter": "\u9580\u724c\u5b57\u6bcd/\u9644\u52a0\u8cc7\u8a0a", + "house_number": "\u9580\u724c\u865f\u78bc", + "post_code": "\u90f5\u905e\u5340\u865f" + }, + "description": "\u8a2d\u5b9a Twente Milieu \u4ee5\u53d6\u5f97\u8a72\u5730\u5740\u5ee2\u68c4\u7269\u56de\u6536\u8cc7\u8a0a\u3002", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/sensor.py b/homeassistant/components/twentemilieu/sensor.py index 9dc109c98de..b1be9a071e4 100644 --- a/homeassistant/components/twentemilieu/sensor.py +++ b/homeassistant/components/twentemilieu/sensor.py @@ -40,28 +40,28 @@ async def async_setup_entry( TwenteMilieuSensor( twentemilieu, unique_id=entry.data[CONF_ID], - name="{} Waste Pickup".format(WASTE_TYPE_NON_RECYCLABLE), + name=f"{WASTE_TYPE_NON_RECYCLABLE} Waste Pickup", waste_type=WASTE_TYPE_NON_RECYCLABLE, icon="mdi:delete-empty", ), TwenteMilieuSensor( twentemilieu, unique_id=entry.data[CONF_ID], - name="{} Waste Pickup".format(WASTE_TYPE_ORGANIC), + name=f"{WASTE_TYPE_ORGANIC} Waste Pickup", waste_type=WASTE_TYPE_ORGANIC, icon="mdi:delete-empty", ), TwenteMilieuSensor( twentemilieu, unique_id=entry.data[CONF_ID], - name="{} Waste Pickup".format(WASTE_TYPE_PAPER), + name=f"{WASTE_TYPE_PAPER} Waste Pickup", waste_type=WASTE_TYPE_PAPER, icon="mdi:delete-empty", ), TwenteMilieuSensor( twentemilieu, unique_id=entry.data[CONF_ID], - name="{} Waste Pickup".format(WASTE_TYPE_PLASTIC), + name=f"{WASTE_TYPE_PLASTIC} Waste Pickup", waste_type=WASTE_TYPE_PLASTIC, icon="mdi:delete-empty", ), @@ -110,7 +110,7 @@ class TwenteMilieuSensor(Entity): @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" - return "{}_{}_{}".format(DOMAIN, self._unique_id, self._waste_type) + return f"{DOMAIN}_{self._unique_id}_{self._waste_type}" @property def should_poll(self) -> bool: diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index 74264a31f06..ea5629e7cab 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -11,7 +11,7 @@ CONF_AUTH_TOKEN = "auth_token" DATA_TWILIO = DOMAIN -RECEIVED_DATA = "{}_data_received".format(DOMAIN) +RECEIVED_DATA = f"{DOMAIN}_data_received" CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/ubus/device_tracker.py b/homeassistant/components/ubus/device_tracker.py index 53cd900c0d4..f14ea5af02c 100644 --- a/homeassistant/components/ubus/device_tracker.py +++ b/homeassistant/components/ubus/device_tracker.py @@ -80,7 +80,7 @@ class UbusDeviceScanner(DeviceScanner): self.parse_api_pattern = re.compile(r"(?P\w*) = (?P.*);") self.last_results = {} - self.url = "http://{}/ubus".format(host) + self.url = f"http://{host}/ubus" self.session_id = _get_session_id(self.url, self.username, self.password) self.hostapd = [] diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index 38183d23a0e..eb325d32212 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -11,6 +11,7 @@ import requests import voluptuous as vol import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_MODE from homeassistant.helpers.entity import Entity @@ -157,10 +158,10 @@ class UkTransportLiveBusTimeSensor(UkTransportSensor): self._stop_atcocode = stop_atcocode self._bus_direction = bus_direction self._next_buses = [] - self._destination_re = re.compile("{}".format(bus_direction), re.IGNORECASE) + self._destination_re = re.compile(f"{bus_direction}", re.IGNORECASE) - sensor_name = "Next bus to {}".format(bus_direction) - stop_url = "bus/stop/{}/live.json".format(stop_atcocode) + sensor_name = f"Next bus to {bus_direction}" + stop_url = f"bus/stop/{stop_atcocode}/live.json" UkTransportSensor.__init__(self, sensor_name, api_app_id, api_app_key, stop_url) self.update = Throttle(interval)(self._update) @@ -220,8 +221,8 @@ class UkTransportLiveTrainTimeSensor(UkTransportSensor): self._calling_at = calling_at self._next_trains = [] - sensor_name = "Next train to {}".format(calling_at) - query_url = "train/station/{}/live.json".format(station_code) + sensor_name = f"Next train to {calling_at}" + query_url = f"train/station/{station_code}/live.json" UkTransportSensor.__init__( self, sensor_name, api_app_id, api_app_key, query_url @@ -277,12 +278,11 @@ class UkTransportLiveTrainTimeSensor(UkTransportSensor): def _delta_mins(hhmm_time_str): """Calculate time delta in minutes to a time in hh:mm format.""" - now = datetime.now() + now = dt_util.now() hhmm_time = datetime.strptime(hhmm_time_str, "%H:%M") - hhmm_datetime = datetime( - now.year, now.month, now.day, hour=hhmm_time.hour, minute=hhmm_time.minute - ) + hhmm_datetime = now.replace(hour=hhmm_time.hour, minute=hhmm_time.minute) + if hhmm_datetime < now: hhmm_datetime += timedelta(days=1) diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 442d82d9a3f..8a8d8b11f57 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -22,5 +22,17 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Temps (en segons) des de s'ha vist per \u00faltima vegada fins que es considera a fora", + "track_clients": "Segueix clients de la xarxa", + "track_devices": "Segueix dispositius de la xarxa (dispositius Ubiquiti)", + "track_wired_clients": "Inclou clients de xarxa per cable" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 4155658d7de..53b794ed435 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -22,5 +22,17 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tid i sekunder fra sidst set indtil betragtet som v\u00e6k", + "track_clients": "Spor netv\u00e6rksklienter", + "track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)", + "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index 2b71d01417b..e447e89644f 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -22,5 +22,23 @@ } }, "title": "UniFi-Controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Zeit in Sekunden vom letzten Gesehenen bis zur Entfernung", + "track_clients": "Nachverfolgen von Netzwerkclients", + "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)", + "track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients" + } + }, + "init": { + "data": { + "one": "eins", + "other": "andere" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 4f570fe1386..8b0eb562037 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -22,5 +22,16 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", + "track_clients": "Seguimiento de los clientes de red", + "track_wired_clients": "Incluir clientes de red cableada" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index 407371bf89f..5285ed21873 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -22,5 +22,23 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano", + "track_clients": "Traccia i client di rete", + "track_devices": "Tracciare i dispositivi di rete (dispositivi Ubiquiti)", + "track_wired_clients": "Includi i client di rete cablata" + } + }, + "init": { + "data": { + "one": "uno", + "other": "altro" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index 431d6bbf5e6..1fff9887906 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -22,5 +22,17 @@ } }, "title": "UniFi \ucee8\ud2b8\ub864\ub7ec" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ud655\uc778\ub41c \uc2dc\uac04\ubd80\ud130 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\ub294 \uc2dc\uac04 (\ucd08)", + "track_clients": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uc801 \ub300\uc0c1", + "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)", + "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index 7a1eea546a2..f907364327c 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -22,5 +22,17 @@ } }, "title": "UniFi-controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tijd in seconden vanaf laatst gezien tot beschouwd als weg", + "track_clients": "Volg netwerkclients", + "track_devices": "Netwerkapparaten volgen (Ubiquiti-apparaten)", + "track_wired_clients": "Inclusief bedrade netwerkcli\u00ebnten" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index 541b0f60d17..068f4341544 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -22,5 +22,17 @@ } }, "title": "UniFi kontroller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tid i sekunder fra sist sett til den ble ansett borte", + "track_clients": "Spor nettverksklienter", + "track_devices": "Spore nettverksenheter (Ubiquiti-enheter)", + "track_wired_clients": "Inkluder kablede nettverksklienter" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 5382adcbf7d..6366f82b3da 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -22,5 +22,25 @@ } }, "title": "Kontroler UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Czas w sekundach od momentu, kiedy ostatnio widziano, a\u017c do momentu, kiedy uznano go za nieobecny.", + "track_clients": "\u015aled\u017a klient\u00f3w sieciowych", + "track_devices": "\u015aled\u017a urz\u0105dzenia sieciowe (urz\u0105dzenia Ubiquiti)", + "track_wired_clients": "Uwzgl\u0119dnij klient\u00f3w sieci przewodowej" + } + }, + "init": { + "data": { + "few": "Kilka", + "many": "Wiele", + "one": "Jeden", + "other": "Inne" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index f4d86300aca..76802a96367 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -22,5 +22,17 @@ } }, "title": "UniFi Controller" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".", + "track_clients": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u0441\u0435\u0442\u0438", + "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", + "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/sl.json b/homeassistant/components/unifi/.translations/sl.json index 7543542abbf..35000bf4e1f 100644 --- a/homeassistant/components/unifi/.translations/sl.json +++ b/homeassistant/components/unifi/.translations/sl.json @@ -22,5 +22,25 @@ } }, "title": "UniFi Krmilnik" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u010cas v sekundah od zadnjega videnja na omre\u017eju do odsotnosti", + "track_clients": "Sledite odjemalcem omre\u017eja", + "track_devices": "Sledite omre\u017enim napravam (naprave Ubiquiti)", + "track_wired_clients": "Vklju\u010dite kliente iz o\u017ei\u010denega omre\u017eja" + } + }, + "init": { + "data": { + "few": "NEKAJ", + "one": "ENA", + "other": "OSTALO", + "two": "DVA" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index e506c582cb7..2d5bd9027ac 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -22,5 +22,17 @@ } }, "title": "UniFi \u63a7\u5236\u5668" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09", + "track_clients": "\u8ffd\u8e64\u7db2\u8def\u5ba2\u6236\u7aef", + "track_devices": "\u8ffd\u8e64\u7db2\u8def\u88dd\u7f6e\uff08Ubiquiti \u88dd\u7f6e\uff09", + "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index da9bbb8e59e..db635828529 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,6 +1,9 @@ """Support for devices connected to UniFi POE.""" import voluptuous as vol +from homeassistant.components.unifi.config_flow import ( + get_controller_id_from_config_entry, +) from homeassistant.const import CONF_HOST from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -9,20 +12,18 @@ import homeassistant.helpers.config_validation as cv from .const import ( ATTR_MANUFACTURER, CONF_BLOCK_CLIENT, - CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_DONT_TRACK_CLIENTS, + CONF_DONT_TRACK_DEVICES, + CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, - CONTROLLER_ID, DOMAIN, UNIFI_CONFIG, ) from .controller import UniFiController CONF_CONTROLLERS = "controllers" -CONF_DONT_TRACK_CLIENTS = "dont_track_clients" -CONF_DONT_TRACK_DEVICES = "dont_track_devices" -CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" CONTROLLER_SCHEMA = vol.Schema( { @@ -70,10 +71,7 @@ async def async_setup_entry(hass, config_entry): controller = UniFiController(hass, config_entry) - controller_id = CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) + controller_id = get_controller_id_from_config_entry(config_entry) hass.data[DOMAIN][controller_id] = controller @@ -98,9 +96,6 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - controller_id = CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) + controller_id = get_controller_id_from_config_entry(config_entry) controller = hass.data[DOMAIN].pop(controller_id) return await controller.async_reset() diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e1f0a91c774..fdb75d09194 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Unifi.""" +"""Config flow for UniFi.""" import voluptuous as vol from homeassistant import config_entries @@ -13,11 +13,12 @@ from homeassistant.const import ( from .const import ( CONF_CONTROLLER, + CONF_DETECTION_TIME, + CONF_SITE_ID, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONF_DETECTION_TIME, - CONF_SITE_ID, + CONTROLLER_ID, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, @@ -33,8 +34,22 @@ DEFAULT_SITE_ID = "default" DEFAULT_VERIFY_SSL = False -@config_entries.HANDLERS.register(DOMAIN) -class UnifiFlowHandler(config_entries.ConfigFlow): +@callback +def get_controller_id_from_config_entry(config_entry): + """Return controller with a matching bridge id.""" + return CONTROLLER_ID.format( + host=config_entry.data[CONF_CONTROLLER][CONF_HOST], + site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], + ) + + +@callback +def get_controller_from_config_entry(hass, config_entry): + """Return controller with a matching bridge id.""" + return hass.data[DOMAIN][get_controller_id_from_config_entry(config_entry)] + + +class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a UniFi config flow.""" VERSION = 1 @@ -149,20 +164,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow): errors=errors, ) - async def async_step_import(self, import_config): - """Import from UniFi device tracker config.""" - config = { - CONF_HOST: import_config[CONF_HOST], - CONF_USERNAME: import_config[CONF_USERNAME], - CONF_PASSWORD: import_config[CONF_PASSWORD], - CONF_PORT: import_config.get(CONF_PORT), - CONF_VERIFY_SSL: import_config.get(CONF_VERIFY_SSL), - } - - self.desc = import_config[CONF_SITE_ID] - - return await self.async_step_user(user_input=config) - class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi options.""" diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index ffa9a28818b..4522ac4254a 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -18,6 +18,10 @@ CONF_TRACK_DEVICES = "track_devices" CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" +CONF_DONT_TRACK_CLIENTS = "dont_track_clients" +CONF_DONT_TRACK_DEVICES = "dont_track_devices" +CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" + DEFAULT_BLOCK_CLIENTS = [] DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 47c692b12b2..b29b088a815 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -18,6 +18,9 @@ from .const import ( CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, + CONF_DONT_TRACK_CLIENTS, + CONF_DONT_TRACK_DEVICES, + CONF_DONT_TRACK_WIRED_CLIENTS, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, @@ -30,6 +33,7 @@ from .const import ( DEFAULT_TRACK_WIRED_CLIENTS, DEFAULT_DETECTION_TIME, DEFAULT_SSID_FILTER, + DOMAIN, LOGGER, UNIFI_CONFIG, ) @@ -49,7 +53,6 @@ class UniFiController: self._site_name = None self._site_role = None - self.unifi_config = {} @property def host(self): @@ -116,11 +119,14 @@ class UniFiController: return None @property - def event_update(self): + def signal_update(self): """Event specific per UniFi entry to signal new data.""" - return "unifi-update-{}".format( - CONTROLLER_ID.format(host=self.host, site=self.site) - ) + return f"unifi-update-{CONTROLLER_ID.format(host=self.host, site=self.site)}" + + @property + def signal_options_update(self): + """Event specific per UniFi entry to signal new options.""" + return f"unifi-options-{CONTROLLER_ID.format(host=self.host, site=self.site)}" async def request_update(self): """Request an update.""" @@ -164,7 +170,7 @@ class UniFiController: LOGGER.info("Reconnected to controller %s", self.host) self.available = True - async_dispatcher_send(self.hass, self.event_update) + async_dispatcher_send(self.hass, self.signal_update) async def async_setup(self): """Set up a UniFi controller.""" @@ -191,37 +197,9 @@ class UniFiController: LOGGER.error("Unknown error connecting with UniFi controller: %s", err) return False - for unifi_config in hass.data[UNIFI_CONFIG]: - if ( - self.host == unifi_config[CONF_HOST] - and self.site_name == unifi_config[CONF_SITE_ID] - ): - self.unifi_config = unifi_config - break + self.import_configuration() - options = dict(self.config_entry.options) - - if CONF_BLOCK_CLIENT in self.unifi_config: - options[CONF_BLOCK_CLIENT] = self.unifi_config[CONF_BLOCK_CLIENT] - - if CONF_TRACK_CLIENTS in self.unifi_config: - options[CONF_TRACK_CLIENTS] = self.unifi_config[CONF_TRACK_CLIENTS] - - if CONF_TRACK_DEVICES in self.unifi_config: - options[CONF_TRACK_DEVICES] = self.unifi_config[CONF_TRACK_DEVICES] - - if CONF_TRACK_WIRED_CLIENTS in self.unifi_config: - options[CONF_TRACK_WIRED_CLIENTS] = self.unifi_config[ - CONF_TRACK_WIRED_CLIENTS - ] - - if CONF_DETECTION_TIME in self.unifi_config: - options[CONF_DETECTION_TIME] = self.unifi_config[CONF_DETECTION_TIME] - - if CONF_SSID_FILTER in self.unifi_config: - options[CONF_SSID_FILTER] = self.unifi_config[CONF_SSID_FILTER] - - hass.config_entries.async_update_entry(self.config_entry, options=options) + self.config_entry.add_update_listener(self.async_options_updated) for platform in ["device_tracker", "switch"]: hass.async_create_task( @@ -232,6 +210,56 @@ class UniFiController: return True + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + controller_id = CONTROLLER_ID.format( + host=entry.data[CONF_CONTROLLER][CONF_HOST], + site=entry.data[CONF_CONTROLLER][CONF_SITE_ID], + ) + controller = hass.data[DOMAIN][controller_id] + + async_dispatcher_send(hass, controller.signal_options_update) + + def import_configuration(self): + """Import configuration to config entry options.""" + unifi_config = {} + for config in self.hass.data[UNIFI_CONFIG]: + if ( + self.host == config[CONF_HOST] + and self.site_name == config[CONF_SITE_ID] + ): + unifi_config = config + break + + old_options = dict(self.config_entry.options) + new_options = {} + + for config, option in ( + (CONF_BLOCK_CLIENT, CONF_BLOCK_CLIENT), + (CONF_DONT_TRACK_CLIENTS, CONF_TRACK_CLIENTS), + (CONF_DONT_TRACK_WIRED_CLIENTS, CONF_TRACK_WIRED_CLIENTS), + (CONF_DONT_TRACK_DEVICES, CONF_TRACK_DEVICES), + (CONF_DETECTION_TIME, CONF_DETECTION_TIME), + (CONF_SSID_FILTER, CONF_SSID_FILTER), + ): + if config in unifi_config: + if config == option and unifi_config[ + config + ] != self.config_entry.options.get(option): + new_options[option] = unifi_config[config] + elif config != option and ( + option not in self.config_entry.options + or unifi_config[config] == self.config_entry.options.get(option) + ): + new_options[option] = not unifi_config[config] + + if new_options: + options = {**old_options, **new_options} + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + async def async_reset(self): """Reset this controller to default state. diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 4845e9222ce..b3982e7327d 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -1,36 +1,20 @@ -"""Support for Unifi WAP controllers.""" -from datetime import timedelta - +"""Track devices using UniFi controllers.""" import logging import voluptuous as vol -from homeassistant import config_entries -from homeassistant.components import unifi +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.core import callback -from homeassistant.const import ( - CONF_HOST, - CONF_USERNAME, - CONF_PASSWORD, - CONF_PORT, - CONF_VERIFY_SSL, -) from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY -import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from .const import ( - ATTR_MANUFACTURER, - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID, - DOMAIN as UNIFI_DOMAIN, -) +from .const import ATTR_MANUFACTURER LOGGER = logging.getLogger(__name__) @@ -55,61 +39,17 @@ DEVICE_ATTRIBUTES = [ "vlan", ] -CONF_DT_SITE_ID = "site_id" - -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 8443 -DEFAULT_VERIFY_SSL = True -DEFAULT_DETECTION_TIME = timedelta(seconds=300) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_DT_SITE_ID, default="default"): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): vol.Any( - cv.boolean, cv.isfile - ), - }, - extra=vol.ALLOW_EXTRA, -) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) async def async_setup_scanner(hass, config, sync_see, discovery_info): """Set up the Unifi integration.""" - config[CONF_SITE_ID] = config.pop(CONF_DT_SITE_ID) # Current from legacy - - exist = False - - for entry in hass.config_entries.async_entries(UNIFI_DOMAIN): - if ( - config[CONF_HOST] == entry.data[CONF_CONTROLLER][CONF_HOST] - and config[CONF_SITE_ID] == entry.data[CONF_CONTROLLER][CONF_SITE_ID] - ): - exist = True - break - - if not exist: - hass.async_create_task( - hass.config_entries.flow.async_init( - UNIFI_DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=config, - ) - ) - return True async def async_setup_entry(hass, config_entry, async_add_entities): """Set up device tracker for UniFi component.""" - controller_id = CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) - controller = hass.data[unifi.DOMAIN][controller_id] + controller = get_controller_from_config_entry(hass, config_entry) tracked = {} registry = await entity_registry.async_get_registry(hass) @@ -136,7 +76,24 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" update_items(controller, async_add_entities, tracked) - async_dispatcher_connect(hass, controller.event_update, update_controller) + async_dispatcher_connect(hass, controller.signal_update, update_controller) + + @callback + def update_disable_on_entities(): + """Update the values of the controller.""" + for entity in tracked.values(): + + disabled_by = None + if not entity.entity_registry_enabled_default and entity.enabled: + disabled_by = DISABLED_CONFIG_ENTRY + + registry.async_update_entity( + entity.registry_entry.entity_id, disabled_by=disabled_by + ) + + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) update_controller() @@ -146,65 +103,20 @@ def update_items(controller, async_add_entities, tracked): """Update tracked device state from the controller.""" new_tracked = [] - if controller.option_track_clients: + for items, tracker_class in ( + (controller.api.clients, UniFiClientTracker), + (controller.api.devices, UniFiDeviceTracker), + ): - for client_id in controller.api.clients: + for item_id in items: - if client_id in tracked: - if not tracked[client_id].enabled: - continue - LOGGER.debug( - "Updating UniFi tracked client %s (%s)", - tracked[client_id].entity_id, - tracked[client_id].client.mac, - ) - tracked[client_id].async_schedule_update_ha_state() + if item_id in tracked: + if tracked[item_id].enabled: + tracked[item_id].async_schedule_update_ha_state() continue - client = controller.api.clients[client_id] - - if ( - not client.is_wired - and controller.option_ssid_filter - and client.essid not in controller.option_ssid_filter - ): - continue - - if not controller.option_track_wired_clients and client.is_wired: - continue - - tracked[client_id] = UniFiClientTracker(client, controller) - new_tracked.append(tracked[client_id]) - LOGGER.debug( - "New UniFi client tracker %s (%s)", - client.name or client.hostname, - client.mac, - ) - - if controller.option_track_devices: - - for device_id in controller.api.devices: - - if device_id in tracked: - if not tracked[device_id].enabled: - continue - LOGGER.debug( - "Updating UniFi tracked device %s (%s)", - tracked[device_id].entity_id, - tracked[device_id].device.mac, - ) - tracked[device_id].async_schedule_update_ha_state() - continue - - device = controller.api.devices[device_id] - - tracked[device_id] = UniFiDeviceTracker(device, controller) - new_tracked.append(tracked[device_id]) - LOGGER.debug( - "New UniFi device tracker %s (%s)", - device.name or device.model, - device.mac, - ) + tracked[item_id] = tracker_class(items[item_id], controller) + new_tracked.append(tracked[item_id]) if new_tracked: async_add_entities(new_tracked) @@ -218,8 +130,33 @@ class UniFiClientTracker(ScannerEntity): self.client = client self.controller = controller + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if not self.controller.option_track_clients: + return False + + if ( + not self.client.is_wired + and self.controller.option_ssid_filter + and self.client.essid not in self.controller.option_ssid_filter + ): + return False + + if not self.controller.option_track_wired_clients and self.client.is_wired: + return False + + return True + + async def async_added_to_hass(self): + """Client entity created.""" + LOGGER.debug("New UniFi client tracker %s (%s)", self.name, self.client.mac) + async def async_update(self): """Synchronize state with controller.""" + LOGGER.debug( + "Updating UniFi tracked client %s (%s)", self.entity_id, self.client.mac + ) await self.controller.request_update() @property @@ -245,7 +182,7 @@ class UniFiClientTracker(ScannerEntity): @property def unique_id(self) -> str: """Return a unique identifier for this client.""" - return "{}-{}".format(self.client.mac, self.controller.site) + return f"{self.client.mac}-{self.controller.site}" @property def available(self) -> bool: @@ -277,8 +214,23 @@ class UniFiDeviceTracker(ScannerEntity): self.device = device self.controller = controller + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if not self.controller.option_track_devices: + return False + + return True + + async def async_added_to_hass(self): + """Subscribe to device events.""" + LOGGER.debug("New UniFi device tracker %s (%s)", self.name, self.device.mac) + async def async_update(self): """Synchronize state with controller.""" + LOGGER.debug( + "Updating UniFi tracked device %s (%s)", self.entity_id, self.device.mac + ) await self.controller.request_update() @property diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index b7bb9b730ad..4f757102d53 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -1,17 +1,14 @@ """Support for devices connected to UniFi POE.""" import logging -from homeassistant.components import unifi +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from .const import CONF_CONTROLLER, CONF_SITE_ID, CONTROLLER_ID - LOGGER = logging.getLogger(__name__) @@ -25,11 +22,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): Switches are controlling network switch ports with Poe. """ - controller_id = CONTROLLER_ID.format( - host=config_entry.data[CONF_CONTROLLER][CONF_HOST], - site=config_entry.data[CONF_CONTROLLER][CONF_SITE_ID], - ) - controller = hass.data[unifi.DOMAIN][controller_id] + controller = get_controller_from_config_entry(hass, config_entry) if controller.site_role != "admin": return @@ -61,7 +54,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" update_items(controller, async_add_entities, switches, switches_off) - async_dispatcher_connect(hass, controller.event_update, update_controller) + async_dispatcher_connect(hass, controller.signal_update, update_controller) update_controller() switches_off.clear() @@ -76,7 +69,7 @@ def update_items(controller, async_add_entities, switches, switches_off): # block client for client_id in controller.option_block_clients: - block_client_id = "block-{}".format(client_id) + block_client_id = f"block-{client_id}" if block_client_id in switches: LOGGER.debug( @@ -98,7 +91,7 @@ def update_items(controller, async_add_entities, switches, switches_off): # control poe for client_id in controller.api.clients: - poe_client_id = "poe-{}".format(client_id) + poe_client_id = f"poe-{client_id}" if poe_client_id in switches: LOGGER.debug( @@ -201,7 +194,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): @property def unique_id(self): """Return a unique identifier for this switch.""" - return "poe-{}".format(self.client.mac) + return f"poe-{self.client.mac}" @property def is_on(self): @@ -220,7 +213,7 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): or self.client.sw_mac and ( self.controller.available - or self.client.sw_mac in self.controller.api.devices + and self.client.sw_mac in self.controller.api.devices ) ) @@ -262,7 +255,7 @@ class UniFiBlockClientSwitch(UniFiClient, SwitchDevice): @property def unique_id(self): """Return a unique identifier for this switch.""" - return "block-{}".format(self.client.mac) + return f"block-{self.client.mac}" @property def is_on(self): diff --git a/homeassistant/components/upc_connect/device_tracker.py b/homeassistant/components/upc_connect/device_tracker.py index 3355c33ab2a..fc9225c6ef4 100644 --- a/homeassistant/components/upc_connect/device_tracker.py +++ b/homeassistant/components/upc_connect/device_tracker.py @@ -1,10 +1,9 @@ """Support for UPC ConnectBox router.""" -import asyncio import logging +from typing import List, Optional -import aiohttp -from aiohttp.hdrs import REFERER, USER_AGENT -import async_timeout +from connect_box import ConnectBox +from connect_box.exceptions import ConnectBoxError, ConnectBoxLoginError import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -12,119 +11,66 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, HTTP_HEADER_X_REQUESTED_WITH -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import CONF_HOST, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -CMD_DEVICES = 123 - DEFAULT_IP = "192.168.0.1" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string} + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string, + } ) async def async_get_scanner(hass, config): """Return the UPC device scanner.""" - scanner = UPCDeviceScanner(hass, config[DOMAIN]) - success_init = await scanner.async_initialize_token() + conf = config[DOMAIN] + session = hass.helpers.aiohttp_client.async_get_clientsession() + connect_box = ConnectBox(session, conf[CONF_PASSWORD], host=conf[CONF_HOST]) - return scanner if success_init else None + # Check login data + try: + await connect_box.async_initialize_token() + except ConnectBoxLoginError: + _LOGGER.error("ConnectBox login data error!") + return None + except ConnectBoxError: + pass + + async def _shutdown(event): + """Shutdown event.""" + await connect_box.async_close_session() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown) + + return UPCDeviceScanner(connect_box) class UPCDeviceScanner(DeviceScanner): """This class queries a router running UPC ConnectBox firmware.""" - def __init__(self, hass, config): + def __init__(self, connect_box: ConnectBox): """Initialize the scanner.""" - self.hass = hass - self.host = config[CONF_HOST] + self.connect_box: ConnectBox = connect_box - self.data = {} - self.token = None - - self.headers = { - HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest", - REFERER: "http://{}/index.html".format(self.host), - USER_AGENT: ( - "Mozilla/5.0 (Windows NT 10.0; WOW64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/47.0.2526.106 Safari/537.36" - ), - } - - self.websession = async_get_clientsession(hass) - - async def async_scan_devices(self): + async def async_scan_devices(self) -> List[str]: """Scan for new devices and return a list with found device IDs.""" - import defusedxml.ElementTree as ET - - if self.token is None: - token_initialized = await self.async_initialize_token() - if not token_initialized: - _LOGGER.error("Not connected to %s", self.host) - return [] - - raw = await self._async_ws_function(CMD_DEVICES) - try: - xml_root = ET.fromstring(raw) - return [mac.text for mac in xml_root.iter("MACAddr")] - except (ET.ParseError, TypeError): - _LOGGER.warning("Can't read device from %s", self.host) - self.token = None + await self.connect_box.async_get_devices() + except ConnectBoxError: return [] - async def async_get_device_name(self, device): + return [device.mac for device in self.connect_box.devices] + + async def async_get_device_name(self, device: str) -> Optional[str]: """Get the device name (the name of the wireless device not used).""" + for connected_device in self.connect_box.devices: + if connected_device != device: + continue + return connected_device.hostname + return None - - async def async_initialize_token(self): - """Get first token.""" - try: - # get first token - with async_timeout.timeout(10): - response = await self.websession.get( - "http://{}/common_page/login.html".format(self.host), - headers=self.headers, - ) - - await response.text() - - self.token = response.cookies["sessionToken"].value - - return True - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Can not load login page from %s", self.host) - return False - - async def _async_ws_function(self, function): - """Execute a command on UPC firmware webservice.""" - try: - with async_timeout.timeout(10): - # The 'token' parameter has to be first, and 'fun' second - # or the UPC firmware will return an error - response = await self.websession.post( - "http://{}/xml/getter.xml".format(self.host), - data="token={}&fun={}".format(self.token, function), - headers=self.headers, - allow_redirects=False, - ) - - # Error? - if response.status != 200: - _LOGGER.warning("Receive http code %d", response.status) - self.token = None - return - - # Load data, store token for next request - self.token = response.cookies["sessionToken"].value - return await response.text() - - except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Error on %s", function) - self.token = None diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 36a06ac3204..efa38286e7e 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,9 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/components/upc_connect", - "requirements": [ - "defusedxml==0.6.0" - ], + "requirements": ["connect-box==0.2.4"], "dependencies": [], - "codeowners": [] + "codeowners": ["@pvizeli"] } diff --git a/homeassistant/components/upnp/.translations/it.json b/homeassistant/components/upnp/.translations/it.json index 798f6578093..e822895a6cf 100644 --- a/homeassistant/components/upnp/.translations/it.json +++ b/homeassistant/components/upnp/.translations/it.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Abilita almeno i sensori o la mappatura delle porte", "single_instance_allowed": "\u00c8 necessaria una sola configurazione di UPnP/IGD." }, + "error": { + "one": "Vuoto", + "other": "Vuoto" + }, "step": { "confirm": { "description": "Vuoi configurare UPnP/IGD?", diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index 9fa37e1236d..d846a5e38ce 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "UPnP/IGD \uac00 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "incomplete_device": "\ubd88\uc644\uc804\ud55c UPnP \uc7a5\uce58 \ubb34\uc2dc\ud558\uae30", + "incomplete_device": "\ubd88\uc644\uc804\ud55c UPnP \uae30\uae30 \ubb34\uc2dc\ud558\uae30", "no_devices_discovered": "\ubc1c\uacac\ub41c UPnP/IGD \uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_devices_found": "UPnP/IGD \uc7a5\uce58\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "no_devices_found": "UPnP/IGD \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "no_sensors_or_port_mapping": "\ucd5c\uc18c\ud55c \uc13c\uc11c \ud639\uc740 \ud3ec\ud2b8 \ub9e4\ud551\uc744 \ud65c\uc131\ud654 \ud574\uc57c \ud569\ub2c8\ub2e4", "single_instance_allowed": "\ud558\ub098\uc758 UPnP/IGD \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 6120b6b3ca6..9aec23a687c 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/upnp", "requirements": [ - "async-upnp-client==0.14.10" + "async-upnp-client==0.14.11" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 33ffa4d478a..e5746e088f8 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -118,7 +118,7 @@ class RawUPnPIGDSensor(UpnpSensor): @property def unique_id(self) -> str: """Return an unique ID.""" - return "{}_{}".format(self._device.udn, self._type_name) + return f"{self._device.udn}_{self._type_name}" @property def state(self) -> str: @@ -172,12 +172,12 @@ class PerSecondUPnPIGDSensor(UpnpSensor): @property def unique_id(self) -> str: """Return an unique ID.""" - return "{}_{}/sec_{}".format(self._device.udn, self.unit, self._direction) + return f"{self._device.udn}_{self.unit}/sec_{self._direction}" @property def name(self) -> str: """Return the name of the sensor.""" - return "{} {}/sec {}".format(self._device.name, self.unit, self._direction) + return f"{self._device.name} {self.unit}/sec {self._direction}" @property def icon(self) -> str: @@ -187,7 +187,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor): @property def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return "{}/sec".format(self.unit) + return f"{self.unit}/sec" def _is_overflowed(self, new_value) -> bool: """Check if value has overflowed.""" diff --git a/homeassistant/components/usgs_earthquakes_feed/geo_location.py b/homeassistant/components/usgs_earthquakes_feed/geo_location.py index 7e5d6f5ebfe..7890243c1e0 100644 --- a/homeassistant/components/usgs_earthquakes_feed/geo_location.py +++ b/homeassistant/components/usgs_earthquakes_feed/geo_location.py @@ -243,6 +243,11 @@ class UsgsEarthquakesEvent(GeolocationEvent): self._type = feed_entry.type self._alert = feed_entry.alert + @property + def icon(self): + """Return the icon to use in the frontend.""" + return "mdi:pulse" + @property def source(self) -> str: """Return source value of this external event.""" diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index 00aa23c3d4d..0d1c116786a 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -6,5 +6,7 @@ "geojson_client==0.4" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@exxamalte" + ] } diff --git a/homeassistant/components/usps/camera.py b/homeassistant/components/usps/camera.py index 78af9c4feab..3141314b049 100644 --- a/homeassistant/components/usps/camera.py +++ b/homeassistant/components/usps/camera.py @@ -65,7 +65,7 @@ class USPSCamera(Camera): @property def name(self): """Return the name of this camera.""" - return "{} mail".format(self._name) + return f"{self._name} mail" @property def model(self): diff --git a/homeassistant/components/usps/sensor.py b/homeassistant/components/usps/sensor.py index a8aa6f6cc6f..7e26e6c9e5c 100644 --- a/homeassistant/components/usps/sensor.py +++ b/homeassistant/components/usps/sensor.py @@ -36,7 +36,7 @@ class USPSPackageSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} packages".format(self._name) + return f"{self._name} packages" @property def state(self): @@ -85,7 +85,7 @@ class USPSMailSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} mail".format(self._name) + return f"{self._name} mail" @property def state(self): diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index c09c43dc282..17eacc326d3 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -98,7 +98,7 @@ async def async_setup(hass, config): tariff_confs.append( { CONF_METER: meter, - CONF_NAME: "{} {}".format(meter, tariff), + CONF_NAME: f"{meter} {tariff}", CONF_TARIFF: tariff, } ) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 1eceaea2ae5..1ad4300b28b 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -107,7 +107,7 @@ class UtilityMeterSensor(RestoreEntity): if name: self._name = name else: - self._name = "{} meter".format(source_entity) + self._name = f"{source_entity} meter" self._unit_of_measurement = None self._period = meter_type self._period_offset = meter_offset diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index 7c09117d48f..5437f4b83a6 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -4,21 +4,21 @@ reset: description: Resets the counter of an utility meter. fields: entity_id: - description: Name(s) of the utility meter to reset + description: Name(s) of the utility meter to reset example: 'utility_meter.energy' next_tariff: description: Changes the tariff to the next one. fields: entity_id: - description: Name(s) of entities to reset + description: Name(s) of entities to reset example: 'utility_meter.energy' select_tariff: description: selects the current tariff of an utility meter. fields: entity_id: - description: Name of the entity to set the tariff for + description: Name of the entity to set the tariff for example: 'utility_meter.energy' tariff: description: Name of the tariff to switch to diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index 40ae0a83482..c107e4f8894 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -152,7 +152,7 @@ class ValloxStateProxy: raise OSError("Device state out of sync.") if metric_key not in vlxDevConstants.__dict__: - raise KeyError("Unknown metric key: {}".format(metric_key)) + raise KeyError(f"Unknown metric key: {metric_key}") return self._metric_cache[metric_key] diff --git a/homeassistant/components/vallox/sensor.py b/homeassistant/components/vallox/sensor.py index 705ccd3103d..f7be502cecb 100644 --- a/homeassistant/components/vallox/sensor.py +++ b/homeassistant/components/vallox/sensor.py @@ -28,14 +28,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sensors = [ ValloxProfileSensor( - name="{} Current Profile".format(name), + name=f"{name} Current Profile", state_proxy=state_proxy, device_class=None, unit_of_measurement=None, icon="mdi:gauge", ), ValloxFanSpeedSensor( - name="{} Fan Speed".format(name), + name=f"{name} Fan Speed", state_proxy=state_proxy, metric_key="A_CYC_FAN_SPEED", device_class=None, @@ -43,7 +43,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon="mdi:fan", ), ValloxSensor( - name="{} Extract Air".format(name), + name=f"{name} Extract Air", state_proxy=state_proxy, metric_key="A_CYC_TEMP_EXTRACT_AIR", device_class=DEVICE_CLASS_TEMPERATURE, @@ -51,7 +51,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxSensor( - name="{} Exhaust Air".format(name), + name=f"{name} Exhaust Air", state_proxy=state_proxy, metric_key="A_CYC_TEMP_EXHAUST_AIR", device_class=DEVICE_CLASS_TEMPERATURE, @@ -59,7 +59,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxSensor( - name="{} Outdoor Air".format(name), + name=f"{name} Outdoor Air", state_proxy=state_proxy, metric_key="A_CYC_TEMP_OUTDOOR_AIR", device_class=DEVICE_CLASS_TEMPERATURE, @@ -67,7 +67,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxSensor( - name="{} Supply Air".format(name), + name=f"{name} Supply Air", state_proxy=state_proxy, metric_key="A_CYC_TEMP_SUPPLY_AIR", device_class=DEVICE_CLASS_TEMPERATURE, @@ -75,7 +75,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxSensor( - name="{} Humidity".format(name), + name=f"{name} Humidity", state_proxy=state_proxy, metric_key="A_CYC_RH_VALUE", device_class=DEVICE_CLASS_HUMIDITY, @@ -83,7 +83,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= icon=None, ), ValloxFilterRemainingSensor( - name="{} Remaining Time For Filter".format(name), + name=f"{name} Remaining Time For Filter", state_proxy=state_proxy, metric_key="A_CYC_REMAINING_TIME_FOR_FILTER", device_class=DEVICE_CLASS_TIMESTAMP, diff --git a/homeassistant/components/velbus/.translations/ca.json b/homeassistant/components/velbus/.translations/ca.json new file mode 100644 index 00000000000..e38977a483f --- /dev/null +++ b/homeassistant/components/velbus/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "El port ja est\u00e0 configurat" + }, + "error": { + "connection_failed": "Ha fallat la connexi\u00f3 Velbus", + "port_exists": "El port ja est\u00e0 configurat" + }, + "step": { + "user": { + "data": { + "name": "Nom de la connexi\u00f3 Velbus", + "port": "Cadena de connexi\u00f3" + }, + "title": "Tipus de connexi\u00f3 Velbus" + } + }, + "title": "Interf\u00edcie Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/da.json b/homeassistant/components/velbus/.translations/da.json new file mode 100644 index 00000000000..5e636c8bcd7 --- /dev/null +++ b/homeassistant/components/velbus/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Denne port er allerede konfigureret" + }, + "error": { + "connection_failed": "Velbus forbindelsen mislykkedes", + "port_exists": "Denne port er allerede konfigureret" + }, + "step": { + "user": { + "data": { + "name": "Navnet p\u00e5 denne velbus forbindelse", + "port": "Forbindelsesstreng" + }, + "title": "Definer velbus forbindelsestypen" + } + }, + "title": "Velbus-interface" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/de.json b/homeassistant/components/velbus/.translations/de.json new file mode 100644 index 00000000000..72af917e12e --- /dev/null +++ b/homeassistant/components/velbus/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Dieser Port ist bereits konfiguriert" + }, + "error": { + "connection_failed": "Die Velbus-Verbindung ist fehlgeschlagen", + "port_exists": "Dieser Port ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "name": "Der Name f\u00fcr diese Velbus-Verbindung", + "port": "Verbindungs details" + }, + "title": "Definieren des Velbus-Verbindungstyps" + } + }, + "title": "Velbus-Schnittstelle" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/es.json b/homeassistant/components/velbus/.translations/es.json new file mode 100644 index 00000000000..e60ef7b4c67 --- /dev/null +++ b/homeassistant/components/velbus/.translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "port_exists": "Este puerto ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "port": "Cadena de conexi\u00f3n" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/hu.json b/homeassistant/components/velbus/.translations/hu.json new file mode 100644 index 00000000000..c836b414746 --- /dev/null +++ b/homeassistant/components/velbus/.translations/hu.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "port_exists": "Ez a port m\u00e1r konfigur\u00e1lva van" + }, + "error": { + "port_exists": "Ez a port m\u00e1r konfigur\u00e1lva van" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/it.json b/homeassistant/components/velbus/.translations/it.json new file mode 100644 index 00000000000..e4f1fbf9c6b --- /dev/null +++ b/homeassistant/components/velbus/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Questa porta \u00e8 gi\u00e0 configurata" + }, + "error": { + "connection_failed": "La connessione Velbus non \u00e8 riuscita", + "port_exists": "Questa porta \u00e8 gi\u00e0 configurata" + }, + "step": { + "user": { + "data": { + "name": "Il nome per questa connessione Velbus", + "port": "Stringa di connessione" + }, + "title": "Definire il tipo di connessione Velbus" + } + }, + "title": "Interfaccia Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/ko.json b/homeassistant/components/velbus/.translations/ko.json new file mode 100644 index 00000000000..6e218afc97c --- /dev/null +++ b/homeassistant/components/velbus/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "connection_failed": "Velbus \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "port_exists": "\ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "name": "Velbus \uc5f0\uacb0 \uc774\ub984", + "port": "\uc5f0\uacb0 \ubb38\uc790\uc5f4" + }, + "title": "Velbus \uc5f0\uacb0 \uc720\ud615 \uc815\uc758" + } + }, + "title": "Velbus \uc778\ud130\ud398\uc774\uc2a4" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/nl.json b/homeassistant/components/velbus/.translations/nl.json new file mode 100644 index 00000000000..b2908e8d221 --- /dev/null +++ b/homeassistant/components/velbus/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Deze poort is al geconfigureerd" + }, + "error": { + "connection_failed": "De velbus verbinding is mislukt.", + "port_exists": "Deze poort is al geconfigureerd" + }, + "step": { + "user": { + "data": { + "name": "De naam voor deze velbus-verbinding", + "port": "Verbindingsreeks" + }, + "title": "Definieer de velbus-verbindingstype" + } + }, + "title": "Velbus interface" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/no.json b/homeassistant/components/velbus/.translations/no.json new file mode 100644 index 00000000000..c6b16170877 --- /dev/null +++ b/homeassistant/components/velbus/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Denne porten er allerede konfigurert" + }, + "error": { + "connection_failed": "Velbus-tilkoblingen mislyktes", + "port_exists": "Denne porten er allerede konfigurert" + }, + "step": { + "user": { + "data": { + "name": "Navnet p\u00e5 denne velbus tilkoblingen", + "port": "Tilkoblingsstreng" + }, + "title": "Definer tilkoblingstype for velbus" + } + }, + "title": "Velbus-grensesnitt" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/pl.json b/homeassistant/components/velbus/.translations/pl.json new file mode 100644 index 00000000000..72e18b0e2c8 --- /dev/null +++ b/homeassistant/components/velbus/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Ten port jest ju\u017c skonfigurowany" + }, + "error": { + "connection_failed": "Po\u0142\u0105czenie Velbus nie powiod\u0142o si\u0119", + "port_exists": "Ten port jest ju\u017c skonfigurowany" + }, + "step": { + "user": { + "data": { + "name": "Nazwa tego po\u0142\u0105czenia Velbus", + "port": "Parametry po\u0142\u0105czenia" + }, + "title": "Zdefiniuj typ po\u0142\u0105czenia Velbus" + } + }, + "title": "Interfejs Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/ru.json b/homeassistant/components/velbus/.translations/ru.json new file mode 100644 index 00000000000..3434c584221 --- /dev/null +++ b/homeassistant/components/velbus/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\u042d\u0442\u043e\u0442 \u043f\u043e\u0440\u0442 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "error": { + "connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435 \u0441 Velbus.", + "port_exists": "\u042d\u0442\u043e\u0442 \u043f\u043e\u0440\u0442 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Velbus", + "port": "\u0421\u0442\u0440\u043e\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "title": "Velbus" + } + }, + "title": "Velbus" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/sl.json b/homeassistant/components/velbus/.translations/sl.json new file mode 100644 index 00000000000..2fa1ccadcea --- /dev/null +++ b/homeassistant/components/velbus/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "Ta vrata so \u017ee nastavljena" + }, + "error": { + "connection_failed": "Povezava z velbusom ni uspela", + "port_exists": "Ta vrata so \u017ee nastavljena" + }, + "step": { + "user": { + "data": { + "name": "Ime za to velbus povezavo", + "port": "Povezovalni niz" + }, + "title": "Dolo\u010dite vrsto povezave z velbusom" + } + }, + "title": "Velbus vmesnik" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/.translations/zh-Hant.json b/homeassistant/components/velbus/.translations/zh-Hant.json new file mode 100644 index 00000000000..33f9191e8a2 --- /dev/null +++ b/homeassistant/components/velbus/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "port_exists": "\u6b64\u901a\u8a0a\u57e0\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_failed": "Velbus \u9023\u7dda\u5931\u6557", + "port_exists": "\u6b64\u901a\u8a0a\u57e0\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "step": { + "user": { + "data": { + "name": "Velbus \u9023\u7dda\u540d\u7a31", + "port": "\u9023\u7dda\u5b57\u4e32" + }, + "title": "\u5b9a\u7fa9 Velbus \u9023\u7dda\u985e\u578b" + } + }, + "title": "Velbus \u4ecb\u9762" + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 76018dcf548..9946f06446f 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -119,7 +119,7 @@ class VelbusEntity(Entity): serial = self._module.get_module_address() else: serial = self._module.serial - return "{}-{}".format(serial, self._channel) + return f"{serial}-{self._channel}" @property def name(self): diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index a67f8429db9..e9cbe14ce25 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -18,8 +18,7 @@ def velbus_entries(hass: HomeAssistant): ) -@config_entries.HANDLERS.register(DOMAIN) -class VelbusConfigFlow(config_entries.ConfigFlow): +class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index e75ee2387ee..7e1ae1ecd60 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -265,9 +265,11 @@ class VenstarThermostat(ClimateDevice): elif operation_mode == self._client.MODE_AUTO: success = self._client.set_setpoints(temp_low, temp_high) else: + success = False _LOGGER.error( "The thermostat is currently not in a mode " - "that supports target temperature" + "that supports target temperature: %s", + operation_mode, ) if not success: diff --git a/homeassistant/components/vesync/.translations/ca.json b/homeassistant/components/vesync/.translations/ca.json new file mode 100644 index 00000000000..0c253fd4812 --- /dev/null +++ b/homeassistant/components/vesync/.translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Nom\u00e9s es permet una \u00fanica inst\u00e0ncia de VeSync" + }, + "error": { + "invalid_login": "Nom d'usuari o contrasenya incorrectes" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix el nom d\u2019usuari i contrasenya" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/da.json b/homeassistant/components/vesync/.translations/da.json new file mode 100644 index 00000000000..43e56328f99 --- /dev/null +++ b/homeassistant/components/vesync/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Kun en Vesync-forekomst er tilladt" + }, + "error": { + "invalid_login": "Ugyldigt brugernavn eller adgangskode" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email adresse" + }, + "title": "Indtast brugernavn og adgangskode" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/de.json b/homeassistant/components/vesync/.translations/de.json new file mode 100644 index 00000000000..44b3ea86c55 --- /dev/null +++ b/homeassistant/components/vesync/.translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Nur eine Vesync-Instanz ist zul\u00e4ssig" + }, + "error": { + "invalid_login": "Ung\u00fcltiger Benutzername oder Kennwort" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail-Adresse" + }, + "title": "Benutzername und Passwort eingeben" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/es.json b/homeassistant/components/vesync/.translations/es.json new file mode 100644 index 00000000000..99611c5f9bf --- /dev/null +++ b/homeassistant/components/vesync/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_login": "Nombre de usuario o contrase\u00f1a no v\u00e1lidos" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Introduzca el nombre de usuario y la contrase\u00f1a" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/fr.json b/homeassistant/components/vesync/.translations/fr.json new file mode 100644 index 00000000000..4928ea4f0be --- /dev/null +++ b/homeassistant/components/vesync/.translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Une seule instance de Vesync est autoris\u00e9e" + }, + "error": { + "invalid_login": "Nom d'utilisateur ou mot de passe invalide" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + }, + "title": "Entrez vos identifiants" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/hu.json b/homeassistant/components/vesync/.translations/hu.json new file mode 100644 index 00000000000..4735140216f --- /dev/null +++ b/homeassistant/components/vesync/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_login": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Email c\u00edm" + }, + "title": "\u00cdrja be a felhaszn\u00e1l\u00f3nevet \u00e9s a jelsz\u00f3t" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/it.json b/homeassistant/components/vesync/.translations/it.json new file mode 100644 index 00000000000..d3e53547559 --- /dev/null +++ b/homeassistant/components/vesync/.translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u00c8 consentita una sola istanza di Vesync" + }, + "error": { + "invalid_login": "Nome utente o password non validi" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Indirizzo E-mail" + }, + "title": "Immettere nome utente e password" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/ko.json b/homeassistant/components/vesync/.translations/ko.json new file mode 100644 index 00000000000..ca43b90acc9 --- /dev/null +++ b/homeassistant/components/vesync/.translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\ud558\ub098\uc758 Vesync \uc778\uc2a4\ud134\uc2a4\ub9cc \ud5c8\uc6a9\ub429\ub2c8\ub2e4" + }, + "error": { + "invalid_login": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/nl.json b/homeassistant/components/vesync/.translations/nl.json new file mode 100644 index 00000000000..d19d528c61a --- /dev/null +++ b/homeassistant/components/vesync/.translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Er is slechts \u00e9\u00e9n Vesync instantie toegestaan." + }, + "error": { + "invalid_login": "Ongeldige gebruikersnaam of wachtwoord" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "title": "Voer gebruikersnaam en wachtwoord in" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/no.json b/homeassistant/components/vesync/.translations/no.json new file mode 100644 index 00000000000..be5f27b7a0f --- /dev/null +++ b/homeassistant/components/vesync/.translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Bare en Vesync-forekomst er tillatt" + }, + "error": { + "invalid_login": "Ugyldig brukernavn eller passord" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-postadresse" + }, + "title": "Skriv inn brukernavn og passord" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/pl.json b/homeassistant/components/vesync/.translations/pl.json new file mode 100644 index 00000000000..d6584f11d29 --- /dev/null +++ b/homeassistant/components/vesync/.translations/pl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Dozwolona jest tylko jedna instancja Vesync" + }, + "error": { + "invalid_login": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o." + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/ru.json b/homeassistant/components/vesync/.translations/ru.json new file mode 100644 index 00000000000..38b86e9e29f --- /dev/null +++ b/homeassistant/components/vesync/.translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "VeSync" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/sl.json b/homeassistant/components/vesync/.translations/sl.json new file mode 100644 index 00000000000..636237dcfc1 --- /dev/null +++ b/homeassistant/components/vesync/.translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "Dovoljen je samo ena instanca Vesync" + }, + "error": { + "invalid_login": "Neveljavno uporabni\u0161ko ime ali geslo" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Vnesite uporabni\u0161ko Ime in Geslo" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/zh-Hant.json b/homeassistant/components/vesync/.translations/zh-Hant.json new file mode 100644 index 00000000000..05e4a1bbc79 --- /dev/null +++ b/homeassistant/components/vesync/.translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Vesync \u7269\u4ef6" + }, + "error": { + "invalid_login": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "title": "\u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc" + } + }, + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py new file mode 100644 index 00000000000..9fec04f2328 --- /dev/null +++ b/homeassistant/components/vicare/__init__.py @@ -0,0 +1,58 @@ +"""The ViCare integration.""" +import logging + +import voluptuous as vol +from PyViCare.PyViCareDevice import Device + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from homeassistant.helpers import discovery + +_LOGGER = logging.getLogger(__name__) + +VICARE_PLATFORMS = ["climate", "water_heater"] + +DOMAIN = "vicare" +VICARE_API = "api" +VICARE_NAME = "name" + +CONF_CIRCUIT = "circuit" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_CIRCUIT): int, + vol.Optional(CONF_NAME, default="ViCare"): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Create the ViCare component.""" + conf = config[DOMAIN] + params = {"token_file": "/tmp/vicare_token.save"} + if conf.get(CONF_CIRCUIT) is not None: + params["circuit"] = conf[CONF_CIRCUIT] + + try: + vicare_api = Device(conf[CONF_USERNAME], conf[CONF_PASSWORD], **params) + except AttributeError: + _LOGGER.error( + "Failed to create PyViCare API client. Please check your credentials." + ) + return False + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][VICARE_API] = vicare_api + hass.data[DOMAIN][VICARE_NAME] = conf[CONF_NAME] + + for platform in VICARE_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) + + return True diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py new file mode 100644 index 00000000000..7010f943707 --- /dev/null +++ b/homeassistant/components/vicare/climate.py @@ -0,0 +1,235 @@ +"""Viessmann ViCare climate device.""" +import logging + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + PRESET_ECO, + PRESET_COMFORT, + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_AUTO, +) +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE + +from . import DOMAIN as VICARE_DOMAIN +from . import VICARE_API +from . import VICARE_NAME + +_LOGGER = logging.getLogger(__name__) + +VICARE_MODE_DHW = "dhw" +VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_FORCEDREDUCED = "forcedReduced" +VICARE_MODE_FORCEDNORMAL = "forcedNormal" +VICARE_MODE_OFF = "standby" + +VICARE_PROGRAM_ACTIVE = "active" +VICARE_PROGRAM_COMFORT = "comfort" +VICARE_PROGRAM_ECO = "eco" +VICARE_PROGRAM_EXTERNAL = "external" +VICARE_PROGRAM_HOLIDAY = "holiday" +VICARE_PROGRAM_NORMAL = "normal" +VICARE_PROGRAM_REDUCED = "reduced" +VICARE_PROGRAM_STANDBY = "standby" + +VICARE_HOLD_MODE_AWAY = "away" +VICARE_HOLD_MODE_HOME = "home" +VICARE_HOLD_MODE_OFF = "off" + +VICARE_TEMP_HEATING_MIN = 3 +VICARE_TEMP_HEATING_MAX = 37 + +SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + +VICARE_TO_HA_HVAC_HEATING = { + VICARE_MODE_DHW: HVAC_MODE_OFF, + VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO, + VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF, + VICARE_MODE_FORCEDNORMAL: HVAC_MODE_HEAT, + VICARE_MODE_OFF: HVAC_MODE_OFF, +} + +HA_TO_VICARE_HVAC_HEATING = { + HVAC_MODE_HEAT: VICARE_MODE_FORCEDNORMAL, + HVAC_MODE_OFF: VICARE_MODE_FORCEDREDUCED, + HVAC_MODE_AUTO: VICARE_MODE_DHWANDHEATING, +} + +VICARE_TO_HA_PRESET_HEATING = { + VICARE_PROGRAM_COMFORT: PRESET_COMFORT, + VICARE_PROGRAM_ECO: PRESET_ECO, +} + +HA_TO_VICARE_PRESET_HEATING = { + PRESET_COMFORT: VICARE_PROGRAM_COMFORT, + PRESET_ECO: VICARE_PROGRAM_ECO, +} + +PYVICARE_ERROR = "error" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the ViCare climate devices.""" + if discovery_info is None: + return + vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + add_entities( + [ViCareClimate(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", vicare_api)] + ) + + +class ViCareClimate(ClimateDevice): + """Representation of the ViCare heating climate device.""" + + def __init__(self, name, api): + """Initialize the climate device.""" + self._name = name + self._state = None + self._api = api + self._attributes = {} + self._target_temperature = None + self._current_mode = None + self._current_temperature = None + self._current_program = None + + def update(self): + """Let HA know there has been an update from the ViCare API.""" + _room_temperature = self._api.getRoomTemperature() + _supply_temperature = self._api.getSupplyTemperature() + if _room_temperature is not None and _room_temperature != PYVICARE_ERROR: + self._current_temperature = _room_temperature + elif _supply_temperature != PYVICARE_ERROR: + self._current_temperature = _supply_temperature + else: + self._current_temperature = None + self._current_program = self._api.getActiveProgram() + + # The getCurrentDesiredTemperature call can yield 'error' (str) when the system is in standby + desired_temperature = self._api.getCurrentDesiredTemperature() + if desired_temperature == PYVICARE_ERROR: + desired_temperature = None + + self._target_temperature = desired_temperature + + self._current_mode = self._api.getActiveMode() + + # Update the device attributes + self._attributes = {} + self._attributes["room_temperature"] = _room_temperature + self._attributes["supply_temperature"] = _supply_temperature + self._attributes["outside_temperature"] = self._api.getOutsideTemperature() + self._attributes["active_vicare_program"] = self._current_program + self._attributes["active_vicare_mode"] = self._current_mode + self._attributes["heating_curve_slope"] = self._api.getHeatingCurveSlope() + self._attributes["heating_curve_shift"] = self._api.getHeatingCurveShift() + self._attributes[ + "month_since_last_service" + ] = self._api.getMonthSinceLastService() + self._attributes["date_last_service"] = self._api.getLastServiceDate() + self._attributes["error_history"] = self._api.getErrorHistory() + self._attributes["active_error"] = self._api.getActiveError() + self._attributes[ + "circulationpump_active" + ] = self._api.getCirculationPumpActive() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATING + + @property + def name(self): + """Return the name of the climate device.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + @property + def hvac_mode(self): + """Return current hvac mode.""" + return VICARE_TO_HA_HVAC_HEATING.get(self._current_mode) + + def set_hvac_mode(self, hvac_mode): + """Set a new hvac mode on the ViCare API.""" + vicare_mode = HA_TO_VICARE_HVAC_HEATING.get(hvac_mode) + if vicare_mode is None: + _LOGGER.error( + "Cannot set invalid vicare mode: %s / %s", hvac_mode, vicare_mode + ) + return + + _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) + self._api.setMode(vicare_mode) + + @property + def hvac_modes(self): + """Return the list of available hvac modes.""" + return list(HA_TO_VICARE_HVAC_HEATING) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return VICARE_TEMP_HEATING_MIN + + @property + def max_temp(self): + """Return the maximum temperature.""" + return VICARE_TEMP_HEATING_MAX + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + self._api.setProgramTemperature( + self._current_program, self._target_temperature + ) + + @property + def preset_mode(self): + """Return the current preset mode, e.g., home, away, temp.""" + return VICARE_TO_HA_PRESET_HEATING.get(self._current_program) + + @property + def preset_modes(self): + """Return the available preset mode.""" + return list(VICARE_TO_HA_PRESET_HEATING) + + def set_preset_mode(self, preset_mode): + """Set new preset mode and deactivate any existing programs.""" + vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) + if vicare_program is None: + _LOGGER.error( + "Cannot set invalid vicare program: %s / %s", + preset_mode, + vicare_program, + ) + return + + _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) + self._api.deactivateProgram(self._current_program) + self._api.activateProgram(vicare_program) + + @property + def device_state_attributes(self): + """Show Device Attributes.""" + return self._attributes diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json new file mode 100644 index 00000000000..e5f55b20dda --- /dev/null +++ b/homeassistant/components/vicare/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "vicare", + "name": "Viessmann ViCare", + "documentation": "https://www.home-assistant.io/components/vicare", + "dependencies": [], + "codeowners": ["@oischinger"], + "requirements": ["PyViCare==0.1.1"] +} + diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py new file mode 100644 index 00000000000..71c0f6c2aef --- /dev/null +++ b/homeassistant/components/vicare/water_heater.py @@ -0,0 +1,132 @@ +"""Viessmann ViCare water_heater device.""" +import logging + +from homeassistant.components.water_heater import ( + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, +) +from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE + +from . import DOMAIN as VICARE_DOMAIN +from . import VICARE_API +from . import VICARE_NAME + +_LOGGER = logging.getLogger(__name__) + +VICARE_MODE_DHW = "dhw" +VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_FORCEDREDUCED = "forcedReduced" +VICARE_MODE_FORCEDNORMAL = "forcedNormal" +VICARE_MODE_OFF = "standby" + +VICARE_TEMP_WATER_MIN = 10 +VICARE_TEMP_WATER_MAX = 60 + +OPERATION_MODE_ON = "on" +OPERATION_MODE_OFF = "off" + +SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE + +VICARE_TO_HA_HVAC_DHW = { + VICARE_MODE_DHW: OPERATION_MODE_ON, + VICARE_MODE_DHWANDHEATING: OPERATION_MODE_ON, + VICARE_MODE_FORCEDREDUCED: OPERATION_MODE_OFF, + VICARE_MODE_FORCEDNORMAL: OPERATION_MODE_ON, + VICARE_MODE_OFF: OPERATION_MODE_OFF, +} + +HA_TO_VICARE_HVAC_DHW = { + OPERATION_MODE_OFF: VICARE_MODE_OFF, + OPERATION_MODE_ON: VICARE_MODE_DHW, +} + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the ViCare water_heater devices.""" + if discovery_info is None: + return + vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] + add_entities( + [ViCareWater(f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Water", vicare_api)] + ) + + +class ViCareWater(WaterHeaterDevice): + """Representation of the ViCare domestic hot water device.""" + + def __init__(self, name, api): + """Initialize the DHW water_heater device.""" + self._name = name + self._state = None + self._api = api + self._target_temperature = None + self._current_temperature = None + self._current_mode = None + + def update(self): + """Let HA know there has been an update from the ViCare API.""" + current_temperature = self._api.getDomesticHotWaterStorageTemperature() + if current_temperature is not None and current_temperature != "error": + self._current_temperature = current_temperature + else: + self._current_temperature = None + + self._target_temperature = self._api.getDomesticHotWaterConfiguredTemperature() + + self._current_mode = self._api.getActiveMode() + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATER + + @property + def name(self): + """Return the name of the water_heater device.""" + return self._name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temperature + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._target_temperature + + def set_temperature(self, **kwargs): + """Set new target temperatures.""" + temp = kwargs.get(ATTR_TEMPERATURE) + if temp is not None: + self._api.setDomesticHotWaterTemperature(self._target_temperature) + + @property + def min_temp(self): + """Return the minimum temperature.""" + return VICARE_TEMP_WATER_MIN + + @property + def max_temp(self): + """Return the maximum temperature.""" + return VICARE_TEMP_WATER_MAX + + @property + def precision(self): + """Return the precision of the system.""" + return PRECISION_WHOLE + + @property + def current_operation(self): + """Return current operation ie. heat, cool, idle.""" + return VICARE_TO_HA_HVAC_DHW.get(self._current_mode) + + @property + def operation_list(self): + """Return the list of available operation modes.""" + return list(HA_TO_VICARE_HVAC_DHW) diff --git a/homeassistant/components/vivotek/__init__.py b/homeassistant/components/vivotek/__init__.py new file mode 100644 index 00000000000..b5220b12a9b --- /dev/null +++ b/homeassistant/components/vivotek/__init__.py @@ -0,0 +1 @@ +"""The Vivotek camera component.""" diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py new file mode 100644 index 00000000000..bf136731cb6 --- /dev/null +++ b/homeassistant/components/vivotek/camera.py @@ -0,0 +1,120 @@ +"""Support for Vivotek IP Cameras.""" + +import logging + +import voluptuous as vol +from libpyvivotek import VivotekCamera + +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_NAME, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_FRAMERATE = "framerate" + +DEFAULT_CAMERA_BRAND = "Vivotek" +DEFAULT_NAME = "Vivotek Camera" +DEFAULT_EVENT_0_KEY = "event_i0_enable" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_IP_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_FRAMERATE, default=2): cv.positive_int, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up a Vivotek IP Camera.""" + args = dict( + config=config, + cam=VivotekCamera( + host=config[CONF_IP_ADDRESS], + port=(443 if config[CONF_SSL] else 80), + verify_ssl=config[CONF_VERIFY_SSL], + usr=config[CONF_USERNAME], + pwd=config[CONF_PASSWORD], + ), + stream_source=( + "rtsp://%s:%s@%s:554/live.sdp", + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_IP_ADDRESS], + ), + ) + add_entities([VivotekCam(**args)]) + + +class VivotekCam(Camera): + """A Vivotek IP camera.""" + + def __init__(self, config, cam, stream_source): + """Initialize a Vivotek camera.""" + super().__init__() + + self._cam = cam + self._frame_interval = 1 / config[CONF_FRAMERATE] + self._motion_detection_enabled = False + self._name = config[CONF_NAME] + self._stream_source = stream_source + + @property + def supported_features(self): + """Return supported features for this camera.""" + return SUPPORT_STREAM + + @property + def frame_interval(self): + """Return the interval between frames of the mjpeg stream.""" + return self._frame_interval + + def camera_image(self): + """Return bytes of camera image.""" + return self._cam.snapshot() + + @property + def name(self): + """Return the name of this device.""" + return self._name + + async def stream_source(self): + """Return the source of the stream.""" + return self._stream_source + + @property + def motion_detection_enabled(self): + """Return the camera motion detection status.""" + return self._motion_detection_enabled + + def disable_motion_detection(self): + """Disable motion detection in camera.""" + response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 0) + self._motion_detection_enabled = int(response) == 1 + + def enable_motion_detection(self): + """Enable motion detection in camera.""" + response = self._cam.set_param(DEFAULT_EVENT_0_KEY, 1) + self._motion_detection_enabled = int(response) == 1 + + @property + def brand(self): + """Return the camera brand.""" + return DEFAULT_CAMERA_BRAND + + @property + def model(self): + """Return the camera model.""" + return self._cam.model_name diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json new file mode 100644 index 00000000000..8a6a37762d4 --- /dev/null +++ b/homeassistant/components/vivotek/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "vivotek", + "name": "Vivotek", + "documentation": "https://www.home-assistant.io/components/vivotek", + "requirements": [ + "libpyvivotek==0.2.1" + ], + "dependencies": [], + "codeowners": [] +} diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 96e1d883646..8bd1952a650 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -125,7 +125,7 @@ class Volumio(MediaPlayerDevice): async def send_volumio_msg(self, method, params=None): """Send message.""" - url = "http://{}:{}/api/v1/{}/".format(self.host, self.port, method) + url = f"http://{self.host}:{self.port}/api/v1/{method}/" _LOGGER.debug("URL: %s params: %s", url, params) @@ -202,7 +202,7 @@ class Volumio(MediaPlayerDevice): if str(url[0:2]).lower() == "ht": mediaurl = url else: - mediaurl = "http://{}:{}{}".format(self.host, self.port, url) + mediaurl = f"http://{self.host}:{self.port}{url}" return mediaurl @property diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index b007628dbd8..c41c72020c4 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -36,7 +36,7 @@ CONF_SERVICE_URL = "service_url" CONF_SCANDINAVIAN_MILES = "scandinavian_miles" CONF_MUTABLE = "mutable" -SIGNAL_STATE_UPDATED = "{}.updated".format(DOMAIN) +SIGNAL_STATE_UPDATED = f"{DOMAIN}.updated" COMPONENTS = { "sensor": "sensor", @@ -261,7 +261,7 @@ class VolvoEntity(Entity): @property def name(self): """Return full name of the entity.""" - return "{} {}".format(self._vehicle_name, self._entity_name) + return f"{self._vehicle_name} {self._entity_name}" @property def should_poll(self): @@ -278,5 +278,5 @@ class VolvoEntity(Entity): """Return device specific state attributes.""" return dict( self.instrument.attributes, - model="{}/{}".format(self.vehicle.vehicle_type, self.vehicle.model_year), + model=f"{self.vehicle.vehicle_type}/{self.vehicle.model_year}", ) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index a2b9e69e002..dbfe6de1a60 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -113,7 +113,7 @@ class WaqiSensor(Entity): def name(self): """Return the name of the sensor.""" if self.station_name: - return "WAQI {}".format(self.station_name) + return f"WAQI {self.station_name}" return "WAQI {}".format(self.url if self.url else self.uid) @property diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py index 54c11506b29..aef2cc8ccce 100644 --- a/homeassistant/components/watson_iot/__init__.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -126,7 +126,7 @@ def setup(hass, config): if key != "unit_of_measurement": # If the key is already in fields if key in out_event["fields"]: - key = "{}_".format(key) + key = f"{key}_" # For each value we try to cast it as float # But if we can not do it we store the value # as string diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 3fc44f90d42..340c0adbc97 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -175,7 +175,7 @@ class WazeTravelTime(Entity): return _get_location_from_attributes(state) # Check if device is inside a zone. - zone_state = self.hass.states.get("zone.{}".format(state.state)) + zone_state = self.hass.states.get(f"zone.{state.state}") if location.has_location(zone_state): _LOGGER.debug( "%s is in %s, getting zone location", entity_id, zone_state.entity_id diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 8f276279ee5..fd122f66ac2 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -125,11 +125,11 @@ class WeatherEntity(Entity): @property def state_attributes(self): """Return the state attributes.""" - data = { - ATTR_WEATHER_TEMPERATURE: show_temp( + data = {} + if self.temperature is not None: + data[ATTR_WEATHER_TEMPERATURE] = show_temp( self.hass, self.temperature, self.temperature_unit, self.precision ) - } humidity = self.humidity if humidity is not None: diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 38540bbd307..a12e55c771a 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -99,11 +99,15 @@ class WebhookView(HomeAssistantView): name = "api:webhook" requires_auth = False - async def post(self, request, webhook_id): + async def _handle(self, request, webhook_id): """Handle webhook call.""" hass = request.app["hass"] return await async_handle_webhook(hass, webhook_id, request) + head = _handle + post = _handle + put = _handle + @callback def websocket_list(hass, connection, msg): diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 0b5696709fd..1da70bc60ec 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -3,7 +3,7 @@ import asyncio from datetime import timedelta import logging from urllib.parse import urlparse -from typing import Dict # noqa: F401 pylint: disable=unused-import +from typing import Dict import voluptuous as vol @@ -36,7 +36,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.script import Script -_CONFIGURING = {} # type: Dict[str, str] +_CONFIGURING: Dict[str, str] = {} _LOGGER = logging.getLogger(__name__) CONF_SOURCES = "sources" diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 3cdc5afd4a0..9e479991d15 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass, entry): def setup_url_for_device(device): """Determine setup.xml url for given device.""" - return "http://{}:{}/setup.xml".format(device.host, device.port) + return f"http://{device.host}:{device.port}/setup.xml" def setup_url_for_address(host, port): """Determine setup.xml url for given host and port pair.""" @@ -118,7 +118,7 @@ async def async_setup_entry(hass, entry): if not port: return None - return "http://{}:{}/setup.xml".format(host, port) + return f"http://{host}:{port}/setup.xml" def discovery_dispatch(service, discovery_info): """Dispatcher for incoming WeMo discovery events.""" @@ -150,7 +150,7 @@ async def async_setup_entry(hass, entry): if not url: _LOGGER.error( "Unable to get description url for WeMo at: %s", - "{}:{}".format(host, port) if port else host, + f"{host}:{port}" if port else host, ) continue diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 710adfd734d..5af784359d8 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -52,7 +52,7 @@ ATTR_HUB_NAME = "hub_name" WINK_AUTH_CALLBACK_PATH = "/auth/wink/callback" WINK_AUTH_START = "/auth/wink" WINK_CONFIG_FILE = ".wink.conf" -USER_AGENT = "Manufacturer/Home-Assistant{} python/3 Wink/3".format(__version__) +USER_AGENT = f"Manufacturer/Home-Assistant{__version__} python/3 Wink/3" DEFAULT_CONFIG = {"client_id": "CLIENT_ID_HERE", "client_secret": "CLIENT_SECRET_HERE"} @@ -228,7 +228,7 @@ def _request_app_setup(hass, config): _configurator = hass.data[DOMAIN]["configuring"][DOMAIN] configurator.notify_errors(_configurator, error_msg) - start_url = "{}{}".format(hass.config.api.base_url, WINK_AUTH_CALLBACK_PATH) + start_url = f"{hass.config.api.base_url}{WINK_AUTH_CALLBACK_PATH}" description = """Please create a Wink developer app at https://developer.wink.com. @@ -268,9 +268,9 @@ def _request_oauth_completion(hass, config): """Call setup again.""" setup(hass, config) - start_url = "{}{}".format(hass.config.api.base_url, WINK_AUTH_START) + start_url = f"{hass.config.api.base_url}{WINK_AUTH_START}" - description = "Please authorize Wink by visiting {}".format(start_url) + description = f"Please authorize Wink by visiting {start_url}" hass.data[DOMAIN]["configuring"][DOMAIN] = configurator.request_config( DOMAIN, wink_configuration_callback, description=description diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index ad1800b4223..e82a767fde8 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -140,7 +140,7 @@ class WinkHub(WinkBinarySensorDevice): # The service call to set the Kidde code # takes a string of 1s and 0s so it makes # sense to display it to the user that way - _formatted_kidde_code = "{:b}".format(_kidde_code).zfill(8) + _formatted_kidde_code = f"{_kidde_code:b}".zfill(8) _attributes["kidde_radio_code"] = _formatted_kidde_code return _attributes diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 331b88894de..5e0da881076 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -74,14 +74,14 @@ class WirelessTagPlatform: def arm(self, switch): """Arm entity sensor monitoring.""" - func_name = "arm_{}".format(switch.sensor_type) + func_name = f"arm_{switch.sensor_type}" arm_func = getattr(self.api, func_name) if arm_func is not None: arm_func(switch.tag_id, switch.tag_manager_mac) def disarm(self, switch): """Disarm entity sensor monitoring.""" - func_name = "disarm_{}".format(switch.sensor_type) + func_name = f"disarm_{switch.sensor_type}" disarm_func = getattr(self.api, func_name) if disarm_func is not None: disarm_func(switch.tag_id, switch.tag_manager_mac) @@ -132,18 +132,18 @@ class WirelessTagPlatform: port = self.hass.config.api.port if port is not None: - self._local_base_url += ":{}".format(port) + self._local_base_url += f":{port}" return self._local_base_url @property def update_callback_url(self): """Return url for local push notifications(update event).""" - return "{}/api/events/wirelesstag_update_tags".format(self.local_base_url) + return f"{self.local_base_url}/api/events/wirelesstag_update_tags" @property def binary_event_callback_url(self): """Return url for local push notifications(binary event).""" - return "{}/api/events/wirelesstag_binary_event".format(self.local_base_url) + return f"{self.local_base_url}/api/events/wirelesstag_binary_event" def handle_update_tags_event(self, event): """Handle push event from wireless tag manager.""" @@ -254,7 +254,7 @@ class WirelessTagBaseSensor(Entity): # pylint: disable=no-self-use def decorate_value(self, value): """Decorate input value to be well presented for end user.""" - return "{:.1f}".format(value) + return f"{value:.1f}" @property def available(self): @@ -280,8 +280,8 @@ class WirelessTagBaseSensor(Entity): """Return the state attributes.""" return { ATTR_BATTERY_LEVEL: int(self._tag.battery_remaining * 100), - ATTR_VOLTAGE: "{:.2f}V".format(self._tag.battery_volts), - ATTR_TAG_SIGNAL_STRENGTH: "{}dBm".format(self._tag.signal_strength), + ATTR_VOLTAGE: f"{self._tag.battery_volts:.2f}V", + ATTR_TAG_SIGNAL_STRENGTH: f"{self._tag.signal_strength}dBm", ATTR_TAG_OUT_OF_RANGE: not self._tag.is_in_range, - ATTR_TAG_POWER_CONSUMPTION: "{:.2f}%".format(self._tag.power_consumption), + ATTR_TAG_POWER_CONSUMPTION: f"{self._tag.power_consumption:.2f}%", } diff --git a/homeassistant/components/wirelesstag/binary_sensor.py b/homeassistant/components/wirelesstag/binary_sensor.py index 72b68a8762f..4fcebe73478 100644 --- a/homeassistant/components/wirelesstag/binary_sensor.py +++ b/homeassistant/components/wirelesstag/binary_sensor.py @@ -95,7 +95,7 @@ class WirelessTagBinarySensor(WirelessTagBaseSensor, BinarySensorDevice): """Initialize a binary sensor for a Wireless Sensor Tags.""" super().__init__(api, tag) self._sensor_type = sensor_type - self._name = "{0} {1}".format(self._tag.name, self.event.human_readable_name) + self._name = f"{self._tag.name} {self.event.human_readable_name}" async def async_added_to_hass(self): """Register callbacks.""" diff --git a/homeassistant/components/wirelesstag/switch.py b/homeassistant/components/wirelesstag/switch.py index 37f97e3a1e6..1bc806d9e32 100644 --- a/homeassistant/components/wirelesstag/switch.py +++ b/homeassistant/components/wirelesstag/switch.py @@ -79,5 +79,5 @@ class WirelessTagSwitch(WirelessTagBaseSensor, SwitchDevice): @property def principal_value(self): """Provide actual value of switch.""" - attr_name = "is_{}_sensor_armed".format(self.sensor_type) + attr_name = f"is_{self.sensor_type}_sensor_armed" return getattr(self._tag, attr_name, False) diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json new file mode 100644 index 00000000000..a96f8cff523 --- /dev/null +++ b/homeassistant/components/withings/.translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." + }, + "step": { + "user": { + "data": { + "profile": "Perfil" + }, + "description": "Selecciona un perfil d'usuari amb el qual vols que Home Assistant s'uneixi amb un perfil de Withings. A la p\u00e0gina de Withings, assegura't de seleccionar el mateix usuari o, les dades no seran les correctes.", + "title": "Perfil d'usuari." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json new file mode 100644 index 00000000000..d2dddbbd204 --- /dev/null +++ b/homeassistant/components/withings/.translations/da.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Godkendt med Withings for den valgte profil." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "V\u00e6lg en brugerprofil, som du vil have Home Assistant til at tilknytte med en Withings-profil. P\u00e5 siden Withings skal du s\u00f8rge for at v\u00e6lge den samme bruger eller data vil ikke blive m\u00e6rket korrekt.", + "title": "Brugerprofil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json new file mode 100644 index 00000000000..15b6f4e3b01 --- /dev/null +++ b/homeassistant/components/withings/.translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "W\u00e4hlen Sie ein Benutzerprofil aus, dem Home Assistant ein Withings-Profil zuordnen soll. Stellen Sie sicher, dass Sie auf der Withings-Seite denselben Benutzer ausw\u00e4hlen, da sonst die Daten nicht korrekt gekennzeichnet werden.", + "title": "Benutzerprofil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json new file mode 100644 index 00000000000..2b906dd8003 --- /dev/null +++ b/homeassistant/components/withings/.translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Successfully authenticated with Withings for the selected profile." + }, + "step": { + "user": { + "data": { + "profile": "Profile" + }, + "description": "Select a user profile to which you want Home Assistant to map with a Withings profile. On the withings page, be sure to select the same user or data will not be labeled correctly.", + "title": "User Profile." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json new file mode 100644 index 00000000000..fac325a7097 --- /dev/null +++ b/homeassistant/components/withings/.translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Autenticado correctamente con Withings para el perfil seleccionado." + }, + "step": { + "user": { + "data": { + "profile": "Perfil" + }, + "description": "Seleccione un perfil de usuario para el cual desea que Home Assistant se conecte con el perfil de Withings. En la p\u00e1gina de Withings, aseg\u00farese de seleccionar el mismo usuario o los datos no se identificar\u00e1n correctamente.", + "title": "Perfil de usuario." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json new file mode 100644 index 00000000000..b66786cc9e0 --- /dev/null +++ b/homeassistant/components/withings/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "S\u00e9lectionnez l'utilisateur que vous souhaitez associer \u00e0 Withings. Sur la page withings, veillez \u00e0 s\u00e9lectionner le m\u00eame utilisateur, sinon les donn\u00e9es ne seront pas \u00e9tiquet\u00e9es correctement.", + "title": "Profil utilisateur" + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json new file mode 100644 index 00000000000..5bf342836ce --- /dev/null +++ b/homeassistant/components/withings/.translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Autenticazione completata con Withings per il profilo selezionato." + }, + "step": { + "user": { + "data": { + "profile": "Profilo" + }, + "description": "Seleziona un profilo utente a cui desideri associare Home Assistant con un profilo Withings. Nella pagina Withings, assicurati di selezionare lo stesso utente o i dati non saranno etichettati correttamente.", + "title": "Profilo utente." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json new file mode 100644 index 00000000000..3c2f00ba4ae --- /dev/null +++ b/homeassistant/components/withings/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "\uc120\ud0dd\ud55c \ud504\ub85c\ud544\ub85c Withings \uc5d0 \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "profile": "\ud504\ub85c\ud544" + }, + "description": "Home Assistant \uac00 Withings \ud504\ub85c\ud544\uacfc \ub9f5\ud551\ud560 \uc0ac\uc6a9\uc790 \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. Withings \ud398\uc774\uc9c0\uc5d0\uc11c \ub3d9\uc77c\ud55c \uc0ac\uc6a9\uc790\ub97c \uc120\ud0dd\ud574\uc57c\ud569\ub2c8\ub2e4. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74 \ub370\uc774\ud130\uc5d0 \uc62c\ubc14\ub978 \ub808\uc774\ube14\uc774 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json new file mode 100644 index 00000000000..1729879a154 --- /dev/null +++ b/homeassistant/components/withings/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." + }, + "step": { + "user": { + "data": { + "profile": "Profiel" + }, + "description": "Selecteer een gebruikersprofiel waaraan u Home Assistant wilt toewijzen met een Withings-profiel. Zorg ervoor dat u op de pagina Withings dezelfde gebruiker selecteert, anders worden de gegevens niet correct gelabeld.", + "title": "Gebruikersprofiel." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json new file mode 100644 index 00000000000..22d8884d66a --- /dev/null +++ b/homeassistant/components/withings/.translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Vellykket autentisering for Withings og den valgte profilen." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "Velg en brukerprofil som du vil at Home Assistant skal kartlegge med en Withings-profil. P\u00e5 Withings-siden m\u00e5 du passe p\u00e5 at du velger samme bruker ellers vil ikke dataen bli merket riktig.", + "title": "Brukerprofil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json new file mode 100644 index 00000000000..1643ecb1480 --- /dev/null +++ b/homeassistant/components/withings/.translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "Wybierz profil u\u017cytkownika Withings, na kt\u00f3ry chcesz po\u0142\u0105czy\u0107 z Home Assistant'em. Na stronie Withings wybierz ten sam profil u\u017cytkownika by dane by\u0142y poprawnie oznaczone.", + "title": "Profil u\u017cytkownika" + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json new file mode 100644 index 00000000000..d9d5e14208f --- /dev/null +++ b/homeassistant/components/withings/.translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "user": { + "data": { + "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 Withings \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u043e\u0433\u043e \u0436\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u0438\u043d\u0430\u0447\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.", + "title": "Withings" + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json new file mode 100644 index 00000000000..d0fcb6a5276 --- /dev/null +++ b/homeassistant/components/withings/.translations/sl.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "Uspe\u0161no overjen z Withings za izbrani profil." + }, + "step": { + "user": { + "data": { + "profile": "Profil" + }, + "description": "Izberite uporabni\u0161ki profil, za katerega \u017eelite, da se Home Assistant prika\u017ee s profilom Withings. Na Withings strani ne pozabite izbrati istega uporabnika sicer podatki ne bodo pravilno ozna\u010deni.", + "title": "Uporabni\u0161ki profil." + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json new file mode 100644 index 00000000000..30a77102d04 --- /dev/null +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "create_entry": { + "default": "\u5df2\u6210\u529f\u4f7f\u7528\u6240\u9078\u8a2d\u5b9a\u8a8d\u8b49 Withings \u88dd\u7f6e\u3002" + }, + "step": { + "user": { + "data": { + "profile": "\u500b\u4eba\u8a2d\u5b9a" + }, + "description": "\u9078\u64c7 Home Assistant \u6240\u8981\u5c0d\u61c9\u4f7f\u7528\u7684 Withings \u500b\u4eba\u8a2d\u5b9a\u3002\u65bc Withings \u9801\u9762\u3001\u78ba\u5b9a\u9078\u53d6\u76f8\u540c\u7684\u4f7f\u7528\u8005\uff0c\u5426\u5247\u8cc7\u6599\u5c07\u7121\u6cd5\u6b63\u78ba\u6a19\u793a\u3002", + "title": "\u500b\u4eba\u8a2d\u5b9a\u3002" + } + }, + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py new file mode 100644 index 00000000000..ecefa681b87 --- /dev/null +++ b/homeassistant/components/withings/__init__.py @@ -0,0 +1,99 @@ +""" +Support for the Withings API. + +For more details about this platform, please refer to the documentation at +""" +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT, SOURCE_USER +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers import config_validation as cv + +from . import config_flow, const +from .common import _LOGGER, get_data_manager, NotAuthenticatedError + +DOMAIN = const.DOMAIN + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(const.CLIENT_ID): vol.All(cv.string, vol.Length(min=1)), + vol.Required(const.CLIENT_SECRET): vol.All( + cv.string, vol.Length(min=1) + ), + vol.Optional(const.BASE_URL): cv.url, + vol.Required(const.PROFILES): vol.All( + cv.ensure_list, + vol.Unique(), + vol.Length(min=1), + [vol.All(cv.string, vol.Length(min=1))], + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType): + """Set up the Withings component.""" + conf = config.get(DOMAIN) + if not conf: + return True + + hass.data[DOMAIN] = {const.CONFIG: conf} + + base_url = conf.get(const.BASE_URL, hass.config.api.base_url).rstrip("/") + + hass.http.register_view(config_flow.WithingsAuthCallbackView) + + config_flow.register_flow_implementation( + hass, + conf[const.CLIENT_ID], + conf[const.CLIENT_SECRET], + base_url, + conf[const.PROFILES], + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data={} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Withings from a config entry.""" + data_manager = get_data_manager(hass, entry) + + _LOGGER.debug("Confirming we're authenticated") + try: + await data_manager.check_authenticated() + except NotAuthenticatedError: + # Trigger new config flow. + hass.async_create_task( + hass.config_entries.flow.async_init( + const.DOMAIN, + context={"source": SOURCE_USER, const.PROFILE: data_manager.profile}, + data={}, + ) + ) + return False + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload Withings config entry.""" + await hass.async_create_task( + hass.config_entries.async_forward_entry_unload(entry, "sensor") + ) + + return True diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py new file mode 100644 index 00000000000..f2be849cbc7 --- /dev/null +++ b/homeassistant/components/withings/common.py @@ -0,0 +1,308 @@ +"""Common code for Withings.""" +import datetime +import logging +import re +import time + +import nokia +from oauthlib.oauth2.rfc6749.errors import MissingTokenError +from requests_oauthlib import TokenUpdated + +from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import HomeAssistantError, PlatformNotReady +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt, slugify + +from . import const + +_LOGGER = logging.getLogger(const.LOG_NAMESPACE) +NOT_AUTHENTICATED_ERROR = re.compile( + ".*(Error Code (100|101|102|200|401)|Missing access token parameter).*", + re.IGNORECASE, +) + + +class NotAuthenticatedError(HomeAssistantError): + """Raise when not authenticated with the service.""" + + pass + + +class ServiceError(HomeAssistantError): + """Raise when the service has an error.""" + + pass + + +class ThrottleData: + """Throttle data.""" + + def __init__(self, interval: int, data): + """Constructor.""" + self._time = int(time.time()) + self._interval = interval + self._data = data + + @property + def time(self): + """Get time created.""" + return self._time + + @property + def interval(self): + """Get interval.""" + return self._interval + + @property + def data(self): + """Get data.""" + return self._data + + def is_expired(self): + """Is this data expired.""" + return int(time.time()) - self.time > self.interval + + +class WithingsDataManager: + """A class representing an Withings cloud service connection.""" + + service_available = None + + def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + """Constructor.""" + self._hass = hass + self._api = api + self._profile = profile + self._slug = slugify(profile) + + self._measures = None + self._sleep = None + self._sleep_summary = None + + self.sleep_summary_last_update_parameter = None + self.throttle_data = {} + + @property + def profile(self) -> str: + """Get the profile.""" + return self._profile + + @property + def slug(self) -> str: + """Get the slugified profile the data is for.""" + return self._slug + + @property + def api(self): + """Get the api object.""" + return self._api + + @property + def measures(self): + """Get the current measures data.""" + return self._measures + + @property + def sleep(self): + """Get the current sleep data.""" + return self._sleep + + @property + def sleep_summary(self): + """Get the current sleep summary data.""" + return self._sleep_summary + + @staticmethod + def get_throttle_interval(): + """Get the throttle interval.""" + return const.THROTTLE_INTERVAL + + def get_throttle_data(self, domain: str) -> ThrottleData: + """Get throttlel data.""" + return self.throttle_data.get(domain) + + def set_throttle_data(self, domain: str, throttle_data: ThrottleData): + """Set throttle data.""" + self.throttle_data[domain] = throttle_data + + @staticmethod + def print_service_unavailable(): + """Print the service is unavailable (once) to the log.""" + if WithingsDataManager.service_available is not False: + _LOGGER.error("Looks like the service is not available at the moment") + WithingsDataManager.service_available = False + return True + + @staticmethod + def print_service_available(): + """Print the service is available (once) to to the log.""" + if WithingsDataManager.service_available is not True: + _LOGGER.info("Looks like the service is available again") + WithingsDataManager.service_available = True + return True + + async def call(self, function, is_first_call=True, throttle_domain=None): + """Call an api method and handle the result.""" + throttle_data = self.get_throttle_data(throttle_domain) + + should_throttle = ( + throttle_domain and throttle_data and not throttle_data.is_expired() + ) + + try: + if should_throttle: + _LOGGER.debug("Throttling call for domain: %s", throttle_domain) + result = throttle_data.data + else: + _LOGGER.debug("Running call.") + result = await self._hass.async_add_executor_job(function) + + # Update throttle data. + self.set_throttle_data( + throttle_domain, ThrottleData(self.get_throttle_interval(), result) + ) + + WithingsDataManager.print_service_available() + return result + + except TokenUpdated: + WithingsDataManager.print_service_available() + if not is_first_call: + raise ServiceError( + "Stuck in a token update loop. This should never happen" + ) + + _LOGGER.info("Token updated, re-running call.") + return await self.call(function, False, throttle_domain) + + except MissingTokenError as ex: + raise NotAuthenticatedError(ex) + + except Exception as ex: # pylint: disable=broad-except + # Service error, probably not authenticated. + if NOT_AUTHENTICATED_ERROR.match(str(ex)): + raise NotAuthenticatedError(ex) + + # Probably a network error. + WithingsDataManager.print_service_unavailable() + raise PlatformNotReady(ex) + + async def check_authenticated(self): + """Check if the user is authenticated.""" + + def function(): + return self._api.request("user", "getdevice", version="v2") + + return await self.call(function) + + async def update_measures(self): + """Update the measures data.""" + + def function(): + return self._api.get_measures() + + self._measures = await self.call(function, throttle_domain="update_measures") + + return self._measures + + async def update_sleep(self): + """Update the sleep data.""" + end_date = int(time.time()) + start_date = end_date - (6 * 60 * 60) + + def function(): + return self._api.get_sleep(startdate=start_date, enddate=end_date) + + self._sleep = await self.call(function, throttle_domain="update_sleep") + + return self._sleep + + async def update_sleep_summary(self): + """Update the sleep summary data.""" + now = dt.utcnow() + yesterday = now - datetime.timedelta(days=1) + yesterday_noon = datetime.datetime( + yesterday.year, + yesterday.month, + yesterday.day, + 12, + 0, + 0, + 0, + datetime.timezone.utc, + ) + + _LOGGER.debug( + "Getting sleep summary data since: %s", + yesterday.strftime("%Y-%m-%d %H:%M:%S UTC"), + ) + + def function(): + return self._api.get_sleep_summary(lastupdate=yesterday_noon.timestamp()) + + self._sleep_summary = await self.call( + function, throttle_domain="update_sleep_summary" + ) + + return self._sleep_summary + + +def create_withings_data_manager( + hass: HomeAssistantType, entry: ConfigEntry +) -> WithingsDataManager: + """Set up the sensor config entry.""" + entry_creds = entry.data.get(const.CREDENTIALS) or {} + profile = entry.data[const.PROFILE] + credentials = nokia.NokiaCredentials( + entry_creds.get("access_token"), + entry_creds.get("token_expiry"), + entry_creds.get("token_type"), + entry_creds.get("refresh_token"), + entry_creds.get("user_id"), + entry_creds.get("client_id"), + entry_creds.get("consumer_secret"), + ) + + def credentials_saver(credentials_param): + _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) + + # Sanitizing the data as sometimes a NokiaCredentials object + # is passed through from the API. + cred_data = credentials_param + if not isinstance(credentials_param, dict): + cred_data = credentials_param.__dict__ + + entry.data[const.CREDENTIALS] = cred_data + hass.config_entries.async_update_entry(entry, data={**entry.data}) + + _LOGGER.debug("Creating nokia api instance") + api = nokia.NokiaApi( + credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) + ) + + _LOGGER.debug("Creating withings data manager for profile: %s", profile) + return WithingsDataManager(hass, profile, api) + + +def get_data_manager( + hass: HomeAssistantType, entry: ConfigEntry +) -> WithingsDataManager: + """Get a data manager for a config entry. + + If the data manager doesn't exist yet, it will be + created and cached for later use. + """ + profile = entry.data.get(const.PROFILE) + + if not hass.data.get(const.DOMAIN): + hass.data[const.DOMAIN] = {} + + if not hass.data[const.DOMAIN].get(const.DATA_MANAGER): + hass.data[const.DOMAIN][const.DATA_MANAGER] = {} + + if not hass.data[const.DOMAIN][const.DATA_MANAGER].get(profile): + hass.data[const.DOMAIN][const.DATA_MANAGER][ + profile + ] = create_withings_data_manager(hass, entry) + + return hass.data[const.DOMAIN][const.DATA_MANAGER][profile] diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py new file mode 100644 index 00000000000..f28a4f59d80 --- /dev/null +++ b/homeassistant/components/withings/config_flow.py @@ -0,0 +1,192 @@ +"""Config flow for Withings.""" +from collections import OrderedDict +import logging +from typing import Optional + +import aiohttp +import nokia +import voluptuous as vol + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.http import HomeAssistantView +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback + +from . import const + +DATA_FLOW_IMPL = "withings_flow_implementation" + +_LOGGER = logging.getLogger(__name__) + + +@callback +def register_flow_implementation(hass, client_id, client_secret, base_url, profiles): + """Register a flow implementation. + + hass: Home assistant object. + client_id: Client id. + client_secret: Client secret. + base_url: Base url of home assistant instance. + profiles: The profiles to work with. + """ + if DATA_FLOW_IMPL not in hass.data: + hass.data[DATA_FLOW_IMPL] = OrderedDict() + + hass.data[DATA_FLOW_IMPL] = { + const.CLIENT_ID: client_id, + const.CLIENT_SECRET: client_secret, + const.BASE_URL: base_url, + const.PROFILES: profiles, + } + + +@config_entries.HANDLERS.register(const.DOMAIN) +class WithingsFlowHandler(config_entries.ConfigFlow): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize flow.""" + self.flow_profile = None + self.data = None + + def async_profile_config_entry(self, profile: str) -> Optional[ConfigEntry]: + """Get a profile config entry.""" + entries = self.hass.config_entries.async_entries(const.DOMAIN) + for entry in entries: + if entry.data.get(const.PROFILE) == profile: + return entry + + return None + + def get_auth_client(self, profile: str): + """Get a new auth client.""" + flow = self.hass.data[DATA_FLOW_IMPL] + client_id = flow[const.CLIENT_ID] + client_secret = flow[const.CLIENT_SECRET] + base_url = flow[const.BASE_URL].rstrip("/") + + callback_uri = "{}/{}?flow_id={}&profile={}".format( + base_url.rstrip("/"), + const.AUTH_CALLBACK_PATH.lstrip("/"), + self.flow_id, + profile, + ) + + return nokia.NokiaAuth( + client_id, + client_secret, + callback_uri, + scope=",".join(["user.info", "user.metrics", "user.activity"]), + ) + + async def async_step_import(self, user_input=None): + """Create user step.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Create an entry for selecting a profile.""" + flow = self.hass.data.get(DATA_FLOW_IMPL) + + if not flow: + return self.async_abort(reason="no_flows") + + if user_input: + return await self.async_step_auth(user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(const.PROFILE): vol.In(flow.get(const.PROFILES))} + ), + ) + + async def async_step_auth(self, user_input=None): + """Create an entry for auth.""" + if user_input.get(const.CODE): + self.data = user_input + return self.async_external_step_done(next_step_id="finish") + + profile = user_input.get(const.PROFILE) + + auth_client = self.get_auth_client(profile) + + url = auth_client.get_authorize_url() + + return self.async_external_step(step_id="auth", url=url) + + async def async_step_finish(self, user_input=None): + """Received code for authentication.""" + data = user_input or self.data or {} + + _LOGGER.debug( + "Should close all flows below %s", + self.hass.config_entries.flow.async_progress(), + ) + + profile = data[const.PROFILE] + code = data[const.CODE] + + return await self._async_create_session(profile, code) + + async def _async_create_session(self, profile, code): + """Create withings session and entries.""" + auth_client = self.get_auth_client(profile) + + _LOGGER.debug("Requesting credentials with code: %s.", code) + credentials = auth_client.get_credentials(code) + + return self.async_create_entry( + title=profile, + data={const.PROFILE: profile, const.CREDENTIALS: credentials.__dict__}, + ) + + +class WithingsAuthCallbackView(HomeAssistantView): + """Withings Authorization Callback View.""" + + requires_auth = False + url = const.AUTH_CALLBACK_PATH + name = const.AUTH_CALLBACK_NAME + + def __init__(self): + """Constructor.""" + + async def get(self, request): + """Receive authorization code.""" + hass = request.app["hass"] + + code = request.query.get("code") + profile = request.query.get("profile") + flow_id = request.query.get("flow_id") + + if not flow_id: + return aiohttp.web_response.Response( + status=400, text="'flow_id' argument not provided in url." + ) + + if not profile: + return aiohttp.web_response.Response( + status=400, text="'profile' argument not provided in url." + ) + + if not code: + return aiohttp.web_response.Response( + status=400, text="'code' argument not provided in url." + ) + + try: + await hass.config_entries.flow.async_configure( + flow_id, {const.PROFILE: profile, const.CODE: code} + ) + + return aiohttp.web_response.Response( + status=200, + headers={"content-type": "text/html"}, + text="", + ) + + except data_entry_flow.UnknownFlow: + return aiohttp.web_response.Response(status=400, text="Unknown flow") diff --git a/homeassistant/components/withings/const.py b/homeassistant/components/withings/const.py new file mode 100644 index 00000000000..79527d9d557 --- /dev/null +++ b/homeassistant/components/withings/const.py @@ -0,0 +1,103 @@ +"""Constants used by the Withings component.""" +import homeassistant.const as const + +DATA_MANAGER = "data_manager" + +BASE_URL = "base_url" +CLIENT_ID = "client_id" +CLIENT_SECRET = "client_secret" +CODE = "code" +CONFIG = "config" +CREDENTIALS = "credentials" +DOMAIN = "withings" +LOG_NAMESPACE = "homeassistant.components.withings" +MEASURES = "measures" +PROFILE = "profile" +PROFILES = "profiles" + +AUTH_CALLBACK_PATH = "/api/withings/authorize" +AUTH_CALLBACK_NAME = "withings:authorize" + +THROTTLE_INTERVAL = 60 + +STATE_UNKNOWN = const.STATE_UNKNOWN +STATE_AWAKE = "awake" +STATE_DEEP = "deep" +STATE_LIGHT = "light" +STATE_REM = "rem" + +MEASURE_TYPE_BODY_TEMP = 71 +MEASURE_TYPE_BONE_MASS = 88 +MEASURE_TYPE_DIASTOLIC_BP = 9 +MEASURE_TYPE_FAT_MASS = 8 +MEASURE_TYPE_FAT_MASS_FREE = 5 +MEASURE_TYPE_FAT_RATIO = 6 +MEASURE_TYPE_HEART_PULSE = 11 +MEASURE_TYPE_HEIGHT = 4 +MEASURE_TYPE_HYDRATION = 77 +MEASURE_TYPE_MUSCLE_MASS = 76 +MEASURE_TYPE_PWV = 91 +MEASURE_TYPE_SKIN_TEMP = 73 +MEASURE_TYPE_SLEEP_DEEP_DURATION = "deepsleepduration" +MEASURE_TYPE_SLEEP_HEART_RATE_AVERAGE = "hr_average" +MEASURE_TYPE_SLEEP_HEART_RATE_MAX = "hr_max" +MEASURE_TYPE_SLEEP_HEART_RATE_MIN = "hr_min" +MEASURE_TYPE_SLEEP_LIGHT_DURATION = "lightsleepduration" +MEASURE_TYPE_SLEEP_REM_DURATION = "remsleepduration" +MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_AVERAGE = "rr_average" +MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MAX = "rr_max" +MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MIN = "rr_min" +MEASURE_TYPE_SLEEP_STATE_AWAKE = 0 +MEASURE_TYPE_SLEEP_STATE_DEEP = 2 +MEASURE_TYPE_SLEEP_STATE_LIGHT = 1 +MEASURE_TYPE_SLEEP_STATE_REM = 3 +MEASURE_TYPE_SLEEP_TOSLEEP_DURATION = "durationtosleep" +MEASURE_TYPE_SLEEP_TOWAKEUP_DURATION = "durationtowakeup" +MEASURE_TYPE_SLEEP_WAKEUP_DURATION = "wakeupduration" +MEASURE_TYPE_SLEEP_WAKUP_COUNT = "wakeupcount" +MEASURE_TYPE_SPO2 = 54 +MEASURE_TYPE_SYSTOLIC_BP = 10 +MEASURE_TYPE_TEMP = 12 +MEASURE_TYPE_WEIGHT = 1 + +MEAS_BODY_TEMP_C = "body_temperature_c" +MEAS_BONE_MASS_KG = "bone_mass_kg" +MEAS_DIASTOLIC_MMHG = "diastolic_blood_pressure_mmhg" +MEAS_FAT_FREE_MASS_KG = "fat_free_mass_kg" +MEAS_FAT_MASS_KG = "fat_mass_kg" +MEAS_FAT_RATIO_PCT = "fat_ratio_pct" +MEAS_HEART_PULSE_BPM = "heart_pulse_bpm" +MEAS_HEIGHT_M = "height_m" +MEAS_HYDRATION = "hydration" +MEAS_MUSCLE_MASS_KG = "muscle_mass_kg" +MEAS_PWV = "pulse_wave_velocity" +MEAS_SKIN_TEMP_C = "skin_temperature_c" +MEAS_SLEEP_DEEP_DURATION_SECONDS = "sleep_deep_duration_seconds" +MEAS_SLEEP_HEART_RATE_AVERAGE = "sleep_heart_rate_average_bpm" +MEAS_SLEEP_HEART_RATE_MAX = "sleep_heart_rate_max_bpm" +MEAS_SLEEP_HEART_RATE_MIN = "sleep_heart_rate_min_bpm" +MEAS_SLEEP_LIGHT_DURATION_SECONDS = "sleep_light_duration_seconds" +MEAS_SLEEP_REM_DURATION_SECONDS = "sleep_rem_duration_seconds" +MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE = "sleep_respiratory_average_bpm" +MEAS_SLEEP_RESPIRATORY_RATE_MAX = "sleep_respiratory_max_bpm" +MEAS_SLEEP_RESPIRATORY_RATE_MIN = "sleep_respiratory_min_bpm" +MEAS_SLEEP_STATE = "sleep_state" +MEAS_SLEEP_TOSLEEP_DURATION_SECONDS = "sleep_tosleep_duration_seconds" +MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS = "sleep_towakeup_duration_seconds" +MEAS_SLEEP_WAKEUP_COUNT = "sleep_wakeup_count" +MEAS_SLEEP_WAKEUP_DURATION_SECONDS = "sleep_wakeup_duration_seconds" +MEAS_SPO2_PCT = "spo2_pct" +MEAS_SYSTOLIC_MMGH = "systolic_blood_pressure_mmhg" +MEAS_TEMP_C = "temperature_c" +MEAS_WEIGHT_KG = "weight_kg" + +UOM_BEATS_PER_MINUTE = "bpm" +UOM_BREATHS_PER_MINUTE = "br/m" +UOM_FREQUENCY = "times" +UOM_METERS_PER_SECOND = "m/s" +UOM_MMHG = "mmhg" +UOM_PERCENT = "%" +UOM_LENGTH_M = const.LENGTH_METERS +UOM_MASS_KG = const.MASS_KILOGRAMS +UOM_SECONDS = "seconds" +UOM_TEMP_C = const.TEMP_CELSIUS diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json new file mode 100644 index 00000000000..726d9f13eda --- /dev/null +++ b/homeassistant/components/withings/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "withings", + "name": "Withings", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/withings", + "requirements": [ + "nokia==1.2.0" + ], + "dependencies": [ + "api", + "http", + "webhook" + ], + "codeowners": [ + "@vangorra" + ] +} \ No newline at end of file diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py new file mode 100644 index 00000000000..67cf966c1bc --- /dev/null +++ b/homeassistant/components/withings/sensor.py @@ -0,0 +1,460 @@ +"""Sensors flow for Withings.""" +import typing as types + +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify + +from . import const +from .common import _LOGGER, WithingsDataManager, get_data_manager + +# There's only 3 calls (per profile) made to the withings api every 5 +# minutes (see throttle values). This component wouldn't benefit +# much from parallel updates. +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: types.Callable[[types.List[Entity], bool], None], +): + """Set up the sensor config entry.""" + data_manager = get_data_manager(hass, entry) + entities = create_sensor_entities(data_manager) + async_add_entities(entities, True) + + +def get_measures(): + """Get all the measures. + + This function exists to be easily mockable so we can test + one measure at a time. This becomes necessary when integration + testing throttle functionality in the data manager. + """ + return list(WITHINGS_MEASUREMENTS_MAP) + + +def create_sensor_entities(data_manager: WithingsDataManager): + """Create sensor entities.""" + entities = [] + + measures = get_measures() + + for attribute in WITHINGS_ATTRIBUTES: + if attribute.measurement not in measures: + _LOGGER.debug( + "Skipping measurement %s as it is not in the" + "list of measurements to use", + attribute.measurement, + ) + continue + + _LOGGER.debug( + "Creating entity for measurement: %s, measure_type: %s," + "friendly_name: %s, unit_of_measurement: %s", + attribute.measurement, + attribute.measure_type, + attribute.friendly_name, + attribute.unit_of_measurement, + ) + + entity = WithingsHealthSensor(data_manager, attribute) + + entities.append(entity) + + return entities + + +class WithingsAttribute: + """Base class for modeling withing data.""" + + def __init__( + self, + measurement: str, + measure_type, + friendly_name: str, + unit_of_measurement: str, + icon: str, + ) -> None: + """Constructor.""" + self.measurement = measurement + self.measure_type = measure_type + self.friendly_name = friendly_name + self.unit_of_measurement = unit_of_measurement + self.icon = icon + + +class WithingsMeasureAttribute(WithingsAttribute): + """Model measure attributes.""" + + +class WithingsSleepStateAttribute(WithingsAttribute): + """Model sleep data attributes.""" + + def __init__( + self, measurement: str, friendly_name: str, unit_of_measurement: str, icon: str + ) -> None: + """Constructor.""" + super().__init__(measurement, None, friendly_name, unit_of_measurement, icon) + + +class WithingsSleepSummaryAttribute(WithingsAttribute): + """Models sleep summary attributes.""" + + +WITHINGS_ATTRIBUTES = [ + WithingsMeasureAttribute( + const.MEAS_WEIGHT_KG, + const.MEASURE_TYPE_WEIGHT, + "Weight", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_FAT_MASS_KG, + const.MEASURE_TYPE_FAT_MASS, + "Fat Mass", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_FAT_FREE_MASS_KG, + const.MEASURE_TYPE_FAT_MASS_FREE, + "Fat Free Mass", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_MUSCLE_MASS_KG, + const.MEASURE_TYPE_MUSCLE_MASS, + "Muscle Mass", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_BONE_MASS_KG, + const.MEASURE_TYPE_BONE_MASS, + "Bone Mass", + const.UOM_MASS_KG, + "mdi:weight-kilogram", + ), + WithingsMeasureAttribute( + const.MEAS_HEIGHT_M, + const.MEASURE_TYPE_HEIGHT, + "Height", + const.UOM_LENGTH_M, + "mdi:ruler", + ), + WithingsMeasureAttribute( + const.MEAS_TEMP_C, + const.MEASURE_TYPE_TEMP, + "Temperature", + const.UOM_TEMP_C, + "mdi:thermometer", + ), + WithingsMeasureAttribute( + const.MEAS_BODY_TEMP_C, + const.MEASURE_TYPE_BODY_TEMP, + "Body Temperature", + const.UOM_TEMP_C, + "mdi:thermometer", + ), + WithingsMeasureAttribute( + const.MEAS_SKIN_TEMP_C, + const.MEASURE_TYPE_SKIN_TEMP, + "Skin Temperature", + const.UOM_TEMP_C, + "mdi:thermometer", + ), + WithingsMeasureAttribute( + const.MEAS_FAT_RATIO_PCT, + const.MEASURE_TYPE_FAT_RATIO, + "Fat Ratio", + const.UOM_PERCENT, + None, + ), + WithingsMeasureAttribute( + const.MEAS_DIASTOLIC_MMHG, + const.MEASURE_TYPE_DIASTOLIC_BP, + "Diastolic Blood Pressure", + const.UOM_MMHG, + None, + ), + WithingsMeasureAttribute( + const.MEAS_SYSTOLIC_MMGH, + const.MEASURE_TYPE_SYSTOLIC_BP, + "Systolic Blood Pressure", + const.UOM_MMHG, + None, + ), + WithingsMeasureAttribute( + const.MEAS_HEART_PULSE_BPM, + const.MEASURE_TYPE_HEART_PULSE, + "Heart Pulse", + const.UOM_BEATS_PER_MINUTE, + "mdi:heart-pulse", + ), + WithingsMeasureAttribute( + const.MEAS_SPO2_PCT, const.MEASURE_TYPE_SPO2, "SP02", const.UOM_PERCENT, None + ), + WithingsMeasureAttribute( + const.MEAS_HYDRATION, const.MEASURE_TYPE_HYDRATION, "Hydration", "", "mdi:water" + ), + WithingsMeasureAttribute( + const.MEAS_PWV, + const.MEASURE_TYPE_PWV, + "Pulse Wave Velocity", + const.UOM_METERS_PER_SECOND, + None, + ), + WithingsSleepStateAttribute( + const.MEAS_SLEEP_STATE, "Sleep state", None, "mdi:sleep" + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_WAKEUP_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_WAKEUP_DURATION, + "Wakeup time", + const.UOM_SECONDS, + "mdi:sleep-off", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_LIGHT_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_LIGHT_DURATION, + "Light sleep", + const.UOM_SECONDS, + "mdi:sleep", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_DEEP_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_DEEP_DURATION, + "Deep sleep", + const.UOM_SECONDS, + "mdi:sleep", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_REM_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_REM_DURATION, + "REM sleep", + const.UOM_SECONDS, + "mdi:sleep", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_WAKEUP_COUNT, + const.MEASURE_TYPE_SLEEP_WAKUP_COUNT, + "Wakeup count", + const.UOM_FREQUENCY, + "mdi:sleep-off", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_TOSLEEP_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_TOSLEEP_DURATION, + "Time to sleep", + const.UOM_SECONDS, + "mdi:sleep", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_TOWAKEUP_DURATION_SECONDS, + const.MEASURE_TYPE_SLEEP_TOWAKEUP_DURATION, + "Time to wakeup", + const.UOM_SECONDS, + "mdi:sleep-off", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_HEART_RATE_AVERAGE, + const.MEASURE_TYPE_SLEEP_HEART_RATE_AVERAGE, + "Average heart rate", + const.UOM_BEATS_PER_MINUTE, + "mdi:heart-pulse", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_HEART_RATE_MIN, + const.MEASURE_TYPE_SLEEP_HEART_RATE_MIN, + "Minimum heart rate", + const.UOM_BEATS_PER_MINUTE, + "mdi:heart-pulse", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_HEART_RATE_MAX, + const.MEASURE_TYPE_SLEEP_HEART_RATE_MAX, + "Maximum heart rate", + const.UOM_BEATS_PER_MINUTE, + "mdi:heart-pulse", + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_RESPIRATORY_RATE_AVERAGE, + const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_AVERAGE, + "Average respiratory rate", + const.UOM_BREATHS_PER_MINUTE, + None, + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_RESPIRATORY_RATE_MIN, + const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MIN, + "Minimum respiratory rate", + const.UOM_BREATHS_PER_MINUTE, + None, + ), + WithingsSleepSummaryAttribute( + const.MEAS_SLEEP_RESPIRATORY_RATE_MAX, + const.MEASURE_TYPE_SLEEP_RESPIRATORY_RATE_MAX, + "Maximum respiratory rate", + const.UOM_BREATHS_PER_MINUTE, + None, + ), +] + +WITHINGS_MEASUREMENTS_MAP = {attr.measurement: attr for attr in WITHINGS_ATTRIBUTES} + + +class WithingsHealthSensor(Entity): + """Implementation of a Withings sensor.""" + + def __init__( + self, data_manager: WithingsDataManager, attribute: WithingsAttribute + ) -> None: + """Initialize the Withings sensor.""" + self._data_manager = data_manager + self._attribute = attribute + self._state = None + + self._slug = self._data_manager.slug + self._user_id = self._data_manager.api.get_credentials().user_id + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return f"Withings {self._attribute.measurement} {self._slug}" + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return "withings_{}_{}_{}".format( + self._slug, self._user_id, slugify(self._attribute.measurement) + ) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement of this entity, if any.""" + return self._attribute.unit_of_measurement + + @property + def icon(self) -> str: + """Icon to use in the frontend, if any.""" + return self._attribute.icon + + @property + def device_state_attributes(self): + """Get withings attributes.""" + return self._attribute.__dict__ + + async def async_update(self) -> None: + """Update the data.""" + _LOGGER.debug( + "Async update slug: %s, measurement: %s, user_id: %s", + self._slug, + self._attribute.measurement, + self._user_id, + ) + + if isinstance(self._attribute, WithingsMeasureAttribute): + _LOGGER.debug("Updating measures state") + await self._data_manager.update_measures() + await self.async_update_measure(self._data_manager.measures) + + elif isinstance(self._attribute, WithingsSleepStateAttribute): + _LOGGER.debug("Updating sleep state") + await self._data_manager.update_sleep() + await self.async_update_sleep_state(self._data_manager.sleep) + + elif isinstance(self._attribute, WithingsSleepSummaryAttribute): + _LOGGER.debug("Updating sleep summary state") + await self._data_manager.update_sleep_summary() + await self.async_update_sleep_summary(self._data_manager.sleep_summary) + + async def async_update_measure(self, data) -> None: + """Update the measures data.""" + if data is None: + _LOGGER.error("Provided data is None. Setting state to %s", None) + self._state = None + return + + measure_type = self._attribute.measure_type + + _LOGGER.debug( + "Finding the unambiguous measure group with measure_type: %s", measure_type + ) + measure_groups = [ + g + for g in data + if (not g.is_ambiguous() and g.get_measure(measure_type) is not None) + ] + + if not measure_groups: + _LOGGER.warning("No measure groups found, setting state to %s", None) + self._state = None + return + + _LOGGER.debug( + "Sorting list of %s measure groups by date created (DESC)", + len(measure_groups), + ) + measure_groups.sort(key=(lambda g: g.created), reverse=True) + + self._state = round(measure_groups[0].get_measure(measure_type), 4) + + async def async_update_sleep_state(self, data) -> None: + """Update the sleep state data.""" + if data is None: + _LOGGER.error("Provided data is None. Setting state to %s", None) + self._state = None + return + + if not data.series: + _LOGGER.warning("No sleep data, setting state to %s", None) + self._state = None + return + + series = sorted(data.series, key=lambda o: o.enddate, reverse=True) + + serie = series[0] + + if serie.state == const.MEASURE_TYPE_SLEEP_STATE_AWAKE: + self._state = const.STATE_AWAKE + elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_LIGHT: + self._state = const.STATE_LIGHT + elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_DEEP: + self._state = const.STATE_DEEP + elif serie.state == const.MEASURE_TYPE_SLEEP_STATE_REM: + self._state = const.STATE_REM + else: + self._state = None + + async def async_update_sleep_summary(self, data) -> None: + """Update the sleep summary data.""" + if data is None: + _LOGGER.error("Provided data is None. Setting state to %s", None) + self._state = None + return + + if not data.series: + _LOGGER.warning("Sleep data has no series, setting state to %s", None) + self._state = None + return + + measurement = self._attribute.measurement + measure_type = self._attribute.measure_type + + _LOGGER.debug("Determining total value for: %s", measurement) + total = 0 + for serie in data.series: + if hasattr(serie, measure_type): + total += getattr(serie, measure_type) + + self._state = round(total, 4) diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json new file mode 100644 index 00000000000..1a99abc7255 --- /dev/null +++ b/homeassistant/components/withings/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "title": "Withings", + "step": { + "user": { + "title": "User Profile.", + "description": "Select a user profile to which you want Home Assistant to map with a Withings profile. On the withings page, be sure to select the same user or data will not be labeled correctly.", + "data": { + "profile": "Profile" + } + } + }, + "create_entry": { + "default": "Successfully authenticated with Withings for the selected profile." + }, + "abort": { + "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." + } + } +} diff --git a/homeassistant/components/worldtidesinfo/sensor.py b/homeassistant/components/worldtidesinfo/sensor.py index bd20431d706..aaa9f2d1585 100644 --- a/homeassistant/components/worldtidesinfo/sensor.py +++ b/homeassistant/components/worldtidesinfo/sensor.py @@ -96,12 +96,12 @@ class WorldTidesInfoSensor(Entity): tidetime = time.strftime( "%I:%M %p", time.localtime(self.data["extremes"][0]["dt"]) ) - return "High tide at {}".format(tidetime) + return f"High tide at {tidetime}" if "Low" in str(self.data["extremes"][0]["type"]): tidetime = time.strftime( "%I:%M %p", time.localtime(self.data["extremes"][0]["dt"]) ) - return "Low tide at {}".format(tidetime) + return f"Low tide at {tidetime}" return None return None diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 69e2813e7d1..4e9bf0a6a4a 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -63,12 +63,12 @@ class WorxLandroidSensor(Entity): self.pin = config.get(CONF_PIN) self.timeout = config.get(CONF_TIMEOUT) self.allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE) - self.url = "http://{}/jsondata.cgi".format(self.host) + self.url = f"http://{self.host}/jsondata.cgi" @property def name(self): """Return the name of the sensor.""" - return "worxlandroid-{}".format(self.sensor) + return f"worxlandroid-{self.sensor}" @property def state(self): diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 21f87d9ce0b..5272b33ccb5 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -968,7 +968,7 @@ async def async_setup_platform( ) if pws_id is None: - unique_id_base = "@{:06f},{:06f}".format(longitude, latitude) + unique_id_base = f"@{longitude:06f},{latitude:06f}" else: # Manually specified weather station, use that for unique_id unique_id_base = pws_id @@ -999,7 +999,7 @@ class WUndergroundSensor(Entity): # This is only the suggested entity id, it might get changed by # the entity registry later. self.entity_id = sensor.ENTITY_ID_FORMAT.format("pws_" + condition) - self._unique_id = "{},{}".format(unique_id_base, condition) + self._unique_id = f"{unique_id_base},{condition}" self._device_class = self._cfg_expand("device_class") def _cfg_expand(self, what, default=None): @@ -1106,7 +1106,7 @@ class WUndergroundData: self._hass = hass self._api_key = api_key self._pws_id = pws_id - self._lang = "lang:{}".format(lang) + self._lang = f"lang:{lang}" self._latitude = latitude self._longitude = longitude self._features = set() @@ -1122,9 +1122,9 @@ class WUndergroundData: self._api_key, "/".join(sorted(self._features)), self._lang ) if self._pws_id: - url = url + "pws:{}".format(self._pws_id) + url = url + f"pws:{self._pws_id}" else: - url = url + "{},{}".format(self._latitude, self._longitude) + url = url + f"{self._latitude},{self._longitude}" return url + ".json" diff --git a/homeassistant/components/wwlln/.translations/hu.json b/homeassistant/components/wwlln/.translations/hu.json new file mode 100644 index 00000000000..740fc1a8179 --- /dev/null +++ b/homeassistant/components/wwlln/.translations/hu.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Sz\u00e9less\u00e9g", + "longitude": "Hossz\u00fas\u00e1g" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/it.json b/homeassistant/components/wwlln/.translations/it.json new file mode 100644 index 00000000000..f0fc3263607 --- /dev/null +++ b/homeassistant/components/wwlln/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" + }, + "step": { + "user": { + "data": { + "latitude": "Latitudine", + "longitude": "Longitudine", + "radius": "Raggio (utilizzando il tuo sistema di unit\u00e0 di misura di base)" + }, + "title": "Inserisci le informazioni sulla tua posizione." + } + }, + "title": "Rete mondiale di localizzazione dei fulmini (WWLLN)" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/ko.json b/homeassistant/components/wwlln/.translations/ko.json index 5e879cd7330..e5831f5af29 100644 --- a/homeassistant/components/wwlln/.translations/ko.json +++ b/homeassistant/components/wwlln/.translations/ko.json @@ -10,7 +10,7 @@ "longitude": "\uacbd\ub3c4", "radius": "\ubc18\uacbd (\uae30\ubcf8 \ub2e8\uc704 \uc2dc\uc2a4\ud15c \uc0ac\uc6a9)" }, - "title": "\uc704\uce58 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + "title": "\uc704\uce58 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." } }, "title": "\uc138\uacc4 \ub099\ub8b0 \uc704\uce58\ub9dd (WWLLN)" diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json index 704c7baeecb..652d580644f 100644 --- a/homeassistant/components/wwlln/.translations/pl.json +++ b/homeassistant/components/wwlln/.translations/pl.json @@ -10,7 +10,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "radius": "Promie\u0144 (przy u\u017cyciu systemu jednostki bazowej)" }, - "title": "Wpisz informacje o swojej lokalizacji." + "title": "Wprowad\u017a informacje o lokalizacji." } }, "title": "\u015awiatowa sie\u0107 lokalizacji wy\u0142adowa\u0144 atmosferycznych (WWLLN)" diff --git a/homeassistant/components/wwlln/__init__.py b/homeassistant/components/wwlln/__init__.py index ca3711490e7..412efc904db 100644 --- a/homeassistant/components/wwlln/__init__.py +++ b/homeassistant/components/wwlln/__init__.py @@ -47,7 +47,7 @@ async def async_setup(hass, config): latitude = conf.get(CONF_LATITUDE, hass.config.latitude) longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) - identifier = "{0}, {1}".format(latitude, longitude) + identifier = f"{latitude}, {longitude}" if identifier in configured_instances(hass): return True diff --git a/homeassistant/components/wwlln/geo_location.py b/homeassistant/components/wwlln/geo_location.py index 661972ff437..e8dd7ec08c7 100644 --- a/homeassistant/components/wwlln/geo_location.py +++ b/homeassistant/components/wwlln/geo_location.py @@ -88,6 +88,7 @@ class WWLLNEventManager: @callback def _create_events(self, ids_to_create): """Create new geo location events.""" + _LOGGER.debug("Going to create %s", ids_to_create) events = [] for strike_id in ids_to_create: strike = self._strikes[strike_id] @@ -106,6 +107,7 @@ class WWLLNEventManager: @callback def _remove_events(self, ids_to_remove): """Remove old geo location events.""" + _LOGGER.debug("Going to remove %s", ids_to_remove) for strike_id in ids_to_remove: async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(strike_id)) @@ -136,12 +138,17 @@ class WWLLNEventManager: return new_strike_ids = set(self._strikes) + # Remove all managed entities that are not in the latest update anymore. ids_to_remove = self._managed_strike_ids.difference(new_strike_ids) self._remove_events(ids_to_remove) + # Create new entities for all strikes that are not managed entities yet. ids_to_create = new_strike_ids.difference(self._managed_strike_ids) self._create_events(ids_to_create) + # Store all external IDs of all managed strikes. + self._managed_strike_ids = new_strike_ids + class WWLLNEvent(GeolocationEvent): """Define a lightning strike event.""" diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json index ef9295341c0..6d13f7adbfd 100644 --- a/homeassistant/components/wwlln/manifest.json +++ b/homeassistant/components/wwlln/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/wwlln", "requirements": [ - "aiowwlln==1.0.0" + "aiowwlln==2.0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/xiaomi/camera.py b/homeassistant/components/xiaomi/camera.py index 905b19c622b..363c17fe4a9 100644 --- a/homeassistant/components/xiaomi/camera.py +++ b/homeassistant/components/xiaomi/camera.py @@ -5,6 +5,7 @@ import logging import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA +from homeassistant.exceptions import TemplateError from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ( CONF_HOST, @@ -34,7 +35,7 @@ MODEL_XIAOFANG = "xiaofang" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_HOST): cv.template, vol.Required(CONF_MODEL): vol.Any(MODEL_YI, MODEL_XIAOFANG), vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PATH, default=DEFAULT_PATH): cv.string, @@ -63,6 +64,7 @@ class XiaomiCamera(Camera): self._manager = hass.data[DATA_FFMPEG] self._name = config[CONF_NAME] self.host = config[CONF_HOST] + self.host.hass = hass self._model = config[CONF_MODEL] self.port = config[CONF_PORT] self.path = config[CONF_PATH] @@ -84,11 +86,11 @@ class XiaomiCamera(Camera): """Return the camera model.""" return self._model - def get_latest_video_url(self): + def get_latest_video_url(self, host): """Retrieve the latest video file from the Xiaomi Camera FTP server.""" from ftplib import FTP, error_perm - ftp = FTP(self.host) + ftp = FTP(host) try: ftp.login(self.user, self.passwd) except error_perm as exc: @@ -133,14 +135,20 @@ class XiaomiCamera(Camera): video = videos[-1] return "ftp://{0}:{1}@{2}:{3}{4}/{5}".format( - self.user, self.passwd, self.host, self.port, ftp.pwd(), video + self.user, self.passwd, host, self.port, ftp.pwd(), video ) async def async_camera_image(self): """Return a still image response from the camera.""" from haffmpeg.tools import ImageFrame, IMAGE_JPEG - url = await self.hass.async_add_job(self.get_latest_video_url) + try: + host = self.host.async_render() + except TemplateError as exc: + _LOGGER.error("Error parsing template %s: %s", self.host, exc) + return self._last_image + + url = await self.hass.async_add_executor_job(self.get_latest_video_url, host) if url != self._last_url: ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) self._last_image = await asyncio.shield( diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 36ce4589396..dbc647f4982 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -143,7 +143,7 @@ def _retrieve_list(host, token, **kwargs): def _get_token(host, username, password): """Get authentication token for the given host+username+password.""" - url = "http://{}/cgi-bin/luci/api/xqsystem/login".format(host) + url = f"http://{host}/cgi-bin/luci/api/xqsystem/login" data = {"username": username, "password": password} try: res = requests.post(url, data=data, timeout=5) diff --git a/homeassistant/components/xiaomi_aqara/__init__.py b/homeassistant/components/xiaomi_aqara/__init__.py index cf2411ccda5..6e2298e05b9 100644 --- a/homeassistant/components/xiaomi_aqara/__init__.py +++ b/homeassistant/components/xiaomi_aqara/__init__.py @@ -232,7 +232,7 @@ class XiaomiDevice(Entity): self._state = None self._is_available = True self._sid = device["sid"] - self._name = "{}_{}".format(device_type, self._sid) + self._name = f"{device_type}_{self._sid}" self._type = device_type self._write_to_hub = xiaomi_hub.write_to_hub self._get_from_hub = xiaomi_hub.get_from_hub @@ -247,7 +247,7 @@ class XiaomiDevice(Entity): self._data_key, self._sid # pylint: disable=no-member ) else: - self._unique_id = "{}{}".format(self._type, self._sid) + self._unique_id = f"{self._type}{self._sid}" def _add_push_data_job(self, *args): self.hass.add_job(self.push_data, *args) @@ -345,7 +345,7 @@ def _add_gateway_to_schema(xiaomi, schema): if gateway.sid == sid: return gateway - raise vol.Invalid("Unknown gateway sid {}".format(sid)) + raise vol.Invalid(f"Unknown gateway sid {sid}") gateways = list(xiaomi.gateways.values()) kwargs = {} diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 93ca7e4bde0..c6ca6db32fb 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -440,7 +440,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index ebb5be2cc06..3d23f1dfc98 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -136,7 +136,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, @@ -731,7 +731,7 @@ class XiaomiPhilipsEyecareLampAmbientLight(XiaomiPhilipsAbstractLight): def __init__(self, name, light, model, unique_id): """Initialize the light device.""" - name = "{} Ambient Light".format(name) + name = f"{name} Ambient Light" if unique_id is not None: unique_id = "{}-{}".format(unique_id, "ambient") super().__init__(name, light, model, unique_id) diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index d66d8ce39b1..311a356870c 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -90,7 +90,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= try: device_info = device.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ffbdf281843..0ebffb06fcd 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -52,7 +52,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= air_quality_monitor = AirQualityMonitor(host, token) device_info = air_quality_monitor.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index 8188d791188..5f79652621b 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -117,7 +117,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= miio_device = Device(host, token) device_info = miio_device.info() model = device_info.model - unique_id = "{}-{}".format(model, device_info.mac_address) + unique_id = f"{model}-{device_info.mac_address}" _LOGGER.info( "%s %s %s detected", model, @@ -426,7 +426,7 @@ class ChuangMiPlugSwitch(XiaomiPlugGenericSwitch): def __init__(self, name, plug, model, unique_id, channel_usb): """Initialize the plug switch.""" - name = "{} USB".format(name) if channel_usb else name + name = f"{name} USB" if channel_usb else name if unique_id is not None and channel_usb: unique_id = "{}-{}".format(unique_id, "usb") diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index ce22bf7a953..3719113f7c9 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -87,12 +87,12 @@ class XmppNotificationService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) - text = "{}: {}".format(title, message) if title else message + text = f"{title}: {message}" if title else message data = kwargs.get(ATTR_DATA) timeout = data.get(ATTR_TIMEOUT, XEP_0363_TIMEOUT) if data else None await async_send_message( - "{}/{}".format(self._sender, self._resource), + f"{self._sender}/{self._resource}", self._password, self._recipient, self._tls, diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index ff976c6b12f..e699ab74e68 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -114,7 +114,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for recv in rxv.find(): receivers.extend(recv.zone_controllers()) else: - ctrl_url = "http://{}:80/YamahaRemoteControl/ctrl".format(host) + ctrl_url = f"http://{host}:80/YamahaRemoteControl/ctrl" receivers = rxv.RXV(ctrl_url, name).zone_controllers() devices = [] @@ -276,7 +276,7 @@ class YamahaDevice(MediaPlayerDevice): @property def zone_id(self): """Return a zone_id to ensure 1 media player per zone.""" - return "{0}:{1}".format(self.receiver.ctrl_url, self._zone) + return f"{self.receiver.ctrl_url}:{self._zone}" @property def supported_features(self): @@ -410,6 +410,6 @@ class YamahaDevice(MediaPlayerDevice): # If both song and station is available, print both, otherwise # just the one we have. if song and station: - return "{}: {}".format(station, song) + return f"{station}: {song}" return song or station diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index d82b093ca7e..38e606a0962 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -128,7 +128,7 @@ class YamahaDevice(MediaPlayerDevice): @property def name(self): """Return the name of the device.""" - return "{} ({})".format(self._name, self._zone.zone_id) + return f"{self._name} ({self._zone.zone_id})" @property def state(self): diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 172d66f9bf5..c899c811a47 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "yeelight" DATA_YEELIGHT = DOMAIN DATA_UPDATED = "yeelight_{}_data_updated" -DEVICE_INITIALIZED = "{}_device_initialized".format(DOMAIN) +DEVICE_INITIALIZED = f"{DOMAIN}_device_initialized" DEFAULT_NAME = "Yeelight" DEFAULT_TRANSITION = 350 @@ -37,6 +37,7 @@ CONF_SAVE_ON_CHANGE = "save_on_change" CONF_MODE_MUSIC = "use_music_mode" CONF_FLOW_PARAMS = "flow_params" CONF_CUSTOM_EFFECTS = "custom_effects" +CONF_NIGHTLIGHT_SWITCH_TYPE = "nightlight_switch_type" ATTR_COUNT = "count" ATTR_ACTION = "action" @@ -48,6 +49,8 @@ ACTION_OFF = "off" ACTIVE_MODE_NIGHTLIGHT = "1" +NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" + SCAN_INTERVAL = timedelta(seconds=30) YEELIGHT_RGB_TRANSITION = "RGBTransition" @@ -84,6 +87,9 @@ DEVICE_SCHEMA = vol.Schema( vol.Optional(CONF_TRANSITION, default=DEFAULT_TRANSITION): cv.positive_int, vol.Optional(CONF_MODE_MUSIC, default=False): cv.boolean, vol.Optional(CONF_SAVE_ON_CHANGE, default=False): cv.boolean, + vol.Optional(CONF_NIGHTLIGHT_SWITCH_TYPE): vol.Any( + NIGHTLIGHT_SWITCH_TYPE_LIGHT + ), vol.Optional(CONF_MODEL): cv.string, } ) @@ -256,10 +262,12 @@ class YeelightDevice: return self._device_type - def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None): + def turn_on(self, duration=DEFAULT_TRANSITION, light_type=None, power_mode=None): """Turn on device.""" try: - self.bulb.turn_on(duration=duration, light_type=light_type) + self.bulb.turn_on( + duration=duration, light_type=light_type, power_mode=power_mode + ) except BulbException as ex: _LOGGER.error("Unable to turn the bulb on: %s", ex) diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index 0a6e021df94..da39152e9ca 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -48,7 +48,7 @@ class YeelightNightlightModeSensor(BinarySensorDevice): @property def name(self): """Return the name of the sensor.""" - return "{} nightlight".format(self._device.name) + return f"{self._device.name} nightlight" @property def is_on(self): diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index a3d5d2dec2e..b47cdb98161 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -3,9 +3,10 @@ import logging import voluptuous as vol from yeelight import RGBTransition, SleepTransition, Flow, BulbException -from yeelight.enums import PowerMode, LightType, BulbType +from yeelight.enums import PowerMode, LightType, BulbType, SceneClass from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import extract_entity_ids +import homeassistant.helpers.config_validation as cv from homeassistant.util.color import ( color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired, @@ -28,6 +29,8 @@ from homeassistant.components.light import ( SUPPORT_FLASH, SUPPORT_EFFECT, Light, + ATTR_RGB_COLOR, + ATTR_KELVIN, ) import homeassistant.util.color as color_util from . import ( @@ -45,10 +48,14 @@ from . import ( CONF_FLOW_PARAMS, ATTR_ACTION, ATTR_COUNT, + NIGHTLIGHT_SWITCH_TYPE_LIGHT, + CONF_NIGHTLIGHT_SWITCH_TYPE, ) _LOGGER = logging.getLogger(__name__) +PLATFORM_DATA_KEY = f"{DATA_YEELIGHT}_lights" + SUPPORT_YEELIGHT = ( SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_EFFECT ) @@ -58,9 +65,15 @@ SUPPORT_YEELIGHT_WHITE_TEMP = SUPPORT_YEELIGHT | SUPPORT_COLOR_TEMP SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR ATTR_MODE = "mode" +ATTR_MINUTES = "minutes" SERVICE_SET_MODE = "set_mode" SERVICE_START_FLOW = "start_flow" +SERVICE_SET_COLOR_SCENE = "set_color_scene" +SERVICE_SET_HSV_SCENE = "set_hsv_scene" +SERVICE_SET_COLOR_TEMP_SCENE = "set_color_temp_scene" +SERVICE_SET_COLOR_FLOW_SCENE = "set_color_flow_scene" +SERVICE_SET_AUTO_DELAY_OFF_SCENE = "set_auto_delay_off_scene" EFFECT_DISCO = "Disco" EFFECT_TEMP = "Slow Temp" @@ -121,6 +134,60 @@ MODEL_TO_DEVICE_TYPE = { "ceiling4": BulbType.WhiteTempMood, } +VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) + +SERVICE_SCHEMA_SET_MODE = YEELIGHT_SERVICE_SCHEMA.extend( + {vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])} +) + +SERVICE_SCHEMA_START_FLOW = YEELIGHT_SERVICE_SCHEMA.extend( + YEELIGHT_FLOW_TRANSITION_SCHEMA +) + +SERVICE_SCHEMA_SET_COLOR_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_RGB_COLOR): vol.All( + vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + ), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + } +) + +SERVICE_SCHEMA_SET_HSV_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_HS_COLOR): vol.All( + vol.ExactSequence( + ( + vol.All(vol.Coerce(float), vol.Range(min=0, max=359)), + vol.All(vol.Coerce(float), vol.Range(min=0, max=100)), + ) + ), + vol.Coerce(tuple), + ), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + } +) + +SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1700, max=6500) + ), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + } +) + +SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE = YEELIGHT_SERVICE_SCHEMA.extend( + YEELIGHT_FLOW_TRANSITION_SCHEMA +) + +SERVICE_SCHEMA_SET_AUTO_DELAY_OFF = YEELIGHT_SERVICE_SCHEMA.extend( + { + vol.Required(ATTR_MINUTES): vol.All(vol.Coerce(int), vol.Range(min=1, max=60)), + vol.Required(ATTR_BRIGHTNESS): VALID_BRIGHTNESS, + } +) + def _transitions_config_parser(transitions): """Parse transitions config into initialized objects.""" @@ -165,18 +232,20 @@ def _cmd(func): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight bulbs.""" - data_key = "{}_lights".format(DATA_YEELIGHT) if not discovery_info: return - if data_key not in hass.data: - hass.data[data_key] = [] + if PLATFORM_DATA_KEY not in hass.data: + hass.data[PLATFORM_DATA_KEY] = [] device = hass.data[DATA_YEELIGHT][discovery_info[CONF_HOST]] _LOGGER.debug("Adding %s", device.name) custom_effects = _parse_custom_effects(discovery_info[CONF_CUSTOM_EFFECTS]) + nl_switch_light = ( + discovery_info.get(CONF_NIGHTLIGHT_SWITCH_TYPE) == NIGHTLIGHT_SWITCH_TYPE_LIGHT + ) lights = [] @@ -193,9 +262,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): elif device_type == BulbType.Color: _lights_setup_helper(YeelightColorLight) elif device_type == BulbType.WhiteTemp: - _lights_setup_helper(YeelightWhiteTempLight) + if nl_switch_light and device.is_nightlight_supported: + _lights_setup_helper(YeelightWithNightLight) + _lights_setup_helper(YeelightNightLightMode) + else: + _lights_setup_helper(YeelightWhiteTempWithoutNightlightSwitch) elif device_type == BulbType.WhiteTempMood: - _lights_setup_helper(YeelightWithAmbientLight) + if nl_switch_light and device.is_nightlight_supported: + _lights_setup_helper(YeelightNightLightMode) + _lights_setup_helper(YeelightWithAmbientAndNightlight) + else: + _lights_setup_helper(YeelightWithAmbientWithoutNightlight) _lights_setup_helper(YeelightAmbientLight) else: _lights_setup_helper(YeelightGenericLight) @@ -205,41 +282,120 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device.name, ) - hass.data[data_key] += lights + hass.data[PLATFORM_DATA_KEY] += lights add_entities(lights, True) + setup_services(hass) - def service_handler(service): - """Dispatch service calls to target entities.""" - params = { - key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID - } - entity_ids = extract_entity_ids(hass, service) - target_devices = [ - light for light in hass.data[data_key] if light.entity_id in entity_ids - ] +def setup_services(hass): + """Set up the service listeners.""" - for target_device in target_devices: - if service.service == SERVICE_SET_MODE: - target_device.set_mode(**params) - elif service.service == SERVICE_START_FLOW: - params[ATTR_TRANSITIONS] = _transitions_config_parser( - params[ATTR_TRANSITIONS] - ) - target_device.start_flow(**params) + def service_call(func): + def service_to_entities(service): + """Return the known entities that a service call mentions.""" - service_schema_set_mode = YEELIGHT_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode])} + entity_ids = extract_entity_ids(hass, service) + target_devices = [ + light + for light in hass.data[PLATFORM_DATA_KEY] + if light.entity_id in entity_ids + ] + + return target_devices + + def service_to_params(service): + """Return service call params, without entity_id.""" + return { + key: value + for key, value in service.data.items() + if key != ATTR_ENTITY_ID + } + + def wrapper(service): + params = service_to_params(service) + target_devices = service_to_entities(service) + for device in target_devices: + func(device, params) + + return wrapper + + @service_call + def service_set_mode(target_device, params): + target_device.set_mode(**params) + + @service_call + def service_start_flow(target_devices, params): + params[ATTR_TRANSITIONS] = _transitions_config_parser(params[ATTR_TRANSITIONS]) + target_devices.start_flow(**params) + + @service_call + def service_set_color_scene(target_device, params): + target_device.set_scene( + SceneClass.COLOR, *[*params[ATTR_RGB_COLOR], params[ATTR_BRIGHTNESS]] + ) + + @service_call + def service_set_hsv_scene(target_device, params): + target_device.set_scene( + SceneClass.HSV, *[*params[ATTR_HS_COLOR], params[ATTR_BRIGHTNESS]] + ) + + @service_call + def service_set_color_temp_scene(target_device, params): + target_device.set_scene( + SceneClass.CT, params[ATTR_KELVIN], params[ATTR_BRIGHTNESS] + ) + + @service_call + def service_set_color_flow_scene(target_device, params): + flow = Flow( + count=params[ATTR_COUNT], + action=Flow.actions[params[ATTR_ACTION]], + transitions=_transitions_config_parser(params[ATTR_TRANSITIONS]), + ) + target_device.set_scene(SceneClass.CF, flow) + + @service_call + def service_set_auto_delay_off_scene(target_device, params): + target_device.set_scene( + SceneClass.AUTO_DELAY_OFF, params[ATTR_BRIGHTNESS], params[ATTR_MINUTES] + ) + + hass.services.register( + DOMAIN, SERVICE_SET_MODE, service_set_mode, schema=SERVICE_SCHEMA_SET_MODE ) hass.services.register( - DOMAIN, SERVICE_SET_MODE, service_handler, schema=service_schema_set_mode - ) - - service_schema_start_flow = YEELIGHT_SERVICE_SCHEMA.extend( - YEELIGHT_FLOW_TRANSITION_SCHEMA + DOMAIN, SERVICE_START_FLOW, service_start_flow, schema=SERVICE_SCHEMA_START_FLOW ) hass.services.register( - DOMAIN, SERVICE_START_FLOW, service_handler, schema=service_schema_start_flow + DOMAIN, + SERVICE_SET_COLOR_SCENE, + service_set_color_scene, + schema=SERVICE_SCHEMA_SET_COLOR_SCENE, + ) + hass.services.register( + DOMAIN, + SERVICE_SET_HSV_SCENE, + service_set_hsv_scene, + schema=SERVICE_SCHEMA_SET_HSV_SCENE, + ) + hass.services.register( + DOMAIN, + SERVICE_SET_COLOR_TEMP_SCENE, + service_set_color_temp_scene, + schema=SERVICE_SCHEMA_SET_COLOR_TEMP_SCENE, + ) + hass.services.register( + DOMAIN, + SERVICE_SET_COLOR_FLOW_SCENE, + service_set_color_flow_scene, + schema=SERVICE_SCHEMA_SET_COLOR_FLOW_SCENE, + ) + hass.services.register( + DOMAIN, + SERVICE_SET_AUTO_DELAY_OFF_SCENE, + service_set_auto_delay_off_scene, + schema=SERVICE_SCHEMA_SET_AUTO_DELAY_OFF, ) @@ -376,6 +532,10 @@ class YeelightGenericLight(Light): def _power_property(self): return "power" + @property + def _turn_on_power_mode(self): + return PowerMode.LAST + @property def _predefined_effects(self): return YEELIGHT_MONO_EFFECT_LIST @@ -559,7 +719,11 @@ class YeelightGenericLight(Light): if ATTR_TRANSITION in kwargs: # passed kwarg overrides config duration = int(kwargs.get(ATTR_TRANSITION) * 1000) # kwarg in s - self.device.turn_on(duration=duration, light_type=self.light_type) + self.device.turn_on( + duration=duration, + light_type=self.light_type, + power_mode=self._turn_on_power_mode, + ) if self.config[CONF_MODE_MUSIC] and not self._bulb.music_mode: try: @@ -618,6 +782,18 @@ class YeelightGenericLight(Light): except BulbException as ex: _LOGGER.error("Unable to set effect: %s", ex) + def set_scene(self, scene_class, *args): + """ + Set the light directly to the specified state. + + If the light is off, it will first be turned on. + """ + try: + self._bulb.set_scene(scene_class, *args) + self.device.update() + except BulbException as ex: + _LOGGER.error("Unable to set scene: %s", ex) + class YeelightColorLight(YeelightGenericLight): """Representation of a Color Yeelight light.""" @@ -632,7 +808,7 @@ class YeelightColorLight(YeelightGenericLight): return YEELIGHT_COLOR_EFFECT_LIST -class YeelightWhiteTempLight(YeelightGenericLight): +class YeelightWhiteTempLightsupport: """Representation of a Color Yeelight light.""" @property @@ -640,17 +816,84 @@ class YeelightWhiteTempLight(YeelightGenericLight): """Flag supported features.""" return SUPPORT_YEELIGHT_WHITE_TEMP + @property + def _predefined_effects(self): + return YEELIGHT_TEMP_ONLY_EFFECT_LIST + + +class YeelightWhiteTempWithoutNightlightSwitch( + YeelightWhiteTempLightsupport, YeelightGenericLight +): + """White temp light, when nightlight switch is not set to light.""" + @property def _brightness_property(self): return "current_brightness" + +class YeelightWithNightLight(YeelightWhiteTempLightsupport, YeelightGenericLight): + """Representation of a Yeelight with nightlight support. + + It represents case when nightlight switch is set to light. + """ + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return super().is_on and not self.device.is_nightlight_enabled + + @property + def _turn_on_power_mode(self): + return PowerMode.NORMAL + + +class YeelightNightLightMode(YeelightGenericLight): + """Representation of a Yeelight when in nightlight mode.""" + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return f"{self.device.name} nightlight" + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:weather-night" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + return super().is_on and self.device.is_nightlight_enabled + + @property + def _brightness_property(self): + return "nl_br" + + @property + def _turn_on_power_mode(self): + return PowerMode.MOONLIGHT + @property def _predefined_effects(self): return YEELIGHT_TEMP_ONLY_EFFECT_LIST -class YeelightWithAmbientLight(YeelightWhiteTempLight): - """Representation of a Yeelight which has ambilight support.""" +class YeelightWithAmbientWithoutNightlight(YeelightWhiteTempWithoutNightlightSwitch): + """Representation of a Yeelight which has ambilight support. + + And nightlight switch type is none. + """ + + @property + def _power_property(self): + return "main_power" + + +class YeelightWithAmbientAndNightlight(YeelightWithNightLight): + """Representation of a Yeelight which has ambilight support. + + And nightlight switch type is set to light. + """ @property def _power_property(self): @@ -673,7 +916,7 @@ class YeelightAmbientLight(YeelightColorLight): @property def name(self) -> str: """Return the name of the device if any.""" - return "{} ambilight".format(self.device.name) + return f"{self.device.name} ambilight" def _get_property(self, prop, default=None): bg_prop = self.PROPERTIES_MAPPING.get(prop) diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index 14dcfb27a4d..52106a42063 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -7,7 +7,69 @@ set_mode: mode: description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. example: 'moonlight' - +set_color_scene: + description: Changes the light to the specified RGB color and brightness. If the light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + rgb_color: + description: Color for the light in RGB-format. + example: '[255, 100, 100]' + brightness: + description: The brightness value to set (1-100). + example: 50 +set_hsv_scene: + description: Changes the light to the specified HSV color and brightness. If the light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + hs_color: + description: Color for the light in hue/sat format. Hue is 0-359 and Sat is 0-100. + example: '[300, 70]' + brightness: + description: The brightness value to set (1-100). + example: 50 +set_color_temp_scene: + description: Changes the light to the specified color temperature. If the light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + kelvin: + description: Color temperature for the light in Kelvin. + example: 4000 + brightness: + description: The brightness value to set (1-100). + example: 50 +set_color_flow_scene: + description: starts a color flow. If the light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + count: + description: The number of times to run this flow (0 to run forever). + example: 0 + action: + description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') + example: 'stay' + transitions: + description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html + example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' +set_auto_delay_off_scene: + description: Turns the light on to the specified brightness and sets a timer to turn it back off after the given number of minutes. If the light is off, Set a color scene, if light is off, it will be turned on. + fields: + entity_id: + description: Name of the light entity. + example: 'light.yeelight' + minutes: + description: The minutes to wait before automatically turning the light off. + example: 5 + brightness: + description: The brightness value to set (1-100). + example: 50 start_flow: description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects fields: diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 896daac96c4..fa836f2776f 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -50,7 +50,7 @@ class SunflowerBulb(Light): @property def name(self): """Return the display name of this light.""" - return "sunflower_{}".format(self._light.zid) + return f"sunflower_{self._light.zid}" @property def available(self): diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index 15d966d1354..3d8c63621be 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -106,7 +106,7 @@ class YrSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self._name) + return f"{self.client_name} {self._name}" @property def state(self): @@ -168,7 +168,7 @@ class YrData: with async_timeout.timeout(10): resp = await websession.get(self._url, params=self._urlparams) if resp.status != 200: - try_again("{} returned {}".format(resp.url, resp.status)) + try_again(f"{resp.url} returned {resp.status}") return text = await resp.text() diff --git a/homeassistant/components/yweather/sensor.py b/homeassistant/components/yweather/sensor.py index d23b49a0230..4dc23699872 100644 --- a/homeassistant/components/yweather/sensor.py +++ b/homeassistant/components/yweather/sensor.py @@ -108,7 +108,7 @@ class YahooWeatherSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._client, self._name) + return f"{self._client} {self._name}" @property def state(self): diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 52f6617c397..9eea1f6612c 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -124,7 +124,7 @@ class ZamgSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self.client_name, self.variable) + return f"{self.client_name} {self.variable}" @property def state(self): @@ -212,7 +212,7 @@ class ZamgData: } break else: - raise ValueError("No weather data for station {}".format(self._station_id)) + raise ValueError(f"No weather data for station {self._station_id}") def get_data(self, variable): """Get the data.""" diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 2ed03b73eff..af107a6ae0d 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" - zeroconf_name = "{}.{}".format(hass.config.location_name, ZEROCONF_TYPE) + zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}" params = { "version": __version__, diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 0b5c75934b6..703e3bf25a0 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -19,7 +19,7 @@ CONF_ZPID = "zpid" DEFAULT_NAME = "Zestimate" NAME = "zestimate" -ZESTIMATE = "{}:{}".format(DEFAULT_NAME, NAME) +ZESTIMATE = f"{DEFAULT_NAME}:{NAME}" ICON = "mdi:home-variant" @@ -74,7 +74,7 @@ class ZestimateDataSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, self.address) + return f"{self._name} {self.address}" @property def state(self): diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 95fea9b5e71..be079e83fa6 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -148,6 +148,31 @@ async def websocket_get_devices(hass, connection, msg): connection.send_result(msg[ID], devices) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): convert_ieee} +) +async def websocket_get_device(hass, connection, msg): + """Get ZHA devices.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + ieee = msg[ATTR_IEEE] + device = None + if ieee in zha_gateway.devices: + device = async_get_device_info( + hass, zha_gateway.devices[ieee], ha_device_registry=ha_device_registry + ) + if not device: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Device not found" + ) + ) + return + connection.send_result(msg[ID], device) + + @callback def async_get_device_info(hass, device, ha_device_registry=None): """Get ZHA device.""" @@ -258,10 +283,10 @@ async def websocket_device_cluster_attributes(hass, connection, msg): ) _LOGGER.debug( "Requested attributes for: %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(RESPONSE, cluster_attributes), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{RESPONSE}: [{cluster_attributes}]", ) connection.send_result(msg[ID], cluster_attributes) @@ -312,10 +337,10 @@ async def websocket_device_cluster_commands(hass, connection, msg): ) _LOGGER.debug( "Requested commands for: %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(RESPONSE, cluster_commands), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{RESPONSE}: [{cluster_commands}]", ) connection.send_result(msg[ID], cluster_commands) @@ -356,11 +381,11 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): ) _LOGGER.debug( "Read attribute for: %s %s %s %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), - "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{ATTR_ATTRIBUTE}: [{attribute}]", + f"{ATTR_MANUFACTURER}: [{manufacturer}]", "{}: [{}]".format(RESPONSE, str(success.get(attribute))), "{}: [{}]".format("failure", failure), ) @@ -386,7 +411,7 @@ async def websocket_get_bindable_devices(hass, connection, msg): _LOGGER.debug( "Get bindable devices: %s %s", - "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee), + f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", "{}: [{}]".format("bindable devices:", devices), ) @@ -410,8 +435,8 @@ async def websocket_bind_devices(hass, connection, msg): await async_binding_operation(zha_gateway, source_ieee, target_ieee, BIND_REQUEST) _LOGGER.info( "Issue bind devices: %s %s", - "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee), - "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee), + f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", + f"{ATTR_TARGET_IEEE}: [{target_ieee}]", ) @@ -432,8 +457,8 @@ async def websocket_unbind_devices(hass, connection, msg): await async_binding_operation(zha_gateway, source_ieee, target_ieee, UNBIND_REQUEST) _LOGGER.info( "Issue unbind devices: %s %s", - "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee), - "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee), + f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", + f"{ATTR_TARGET_IEEE}: [{target_ieee}]", ) @@ -457,8 +482,8 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati _LOGGER.debug( "processing binding operation for: %s %s %s", - "{}: [{}]".format(ATTR_SOURCE_IEEE, source_ieee), - "{}: [{}]".format(ATTR_TARGET_IEEE, target_ieee), + f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", + f"{ATTR_TARGET_IEEE}: [{target_ieee}]", "{}: {}".format("cluster", cluster_pair.source_cluster.cluster_id), ) bind_tasks.append( @@ -526,13 +551,13 @@ def async_load_api(hass): ) _LOGGER.debug( "Set attribute for: %s %s %s %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(ATTR_ATTRIBUTE, attribute), - "{}: [{}]".format(ATTR_VALUE, value), - "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), - "{}: [{}]".format(RESPONSE, response), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{ATTR_ATTRIBUTE}: [{attribute}]", + f"{ATTR_VALUE}: [{value}]", + f"{ATTR_MANUFACTURER}: [{manufacturer}]", + f"{RESPONSE}: [{response}]", ) hass.helpers.service.async_register_admin_service( @@ -568,14 +593,14 @@ def async_load_api(hass): ) _LOGGER.debug( "Issue command for: %s %s %s %s %s %s %s %s", - "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), - "{}: [{}]".format(ATTR_CLUSTER_TYPE, cluster_type), - "{}: [{}]".format(ATTR_ENDPOINT_ID, endpoint_id), - "{}: [{}]".format(ATTR_COMMAND, command), - "{}: [{}]".format(ATTR_COMMAND_TYPE, command_type), - "{}: [{}]".format(ATTR_ARGS, args), - "{}: [{}]".format(ATTR_MANUFACTURER, manufacturer), - "{}: [{}]".format(RESPONSE, response), + f"{ATTR_CLUSTER_ID}: [{cluster_id}]", + f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", + f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", + f"{ATTR_COMMAND}: [{command}]", + f"{ATTR_COMMAND_TYPE}: [{command_type}]", + f"{ATTR_ARGS}: [{args}]", + f"{ATTR_MANUFACTURER}: [{manufacturer}]", + f"{RESPONSE}: [{response}]", ) hass.helpers.service.async_register_admin_service( @@ -587,6 +612,7 @@ def async_load_api(hass): websocket_api.async_register_command(hass, websocket_permit_devices) websocket_api.async_register_command(hass, websocket_get_devices) + websocket_api.async_register_command(hass, websocket_get_device) websocket_api.async_register_command(hass, websocket_reconfigure_node) websocket_api.async_register_command(hass, websocket_device_clusters) websocket_api.async_register_command(hass, websocket_device_cluster_attributes) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 20756f26b72..aed12bc65a5 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -87,7 +87,7 @@ class ZigbeeChannel(LogMixin): self._channel_name = cluster.ep_attribute if self.CHANNEL_NAME: self._channel_name = self.CHANNEL_NAME - self._generic_id = "channel_0x{:04x}".format(cluster.cluster_id) + self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._cluster = cluster self._zha_device = device self._unique_id = "{}:{}:0x{:04x}".format( @@ -202,7 +202,7 @@ class ZigbeeChannel(LogMixin): # Xiaomi devices don't need this and it disrupts pairing if self._zha_device.manufacturer != "LUMI": await self.bind() - if self.cluster.cluster_id not in self.cluster.endpoint.out_clusters: + if self.cluster.is_server: for report_config in self._report_config: await self.configure_reporting( report_config["attr"], report_config["config"] @@ -299,9 +299,7 @@ class AttributeListeningChannel(ZigbeeChannel): """Handle attribute updates on this cluster.""" if attrid == self.value_attribute: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 0559c4a1f76..378be778e6f 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -30,9 +30,7 @@ class DoorLockChannel(ZigbeeChannel): result = await self.get_attribute_value("lock_state", from_cache=True) async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - result, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", result ) @callback @@ -44,9 +42,7 @@ class DoorLockChannel(ZigbeeChannel): ) if attrid == self._value_attribute: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 6a828ef1ad8..f67ee2fb75a 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -198,7 +198,7 @@ class LevelControlChannel(ZigbeeChannel): def dispatch_level_change(self, command, level): """Dispatch level change.""" async_dispatcher_send( - self._zha_device.hass, "{}_{}".format(self.unique_id, command), level + self._zha_device.hass, f"{self.unique_id}_{command}", level ) async def async_initialize(self, from_cache): @@ -284,9 +284,7 @@ class OnOffChannel(ZigbeeChannel): """Handle attribute updates on this cluster.""" if attrid == self.ON_OFF: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) self._state = bool(value) @@ -355,9 +353,7 @@ class PowerConfigurationChannel(ZigbeeChannel): attr_id = attr if attrid == attr_id: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index 198eec67a46..7a5f0161fb4 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -72,9 +72,7 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): # This is a polling channel. Don't allow cache. result = await self.get_attribute_value("active_power", from_cache=False) async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - result, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", result ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 46d9ffb52e5..2f6e6c1b3e8 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -48,9 +48,7 @@ class FanChannel(ZigbeeChannel): result = await self.get_attribute_value("fan_mode", from_cache=True) async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - result, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", result ) @callback @@ -62,9 +60,7 @@ class FanChannel(ZigbeeChannel): ) if attrid == self._value_attribute: async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 6ed9de9b303..e15acdaf5e3 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -6,9 +6,17 @@ https://home-assistant.io/components/zha/ """ import logging -from . import AttributeListeningChannel +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from . import AttributeListeningChannel, ZigbeeChannel from .. import registries -from ..const import REPORT_CONFIG_ASAP, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT +from ..const import ( + REPORT_CONFIG_ASAP, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, + SIGNAL_ATTR_UPDATED, +) _LOGGER = logging.getLogger(__name__) @@ -26,6 +34,14 @@ class SmartThingsHumidity(AttributeListeningChannel): ] +@registries.CHANNEL_ONLY_CLUSTERS.register(0xFD00) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFD00) +class OsramButton(ZigbeeChannel): + """Osram button channel.""" + + REPORT_CONFIG = [] + + @registries.ZIGBEE_CHANNEL_REGISTRY.register( registries.SMARTTHINGS_ACCELERATION_CLUSTER ) @@ -38,3 +54,23 @@ class SmartThingsAcceleration(AttributeListeningChannel): {"attr": "y_axis", "config": REPORT_CONFIG_ASAP}, {"attr": "z_axis", "config": REPORT_CONFIG_ASAP}, ] + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + if attrid == self.value_attribute: + async_dispatcher_send( + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value + ) + else: + self.zha_send_event( + self._cluster, + SIGNAL_ATTR_UPDATED, + { + "attribute_id": attrid, + "attribute_name": self._cluster.attributes.get(attrid, ["Unknown"])[ + 0 + ], + "value": value, + }, + ) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index cac93ea7214..cd407cfc416 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -43,9 +43,7 @@ class IASZoneChannel(ZigbeeChannel): if command_id == 0: state = args[0] & 3 async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - state, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", state ) self.debug("Updated alarm state: %s", state) elif command_id == 1: @@ -91,9 +89,7 @@ class IASZoneChannel(ZigbeeChannel): if attrid == 2: value = value & 3 async_dispatcher_send( - self._zha_device.hass, - "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED), - value, + self._zha_device.hass, f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", value ) async def async_initialize(self, from_cache): diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index a182193caba..8e2fa7e3d5a 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -8,6 +8,8 @@ import logging import zigpy.zcl.clusters.smartenergy as smartenergy +from homeassistant.core import callback + from .. import registries from ..channels import AttributeListeningChannel, ZigbeeChannel from ..const import REPORT_CONFIG_DEFAULT @@ -77,6 +79,87 @@ class Metering(AttributeListeningChannel): REPORT_CONFIG = [{"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT}] + unit_of_measure_map = { + 0x00: "kW", + 0x01: "m³/h", + 0x02: "ft³/h", + 0x03: "ccf/h", + 0x04: "US gal/h", + 0x05: "IMP gal/h", + 0x06: "BTU/h", + 0x07: "l/h", + 0x08: "kPa", + 0x09: "kPa", + 0x0A: "mcf/h", + 0x0B: "unitless", + 0x0C: "MJ/s", + } + + def __init__(self, cluster, device): + """Initialize Metering.""" + super().__init__(cluster, device) + self._divisor = None + self._multiplier = None + self._unit_enum = None + self._format_spec = None + + async def async_configure(self): + """Configure channel.""" + await self.fetch_config(False) + await super().async_configure() + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.fetch_config(True) + await super().async_initialize(from_cache) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute update from Metering cluster.""" + super().attribute_updated(attrid, value * self._multiplier / self._divisor) + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return self.unit_of_measure_map.get(self._unit_enum & 0x7F, "unknown") + + async def fetch_config(self, from_cache): + """Fetch config from device and updates format specifier.""" + self._divisor = await self.get_attribute_value("divisor", from_cache=from_cache) + self._multiplier = await self.get_attribute_value( + "multiplier", from_cache=from_cache + ) + self._unit_enum = await self.get_attribute_value( + "unit_of_measure", from_cache=from_cache + ) + fmting = await self.get_attribute_value( + "demand_formatting", from_cache=from_cache + ) + + if self._divisor is None or self._divisor == 0: + self._divisor = 1 + if self._multiplier is None or self._multiplier == 0: + self._multiplier = 1 + if self._unit_enum is None: + self._unit_enum = 0x7F # unknown + if fmting is None: + fmting = 0xF9 # 1 digit to the right, 15 digits to the left + + r_digits = fmting & 0x07 # digits to the right of decimal point + l_digits = (fmting >> 3) & 0x0F # digits to the left of decimal point + if l_digits == 0: + l_digits = 15 + width = r_digits + l_digits + (1 if r_digits > 0 else 0) + + if fmting & 0x80: + self._format_spec = "{:" + str(width) + "." + str(r_digits) + "f}" + else: + self._format_spec = "{:0" + str(width) + "." + str(r_digits) + "f}" + + def formatter_function(self, value): + """Return formatted value for display.""" + return self._format_spec.format(value).lstrip() + @registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id) class Prepayment(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 1c22b41ce86..1db4aafeeb9 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -102,7 +102,7 @@ class ZHADevice(LogMixin): @property def name(self): """Return device name.""" - return "{} {}".format(self.manufacturer, self.model) + return f"{self.manufacturer} {self.model}" @property def ieee(self): @@ -461,10 +461,10 @@ class ZHADevice(LogMixin): except DeliveryError as exc: self.debug( "failed to set attribute: %s %s %s %s %s", - "{}: {}".format(ATTR_VALUE, value), - "{}: {}".format(ATTR_ATTRIBUTE, attribute), - "{}: {}".format(ATTR_CLUSTER_ID, cluster_id), - "{}: {}".format(ATTR_ENDPOINT_ID, endpoint_id), + f"{ATTR_VALUE}: {value}", + f"{ATTR_ATTRIBUTE}: {attribute}", + f"{ATTR_CLUSTER_ID}: {cluster_id}", + f"{ATTR_ENDPOINT_ID}: {endpoint_id}", exc, ) return None @@ -493,13 +493,13 @@ class ZHADevice(LogMixin): self.debug( "Issued cluster command: %s %s %s %s %s %s %s", - "{}: {}".format(ATTR_CLUSTER_ID, cluster_id), - "{}: {}".format(ATTR_COMMAND, command), - "{}: {}".format(ATTR_COMMAND_TYPE, command_type), - "{}: {}".format(ATTR_ARGS, args), - "{}: {}".format(ATTR_CLUSTER_ID, cluster_type), - "{}: {}".format(ATTR_MANUFACTURER, manufacturer), - "{}: {}".format(ATTR_ENDPOINT_ID, endpoint_id), + f"{ATTR_CLUSTER_ID}: {cluster_id}", + f"{ATTR_COMMAND}: {command}", + f"{ATTR_COMMAND_TYPE}: {command_type}", + f"{ATTR_ARGS}: {args}", + f"{ATTR_CLUSTER_ID}: {cluster_type}", + f"{ATTR_MANUFACTURER}: {manufacturer}", + f"{ATTR_ENDPOINT_ID}: {endpoint_id}", ) return response diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index c4489164b0c..5a5ffb34ab1 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -62,7 +62,7 @@ def async_process_endpoint( component = None profile_clusters = [] - device_key = "{}-{}".format(device.ieee, endpoint_id) + device_key = f"{device.ieee}-{endpoint_id}" node_config = {} if CONF_DEVICE_CONFIG in config: node_config = config[CONF_DEVICE_CONFIG].get(device_key, {}) @@ -281,12 +281,12 @@ def _async_handle_single_cluster_match( channels = [] _async_create_cluster_channel(cluster, zha_device, is_new_join, channels=channels) - cluster_key = "{}-{}".format(device_key, cluster.cluster_id) + cluster_key = f"{device_key}-{cluster.cluster_id}" discovery_info = { "unique_id": cluster_key, "zha_device": zha_device, "channels": channels, - "entity_suffix": "_{}".format(cluster.cluster_id), + "entity_suffix": f"_{cluster.cluster_id}", "component": component, } diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 3d8c3e8fd90..be09312f693 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -339,7 +339,7 @@ class ZHAGateway: _LOGGER.debug( "device - %s entering async_device_initialized - is_new_join: %s", - "0x{:04x}:{}".format(device.nwk, device.ieee), + f"0x{device.nwk:04x}:{device.ieee}", zha_device.status is not DeviceStatus.INITIALIZED, ) @@ -348,13 +348,13 @@ class ZHAGateway: # new nwk or device was physically reset and added again without being removed _LOGGER.debug( "device - %s has been reset and readded or its nwk address changed", - "0x{:04x}:{}".format(device.nwk, device.ieee), + f"0x{device.nwk:04x}:{device.ieee}", ) await self._async_device_rejoined(zha_device) else: _LOGGER.debug( "device - %s has joined the ZHA zigbee network", - "0x{:04x}:{}".format(device.nwk, device.ieee), + f"0x{device.nwk:04x}:{device.ieee}", ) await self._async_device_joined(device, zha_device) @@ -413,9 +413,9 @@ class ZHAGateway: # to update it now _LOGGER.debug( "attempting to request fresh state for device - %s %s %s", - "0x{:04x}:{}".format(zha_device.nwk, zha_device.ieee), + f"0x{zha_device.nwk:04x}:{zha_device.ieee}", zha_device.name, - "with power source: {}".format(zha_device.power_source), + f"with power source: {zha_device.power_source}", ) await zha_device.async_initialize(from_cache=False) else: @@ -427,7 +427,7 @@ class ZHAGateway: async def _async_device_rejoined(self, zha_device): _LOGGER.debug( "skipping discovery for previously discovered device - %s", - "0x{:04x}:{}".format(zha_device.nwk, zha_device.ieee), + f"0x{zha_device.nwk:04x}:{zha_device.ieee}", ) # we don't have to do this on a nwk swap but we don't have a way to tell currently await zha_device.async_configure() diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 85b4261e4ec..cea38517767 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -2,7 +2,7 @@ # pylint: disable=W0611 from collections import OrderedDict import logging -from typing import MutableMapping # noqa: F401 +from typing import MutableMapping from typing import cast import attr @@ -35,7 +35,7 @@ class ZhaDeviceStorage: def __init__(self, hass: HomeAssistantType) -> None: """Initialize the zha device storage.""" self.hass = hass - self.devices = {} # type: MutableMapping[str, ZhaDeviceEntry] + self.devices: MutableMapping[str, ZhaDeviceEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback @@ -88,7 +88,7 @@ class ZhaDeviceStorage: """Load the registry of zha device entries.""" data = await self._store.async_load() - devices = OrderedDict() # type: OrderedDict[str, ZhaDeviceEntry] + devices: "OrderedDict[str, ZhaDeviceEntry]" = OrderedDict() if data is not None: for device in data["devices"]: diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 694f7b25695..00c3942358e 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -189,7 +189,7 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): unsub = async_dispatcher_connect(self.hass, signal, func) else: unsub = async_dispatcher_connect( - self.hass, "{}_{}".format(channel.unique_id, signal), func + self.hass, f"{channel.unique_id}_{signal}", func ) self._unsubs.append(unsub) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 8e7de41e626..3095d140619 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,11 +5,11 @@ "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ "bellows-homeassistant==0.9.1", - "zha-quirks==0.0.22", - "zigpy-deconz==0.2.2", - "zigpy-homeassistant==0.7.1", + "zha-quirks==0.0.23", + "zigpy-deconz==0.3.0", + "zigpy-homeassistant==0.8.0", "zigpy-xbee-homeassistant==0.4.0", - "zigpy-zigate==0.1.0" + "zigpy-zigate==0.2.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index e38acebb22c..b260dfc5459 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -136,7 +136,6 @@ UNIT_REGISTRY = { SENSOR_TEMPERATURE: TEMP_CELSIUS, SENSOR_PRESSURE: "hPa", SENSOR_ILLUMINANCE: "lx", - SENSOR_METERING: POWER_WATT, SENSOR_ELECTRICAL_MEASUREMENT: POWER_WATT, SENSOR_GENERIC: None, SENSOR_BATTERY: "%", @@ -219,15 +218,19 @@ class Sensor(ZhaEntity): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._sensor_type = kwargs.get(SENSOR_TYPE, SENSOR_GENERIC) - self._unit = UNIT_REGISTRY.get(self._sensor_type) - self._formatter_function = FORMATTER_FUNC_REGISTRY.get( - self._sensor_type, pass_through_formatter - ) - self._force_update = FORCE_UPDATE_REGISTRY.get(self._sensor_type, False) - self._should_poll = POLLING_REGISTRY.get(self._sensor_type, False) self._channel = self.cluster_channels.get( CHANNEL_REGISTRY.get(self._sensor_type, CHANNEL_ATTRIBUTE) ) + if self._sensor_type == SENSOR_METERING: + self._unit = self._channel.unit_of_measurement + self._formatter_function = self._channel.formatter_function + else: + self._unit = UNIT_REGISTRY.get(self._sensor_type) + self._formatter_function = FORMATTER_FUNC_REGISTRY.get( + self._sensor_type, pass_through_formatter + ) + self._force_update = FORCE_UPDATE_REGISTRY.get(self._sensor_type, False) + self._should_poll = POLLING_REGISTRY.get(self._sensor_type, False) self._device_class = DEVICE_CLASS_REGISTRY.get(self._sensor_type, None) self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get( self._sensor_type, None @@ -271,7 +274,10 @@ class Sensor(ZhaEntity): # this is necessary because HA saves the unit based on what shows in # the UI and not based on what the sensor has configured so we need # to flip it back after state restoration - self._unit = UNIT_REGISTRY.get(self._sensor_type) + if self._sensor_type == SENSOR_METERING: + self._unit = self._channel.unit_of_measurement + else: + self._unit = UNIT_REGISTRY.get(self._sensor_type) self._state = self._formatter_function(state) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 048054077f8..ffd5aa21472 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -18,19 +18,19 @@ remove: example: "00:0d:6f:00:05:7d:2d:34" reconfigure_device: - description: >- - Reconfigure ZHA device (heal device). Use this if you are having issues + description: >- + Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this - service. + service. fields: ieee_address: description: IEEE address of the device to reconfigure example: "00:0d:6f:00:05:7d:2d:34" set_zigbee_cluster_attribute: - description: >- - Set attribute value for the specified cluster on the specified entity. + description: >- + Set attribute value for the specified cluster on the specified entity. fields: ieee: description: IEEE address for the device @@ -55,8 +55,8 @@ set_zigbee_cluster_attribute: example: 0x00FC issue_zigbee_cluster_command: - description: >- - Issue command on the specified cluster on the specified entity. + description: >- + Issue command on the specified cluster on the specified entity. fields: ieee: description: IEEE address for the device diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index 8514ec711cb..f1a363cfede 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -10,6 +10,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -46,7 +47,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY] +SUPPORT_HVAC = [ + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, +] + +ZHONG_HONG_MODE_COOL = "cool" +ZHONG_HONG_MODE_HEAT = "heat" +ZHONG_HONG_MODE_DRY = "dry" +ZHONG_HONG_MODE_FAN_ONLY = "fan_only" + + +MODE_TO_STATE = { + ZHONG_HONG_MODE_COOL: HVAC_MODE_COOL, + ZHONG_HONG_MODE_HEAT: HVAC_MODE_HEAT, + ZHONG_HONG_MODE_DRY: HVAC_MODE_DRY, + ZHONG_HONG_MODE_FAN_ONLY: HVAC_MODE_FAN_ONLY, +} def setup_platform(hass, config, add_entities, discovery_info=None): @@ -117,7 +137,9 @@ class ZhongHongClimate(ClimateDevice): """Handle state update.""" _LOGGER.debug("async update ha state") if self._device.current_operation: - self._current_operation = self._device.current_operation.lower() + self._current_operation = MODE_TO_STATE[ + self._device.current_operation.lower() + ] if self._device.current_temperature: self._current_temperature = self._device.current_temperature if self._device.current_fan_mode: @@ -156,7 +178,9 @@ class ZhongHongClimate(ClimateDevice): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" - return self._current_operation + if self.is_on: + return self._current_operation + return HVAC_MODE_OFF @property def hvac_modes(self): @@ -223,6 +247,14 @@ class ZhongHongClimate(ClimateDevice): def set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + if self.is_on: + self.turn_off() + return + + if not self.is_on: + self.turn_on() + self._device.set_operation_mode(hvac_mode.upper()) def set_fan_mode(self, fan_mode): diff --git a/homeassistant/components/ziggo_mediabox_xl/media_player.py b/homeassistant/components/ziggo_mediabox_xl/media_player.py index f9e4e1ac49d..a5f8b38ac37 100644 --- a/homeassistant/components/ziggo_mediabox_xl/media_player.py +++ b/homeassistant/components/ziggo_mediabox_xl/media_player.py @@ -206,5 +206,5 @@ class ZiggoMediaboxXLDevice(MediaPlayerDevice): if digits is None: return - self.send_keys(["NUM_{}".format(digit) for digit in str(digits)]) + self.send_keys([f"NUM_{digit}" for digit in str(digits)]) self._state = STATE_PLAYING diff --git a/homeassistant/components/zoneminder/__init__.py b/homeassistant/components/zoneminder/__init__.py index 1ce6b87a88f..a116cc31891 100644 --- a/homeassistant/components/zoneminder/__init__.py +++ b/homeassistant/components/zoneminder/__init__.py @@ -64,7 +64,7 @@ def setup(hass, config): schema = "http" host_name = conf[CONF_HOST] - server_origin = "{}://{}".format(schema, host_name) + server_origin = f"{schema}://{host_name}" zm_client = ZoneMinder( server_origin, conf.get(CONF_USERNAME), diff --git a/homeassistant/components/zoneminder/sensor.py b/homeassistant/components/zoneminder/sensor.py index e2ab4b0905f..bfcfcb8f907 100644 --- a/homeassistant/components/zoneminder/sensor.py +++ b/homeassistant/components/zoneminder/sensor.py @@ -68,7 +68,7 @@ class ZMSensorMonitors(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} Status".format(self._monitor.name) + return f"{self._monitor.name} Status" @property def state(self): @@ -105,7 +105,7 @@ class ZMSensorEvents(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._monitor.name, self.time_period.title) + return f"{self._monitor.name} {self.time_period.title}" @property def unit_of_measurement(self): diff --git a/homeassistant/components/zoneminder/switch.py b/homeassistant/components/zoneminder/switch.py index d22ef611b35..d2d761aab1e 100644 --- a/homeassistant/components/zoneminder/switch.py +++ b/homeassistant/components/zoneminder/switch.py @@ -53,7 +53,7 @@ class ZMSwitchMonitors(SwitchDevice): @property def name(self): """Return the name of the switch.""" - return "{} State".format(self._monitor.name) + return f"{self._monitor.name} State" def update(self): """Update the switch value.""" diff --git a/homeassistant/components/zwave/.translations/pl.json b/homeassistant/components/zwave/.translations/pl.json index c392f0093a0..254008ddb4c 100644 --- a/homeassistant/components/zwave/.translations/pl.json +++ b/homeassistant/components/zwave/.translations/pl.json @@ -13,7 +13,7 @@ "network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)", "usb_path": "\u015acie\u017cka do kontrolera Z-Wave USB" }, - "description": "Zobacz https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", + "description": "Przejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", "title": "Konfiguracja Z-Wave" } }, diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index bc40d46b8ba..223ce810d7c 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -478,10 +478,10 @@ async def async_setup_entry(hass, config_entry): def node_removed(node): node_id = node.node_id - node_key = "node-{}".format(node_id) + node_key = f"node-{node_id}" _LOGGER.info("Node Removed: %s", hass.data[DATA_DEVICES][node_key]) for key in list(hass.data[DATA_DEVICES]): - if not key.startswith("{}-".format(node_id)): + if not key.startswith(f"{node_id}-"): continue entity = hass.data[DATA_DEVICES][key] @@ -586,11 +586,11 @@ async def async_setup_entry(hass, config_entry): update_ids = service.data.get(const.ATTR_UPDATE_IDS) # We want to rename the device, the node entity, # and all the contained entities - node_key = "node-{}".format(node_id) + node_key = f"node-{node_id}" entity = hass.data[DATA_DEVICES][node_key] await entity.node_renamed(update_ids) for key in list(hass.data[DATA_DEVICES]): - if not key.startswith("{}-".format(node_id)): + if not key.startswith(f"{node_id}-"): continue entity = hass.data[DATA_DEVICES][key] await entity.value_renamed(update_ids) @@ -607,7 +607,7 @@ async def async_setup_entry(hass, config_entry): "Renamed Z-Wave value (Node %d Value %d) to %s", node_id, value_id, name ) update_ids = service.data.get(const.ATTR_UPDATE_IDS) - value_key = "{}-{}".format(node_id, value_id) + value_key = f"{node_id}-{value_id}" entity = hass.data[DATA_DEVICES][value_key] await entity.value_renamed(update_ids) @@ -1109,7 +1109,7 @@ class ZWaveDeviceEntityValues: if polling_intensity: self.primary.enable_poll(polling_intensity) - platform = import_module(".{}".format(component), __name__) + platform = import_module(f".{component}", __name__) device = platform.get_device( node=self._node, values=self, node_config=node_config, hass=self._hass @@ -1149,9 +1149,7 @@ class ZWaveDeviceEntityValues: self._hass.data[DATA_DEVICES][device.unique_id] = device if component in SUPPORTED_PLATFORMS: - async_dispatcher_send( - self._hass, "zwave_new_{}".format(component), device - ) + async_dispatcher_send(self._hass, f"zwave_new_{component}", device) else: await discovery.async_load_platform( self._hass, @@ -1316,4 +1314,4 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): def compute_value_unique_id(node, value): """Compute unique_id a value would get if it were to get one.""" - return "{}-{}".format(node.node_id, value.object_id) + return f"{node.node_id}-{value.object_id}" diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 2c7ce4b18a4..b40fff66958 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -171,6 +171,28 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): def update_properties(self): """Handle the data changes for node values.""" # Operation Mode + self._update_operation_mode() + + # Current Temp + self._update_current_temp() + + # Fan Mode + self._update_fan_mode() + + # Swing mode + self._update_swing_mode() + + # Set point + self._update_target_temp() + + # Operating state + self._update_operating_state() + + # Fan operating state + self._update_fan_state() + + def _update_operation_mode(self): + """Update hvac and preset modes.""" if self.values.mode: self._hvac_list = [] self._hvac_mapping = {} @@ -259,22 +281,27 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("self._preset_list=%s", self._preset_list) _LOGGER.debug("self._preset_mode=%s", self._preset_mode) - # Current Temp + def _update_current_temp(self): + """Update current temperature.""" if self.values.temperature: self._current_temperature = self.values.temperature.data device_unit = self.values.temperature.units if device_unit is not None: self._unit = device_unit - # Fan Mode + def _update_fan_mode(self): + """Update fan mode.""" if self.values.fan_mode: self._current_fan_mode = self.values.fan_mode.data fan_modes = self.values.fan_mode.data_items if fan_modes: self._fan_modes = list(fan_modes) + _LOGGER.debug("self._fan_modes=%s", self._fan_modes) _LOGGER.debug("self._current_fan_mode=%s", self._current_fan_mode) - # Swing mode + + def _update_swing_mode(self): + """Update swing mode.""" if self._zxt_120 == 1: if self.values.zxt_120_swing_mode: self._current_swing_mode = self.values.zxt_120_swing_mode.data @@ -283,7 +310,9 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._swing_modes = list(swing_modes) _LOGGER.debug("self._swing_modes=%s", self._swing_modes) _LOGGER.debug("self._current_swing_mode=%s", self._current_swing_mode) - # Set point + + def _update_target_temp(self): + """Update target temperature.""" if self.values.primary.data == 0: _LOGGER.debug( "Setpoint is 0, setting default to " "current_temperature=%s", @@ -294,12 +323,14 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): else: self._target_temperature = round((float(self.values.primary.data)), 1) - # Operating state + def _update_operating_state(self): + """Update operating state.""" if self.values.operating_state: mode = self.values.operating_state.data self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode) - # Fan operating state + def _update_fan_state(self): + """Update fan state.""" if self.values.fan_action: self._fan_action = self.values.fan_action.data @@ -448,7 +479,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): return if preset_mode == PRESET_NONE: # Activate the current hvac mode - self.update_properties() + self._update_operation_mode() operation_mode = self._hvac_mapping.get(self.hvac_mode) _LOGGER.debug("Set operation_mode to %s", operation_mode) self.values.mode.data = operation_mode diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index c60314d3579..44241e91daf 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -17,6 +17,7 @@ from .const import ( EVENT_NODE_EVENT, EVENT_SCENE_ACTIVATED, COMMAND_CLASS_CENTRAL_SCENE, + COMMAND_CLASS_VERSION, DOMAIN, ) from .util import node_name, is_node_parsed, node_device_id_and_name @@ -30,6 +31,7 @@ ATTR_FAILED = "is_failed" ATTR_PRODUCT_NAME = "product_name" ATTR_MANUFACTURER_NAME = "manufacturer_name" ATTR_NODE_NAME = "node_name" +ATTR_APPLICATION_VERSION = "application_version" STAGE_COMPLETE = "Complete" @@ -130,10 +132,14 @@ class ZWaveNodeEntity(ZWaveBaseEntity): self._product_name = node.product_name self._manufacturer_name = node.manufacturer_name self._unique_id = self._compute_unique_id() + self._application_version = None self._attributes = {} self.wakeup_interval = None self.location = None self.battery_level = None + dispatcher.connect( + self.network_node_value_added, ZWaveNetwork.SIGNAL_VALUE_ADDED + ) dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_VALUE_CHANGED) dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NODE) dispatcher.connect(self.network_node_changed, ZWaveNetwork.SIGNAL_NOTIFICATION) @@ -161,6 +167,24 @@ class ZWaveNodeEntity(ZWaveBaseEntity): info["via_device"] = (DOMAIN, 1) return info + def maybe_update_application_version(self, value): + """Update application version if value is a Command Class Version, Application Value.""" + if ( + value + and value.command_class == COMMAND_CLASS_VERSION + and value.label == "Application Version" + ): + self._application_version = value.data + + def network_node_value_added(self, node=None, value=None, args=None): + """Handle a added value to a none on the network.""" + if node and node.node_id != self.node_id: + return + if args is not None and "nodeId" in args and args["nodeId"] != self.node_id: + return + + self.maybe_update_application_version(value) + def network_node_changed(self, node=None, value=None, args=None): """Handle a changed node on the network.""" if node and node.node_id != self.node_id: @@ -172,6 +196,8 @@ class ZWaveNodeEntity(ZWaveBaseEntity): if value is not None and value.command_class == COMMAND_CLASS_CENTRAL_SCENE: self.central_scene_activated(value.index, value.data) + self.maybe_update_application_version(value) + self.node_changed() def get_node_statistics(self): @@ -343,10 +369,12 @@ class ZWaveNodeEntity(ZWaveBaseEntity): attrs[ATTR_BATTERY_LEVEL] = self.battery_level if self.wakeup_interval is not None: attrs[ATTR_WAKEUP] = self.wakeup_interval + if self._application_version is not None: + attrs[ATTR_APPLICATION_VERSION] = self._application_version return attrs def _compute_unique_id(self): if is_node_parsed(self.node) or self.node.is_ready: - return "node-{}".format(self.node_id) + return f"node-{self.node_id}" return None diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 1e7b77d2b38..da8fa37f44f 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -91,8 +91,8 @@ def check_value_schema(value, schema): def node_name(node): """Return the name of the node.""" if is_node_parsed(node): - return node.name or "{} {}".format(node.manufacturer_name, node.product_name) - return "Unknown Node {}".format(node.node_id) + return node.name or f"{node.manufacturer_name} {node.product_name}" + return f"Unknown Node {node.node_id}" def node_device_id_and_name(node, instance=1): @@ -100,7 +100,7 @@ def node_device_id_and_name(node, instance=1): name = node_name(node) if instance == 1: return ((const.DOMAIN, node.node_id), name) - name = "{} ({})".format(name, instance) + name = f"{name} ({instance})" return ((const.DOMAIN, node.node_id, instance), name) diff --git a/homeassistant/config.py b/homeassistant/config.py index 1f42b3db25e..d3bd97dad8f 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -7,17 +7,7 @@ import logging import os import re import shutil -from typing import ( # noqa: F401 pylint: disable=unused-import - Any, - Tuple, - Optional, - Dict, - List, - Union, - Callable, - Sequence, - Set, -) +from typing import Any, Tuple, Optional, Dict, Union, Callable, Sequence, Set from types import ModuleType import voluptuous as vol from voluptuous.humanize import humanize_error @@ -118,7 +108,7 @@ def _no_duplicate_auth_provider( Each type of auth provider can only have one config without optional id. Unique id is required if same type of auth provider used multiple times. """ - config_keys = set() # type: Set[Tuple[str, Optional[str]]] + config_keys: Set[Tuple[str, Optional[str]]] = set() for config in configs: key = (config[CONF_TYPE], config.get(CONF_ID)) if key in config_keys: @@ -142,7 +132,7 @@ def _no_duplicate_auth_mfa_module( times. Note: this is different than auth provider """ - config_keys = set() # type: Set[str] + config_keys: Set[str] = set() for config in configs: key = config.get(CONF_ID, config[CONF_TYPE]) if key in config_keys: @@ -299,7 +289,7 @@ def _write_default_config(config_dir: str) -> Optional[str]: return config_path - except IOError: + except OSError: print("Unable to create default configuration file", config_path) return None @@ -317,7 +307,7 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: path = find_config_file(hass.config.config_dir) if path is None: raise HomeAssistantError( - "Config file not found in: {}".format(hass.config.config_dir) + f"Config file not found in: {hass.config.config_dir}" ) config = load_yaml_config_file(path) return config @@ -403,7 +393,7 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: try: with open(config_path, "wt", encoding="utf-8") as config_file: config_file.write(config_raw) - except IOError: + except OSError: _LOGGER.exception("Migrating to google_translate tts failed") pass @@ -443,7 +433,7 @@ def _format_config_error(ex: vol.Invalid, domain: str, config: Dict) -> str: This method must be run in the event loop. """ - message = "Invalid config for [{}]: ".format(domain) + message = f"Invalid config for [{domain}]: " if "extra keys not allowed" in ex.error_message: message += ( "[{option}] is an invalid option for [{domain}]. " @@ -623,7 +613,7 @@ def _identify_config_schema(module: ModuleType) -> Tuple[Optional[str], Optional def _recursive_merge(conf: Dict[str, Any], package: Dict[str, Any]) -> Union[bool, str]: """Merge package into conf, recursively.""" - error = False # type: Union[bool, str] + error: Union[bool, str] = False for key, pack_conf in package.items(): if isinstance(pack_conf, dict): if not pack_conf: @@ -705,7 +695,7 @@ async def merge_packages_config( error = _recursive_merge(conf=config[comp_name], package=comp_conf) if error: _log_pkg_error( - pack_name, comp_name, config, "has duplicate key '{}'".format(error) + pack_name, comp_name, config, f"has duplicate key '{error}'" ) return config @@ -777,7 +767,7 @@ async def async_process_component_config( p_config ) except vol.Invalid as ex: - async_log_exception(ex, "{}.{}".format(domain, p_name), p_config, hass) + async_log_exception(ex, f"{domain}.{p_name}", p_config, hass) continue platforms.append(p_validated) @@ -836,7 +826,7 @@ def async_notify_setup_error( else: part = name - message += " - {}\n".format(part) + message += f" - {part}\n" message += "\nPlease check your config." diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index c2da37943c1..8a40cff1bd5 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -138,10 +138,10 @@ class ConfigEntry: self.state = state # Listeners to call on update - self.update_listeners = [] # type: list + self.update_listeners: List = [] # Function to cancel a scheduled retry - self._async_cancel_retry_setup = None # type: Optional[Callable[[], Any]] + self._async_cancel_retry_setup: Optional[Callable[[], Any]] = None async def async_setup( self, @@ -386,14 +386,14 @@ class ConfigEntries: ) self.options = OptionsFlowManager(hass) self._hass_config = hass_config - self._entries = [] # type: List[ConfigEntry] + self._entries: List[ConfigEntry] = [] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) EntityRegistryDisabledHandler(hass).async_setup() @callback def async_domains(self) -> List[str]: """Return domains for which we have entries.""" - seen = set() # type: Set[str] + seen: Set[str] = set() result = [] for entry in self._entries: diff --git a/homeassistant/const.py b/homeassistant/const.py index 81870cf924d..7013242676d 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 98 -PATCH_VERSION = "5" +MINOR_VERSION = 99 +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) @@ -260,8 +260,8 @@ ATTR_ICON = "icon" # The unit of measurement if applicable ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" -CONF_UNIT_SYSTEM_METRIC = "metric" # type: str -CONF_UNIT_SYSTEM_IMPERIAL = "imperial" # type: str +CONF_UNIT_SYSTEM_METRIC: str = "metric" +CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" # Electrical attributes ATTR_VOLTAGE = "voltage" @@ -334,39 +334,39 @@ TEMP_CELSIUS = "°C" TEMP_FAHRENHEIT = "°F" # Length units -LENGTH_CENTIMETERS = "cm" # type: str -LENGTH_METERS = "m" # type: str -LENGTH_KILOMETERS = "km" # type: str +LENGTH_CENTIMETERS: str = "cm" +LENGTH_METERS: str = "m" +LENGTH_KILOMETERS: str = "km" -LENGTH_INCHES = "in" # type: str -LENGTH_FEET = "ft" # type: str -LENGTH_YARD = "yd" # type: str -LENGTH_MILES = "mi" # type: str +LENGTH_INCHES: str = "in" +LENGTH_FEET: str = "ft" +LENGTH_YARD: str = "yd" +LENGTH_MILES: str = "mi" # Pressure units -PRESSURE_PA = "Pa" # type: str -PRESSURE_HPA = "hPa" # type: str -PRESSURE_BAR = "bar" # type: str -PRESSURE_MBAR = "mbar" # type: str -PRESSURE_INHG = "inHg" # type: str -PRESSURE_PSI = "psi" # type: str +PRESSURE_PA: str = "Pa" +PRESSURE_HPA: str = "hPa" +PRESSURE_BAR: str = "bar" +PRESSURE_MBAR: str = "mbar" +PRESSURE_INHG: str = "inHg" +PRESSURE_PSI: str = "psi" # Volume units -VOLUME_LITERS = "L" # type: str -VOLUME_MILLILITERS = "mL" # type: str +VOLUME_LITERS: str = "L" +VOLUME_MILLILITERS: str = "mL" -VOLUME_GALLONS = "gal" # type: str -VOLUME_FLUID_OUNCE = "fl. oz." # type: str +VOLUME_GALLONS: str = "gal" +VOLUME_FLUID_OUNCE: str = "fl. oz." # Mass units -MASS_GRAMS = "g" # type: str -MASS_KILOGRAMS = "kg" # type: str +MASS_GRAMS: str = "g" +MASS_KILOGRAMS: str = "kg" -MASS_OUNCES = "oz" # type: str -MASS_POUNDS = "lb" # type: str +MASS_OUNCES: str = "oz" +MASS_POUNDS: str = "lb" # UV Index units -UNIT_UV_INDEX = "UV index" # type: str +UNIT_UV_INDEX: str = "UV index" # #### SERVICES #### SERVICE_HOMEASSISTANT_STOP = "stop" @@ -460,15 +460,15 @@ CONTENT_TYPE_TEXT_PLAIN = "text/plain" # The exit code to send to request a restart RESTART_EXIT_CODE = 100 -UNIT_NOT_RECOGNIZED_TEMPLATE = "{} is not a recognized {} unit." # type: str +UNIT_NOT_RECOGNIZED_TEMPLATE: str = "{} is not a recognized {} unit." -LENGTH = "length" # type: str -MASS = "mass" # type: str -PRESSURE = "pressure" # type: str -VOLUME = "volume" # type: str -TEMPERATURE = "temperature" # type: str -SPEED_MS = "speed_ms" # type: str -ILLUMINANCE = "illuminance" # type: str +LENGTH: str = "length" +MASS: str = "mass" +PRESSURE: str = "pressure" +VOLUME: str = "volume" +TEMPERATURE: str = "temperature" +SPEED_MS: str = "speed_ms" +ILLUMINANCE: str = "illuminance" WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] diff --git a/homeassistant/core.py b/homeassistant/core.py index e8e33a0479e..c29d41ace9a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -17,7 +17,7 @@ from time import monotonic import uuid from types import MappingProxyType -from typing import ( # noqa: F401 pylint: disable=unused-import +from typing import ( Optional, Any, Callable, @@ -28,7 +28,6 @@ from typing import ( # noqa: F401 pylint: disable=unused-import Set, TYPE_CHECKING, Awaitable, - Iterator, ) from async_timeout import timeout @@ -170,10 +169,10 @@ class HomeAssistant: """Initialize new Home Assistant object.""" self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop()) - executor_opts = { + executor_opts: Dict[str, Any] = { "max_workers": None, "thread_name_prefix": "SyncWorker", - } # type: Dict[str, Any] + } self.executor = ThreadPoolExecutor(**executor_opts) self.loop.set_default_executor(self.executor) @@ -733,7 +732,7 @@ class State: ) self.entity_id = entity_id.lower() - self.state = state # type: str + self.state: str = state self.attributes = MappingProxyType(attributes or {}) self.last_updated = last_updated or dt_util.utcnow() self.last_changed = last_changed or self.last_updated @@ -836,7 +835,7 @@ class StateMachine: def __init__(self, bus: EventBus, loop: asyncio.events.AbstractEventLoop) -> None: """Initialize state machine.""" - self._states = {} # type: Dict[str, State] + self._states: Dict[str, State] = {} self._bus = bus self._loop = loop @@ -1050,7 +1049,7 @@ class ServiceRegistry: def __init__(self, hass: HomeAssistant) -> None: """Initialize a service registry.""" - self._services = {} # type: Dict[str, Dict[str, Service]] + self._services: Dict[str, Dict[str, Service]] = {} self._hass = hass @property @@ -1269,29 +1268,29 @@ class Config: """Initialize a new config object.""" self.hass = hass - self.latitude = 0 # type: float - self.longitude = 0 # type: float - self.elevation = 0 # type: int - self.location_name = "Home" # type: str - self.time_zone = dt_util.UTC # type: datetime.tzinfo - self.units = METRIC_SYSTEM # type: UnitSystem + self.latitude: float = 0 + self.longitude: float = 0 + self.elevation: int = 0 + self.location_name: str = "Home" + self.time_zone: datetime.tzinfo = dt_util.UTC + self.units: UnitSystem = METRIC_SYSTEM - self.config_source = "default" # type: str + self.config_source: str = "default" # If True, pip install is skipped for requirements on startup - self.skip_pip = False # type: bool + self.skip_pip: bool = False # List of loaded components - self.components = set() # type: set + self.components: set = set() # API (HTTP) server configuration, see components.http.ApiConfig - self.api = None # type: Optional[Any] + self.api: Optional[Any] = None # Directory that holds the configuration - self.config_dir = None # type: Optional[str] + self.config_dir: Optional[str] = None # List of allowed external dirs to access - self.whitelist_external_dirs = set() # type: Set[str] + self.whitelist_external_dirs: Set[str] = set() def distance(self, lat: float, lon: float) -> Optional[float]: """Calculate distance from Home Assistant. @@ -1365,7 +1364,7 @@ class Config: self.time_zone = time_zone dt_util.set_default_time_zone(time_zone) else: - raise ValueError("Received invalid time zone {}".format(time_zone_str)) + raise ValueError(f"Received invalid time zone {time_zone_str}") @callback def _update( diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 0af6677dceb..3b128646219 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,13 +1,6 @@ """Classes to help gather user submissions.""" import logging -from typing import ( - Dict, - Any, - Callable, - Hashable, - List, - Optional, -) # noqa pylint: disable=unused-import +from typing import Dict, Any, Callable, Hashable, List, Optional import uuid import voluptuous as vol from .core import callback, HomeAssistant @@ -52,7 +45,7 @@ class FlowManager: ) -> None: """Initialize the flow manager.""" self.hass = hass - self._progress = {} # type: Dict[str, Any] + self._progress: Dict[str, Any] = {} self._async_create_flow = async_create_flow self._async_finish_flow = async_finish_flow @@ -126,7 +119,7 @@ class FlowManager: self, flow: Any, step_id: str, user_input: Optional[Dict] ) -> Dict: """Handle a step of a flow.""" - method = "async_step_{}".format(step_id) + method = f"async_step_{step_id}" if not hasattr(flow, method): self._progress.pop(flow.flow_id) @@ -136,7 +129,7 @@ class FlowManager: ) ) - result = await getattr(flow, method)(user_input) # type: Dict + result: Dict = await getattr(flow, method)(user_input) if result["type"] not in ( RESULT_TYPE_FORM, diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index dfb001ff0d7..89caf730ad7 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -25,7 +25,7 @@ class TemplateError(HomeAssistantError): def __init__(self, exception: jinja2.TemplateError) -> None: """Init the error.""" - super().__init__("{}: {}".format(exception.__class__.__name__, exception)) + super().__init__(f"{exception.__class__.__name__}: {exception}") class PlatformNotReady(HomeAssistantError): @@ -73,10 +73,10 @@ class ServiceNotFound(HomeAssistantError): def __init__(self, domain: str, service: str) -> None: """Initialize error.""" - super().__init__(self, "Service {}.{} not found".format(domain, service)) + super().__init__(self, f"Service {domain}.{service} not found") self.domain = domain self.service = service def __str__(self) -> str: """Return string representation.""" - return "Unable to find service {}/{}".format(self.domain, self.service) + return f"Unable to find service {self.domain}/{self.service}" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index de665ecf5a6..7f3f5c1f20d 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -3,6 +3,7 @@ To update, run python3 -m script.hassfest """ +# fmt: off FLOWS = [ "adguard", @@ -10,6 +11,7 @@ FLOWS = [ "ambient_station", "axis", "cast", + "cert_expiry", "daikin", "deconz", "dialogflow", @@ -23,12 +25,14 @@ FLOWS = [ "homekit_controller", "homematicip_cloud", "hue", + "iaqualink", "ifttt", "ios", "ipma", "iqvia", "life360", "lifx", + "linky", "locative", "logi_circle", "luftdaten", @@ -47,6 +51,7 @@ FLOWS = [ "simplisafe", "smartthings", "smhi", + "solaredge", "somfy", "sonos", "tellduslive", @@ -61,6 +66,7 @@ FLOWS = [ "velbus", "vesync", "wemo", + "withings", "wwlln", "zha", "zone", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 28df05a872c..6d62c47110b 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -3,6 +3,7 @@ To update, run python3 -m script.hassfest """ +# fmt: off SSDP = { "device_type": {}, diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 09c1712c061..6200e2facb0 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -3,6 +3,7 @@ To update, run python3 -m script.hassfest """ +# fmt: off ZEROCONF = { "_axis-video._tcp.local.": [ diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 89fc9e5488a..7f1579cd2c6 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,9 +1,9 @@ """Helper for aiohttp webclient stuff.""" import asyncio import sys -from ssl import SSLContext # noqa: F401 +from ssl import SSLContext from typing import Any, Awaitable, Optional, cast -from typing import Union # noqa: F401 +from typing import Union import aiohttp from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE @@ -171,7 +171,7 @@ def _async_get_connector( return cast(aiohttp.BaseConnector, hass.data[key]) if verify_ssl: - ssl_context = ssl_util.client_context() # type: Union[bool, SSLContext] + ssl_context: Union[bool, SSLContext] = ssl_util.client_context() else: ssl_context = False diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 04a1858782d..e75b195d386 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -3,7 +3,7 @@ import logging import uuid from asyncio import Event from collections import OrderedDict -from typing import MutableMapping # noqa: F401 +from typing import MutableMapping from typing import Iterable, Optional, cast import attr @@ -36,7 +36,7 @@ class AreaRegistry: def __init__(self, hass: HomeAssistantType) -> None: """Initialize the area registry.""" self.hass = hass - self.areas = {} # type: MutableMapping[str, AreaEntry] + self.areas: MutableMapping[str, AreaEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback @@ -119,7 +119,7 @@ class AreaRegistry: """Load the area registry.""" data = await self._store.async_load() - areas = OrderedDict() # type: OrderedDict[str, AreaEntry] + areas: MutableMapping[str, AreaEntry] = OrderedDict() if data is not None: for area in data["areas"]: diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index bc39d5d5720..4052a94b9de 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -36,7 +36,7 @@ CheckConfigError = namedtuple("CheckConfigError", "message domain config") class HomeAssistantConfig(OrderedDict): """Configuration result with errors attribute.""" - errors = attr.ib(default=attr.Factory(list)) # type: List[CheckConfigError] + errors: List[CheckConfigError] = attr.ib(default=attr.Factory(list)) def add_error(self, message, domain=None, config=None): """Add a single error.""" @@ -62,7 +62,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig message = "Package {} setup failed. Component {} {}".format( package, component, message ) - domain = "homeassistant.packages.{}.{}".format(package, component) + domain = f"homeassistant.packages.{package}.{component}" pack_config = core_config[CONF_PACKAGES].get(package, config) result.add_error(message, domain, pack_config) @@ -77,9 +77,9 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig return result.add_error("File configuration.yaml not found.") config = await hass.async_add_executor_job(load_yaml_config_file, config_path) except FileNotFoundError: - return result.add_error("File not found: {}".format(config_path)) + return result.add_error(f"File not found: {config_path}") except HomeAssistantError as err: - return result.add_error("Error loading {}: {}".format(config_path, err)) + return result.add_error(f"Error loading {config_path}: {err}") finally: yaml_loader.clear_secret_cache() @@ -106,13 +106,13 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig try: integration = await async_get_integration_with_requirements(hass, domain) except (RequirementsNotFound, loader.IntegrationNotFound) as ex: - result.add_error("Component error: {} - {}".format(domain, ex)) + result.add_error(f"Component error: {domain} - {ex}") continue try: component = integration.get_component() except ImportError as ex: - result.add_error("Component error: {} - {}".format(domain, ex)) + result.add_error(f"Component error: {domain} - {ex}") continue config_schema = getattr(component, "CONFIG_SCHEMA", None) @@ -159,7 +159,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig RequirementsNotFound, ImportError, ) as ex: - result.add_error("Platform error {}.{} - {}".format(domain, p_name, ex)) + result.add_error(f"Platform error {domain}.{p_name} - {ex}") continue # Validate platform specific schema @@ -168,7 +168,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig try: p_validated = platform_schema(p_validated) except vol.Invalid as ex: - _comp_error(ex, "{}.{}".format(domain, p_name), p_validated) + _comp_error(ex, f"{domain}.{p_name}", p_validated) continue platforms.append(p_validated) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 40465f83728..133251e779d 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -1,4 +1,5 @@ """Offer reusable conditions.""" +import asyncio from datetime import datetime, timedelta import functools as ft import logging @@ -10,6 +11,9 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.core import HomeAssistant, State from homeassistant.components import zone as zone_cmp +from homeassistant.components.device_automation import ( # noqa: F401 pylint: disable=unused-import + async_device_condition_from_config as async_device_from_config, +) from homeassistant.const import ( ATTR_GPS_ACCURACY, ATTR_LATITUDE, @@ -41,40 +45,9 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" _LOGGER = logging.getLogger(__name__) -# PyLint does not like the use of _threaded_factory -# pylint: disable=invalid-name - -def _threaded_factory( - async_factory: Callable[[ConfigType, bool], Callable[..., bool]] -) -> Callable[[ConfigType, bool], Callable[..., bool]]: - """Create threaded versions of async factories.""" - - @ft.wraps(async_factory) - def factory( - config: ConfigType, config_validation: bool = True - ) -> Callable[..., bool]: - """Threaded factory.""" - async_check = async_factory(config, config_validation) - - def condition_if( - hass: HomeAssistant, variables: TemplateVarsType = None - ) -> bool: - """Validate condition.""" - return cast( - bool, - run_callback_threadsafe( - hass.loop, async_check, hass, variables - ).result(), - ) - - return condition_if - - return factory - - -def async_from_config( - config: ConfigType, config_validation: bool = True +async def async_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True ) -> Callable[..., bool]: """Turn a condition configuration into a method. @@ -95,29 +68,30 @@ def async_from_config( ) ) + # Check for partials to properly determine if coroutine function + check_factory = factory + while isinstance(check_factory, ft.partial): + check_factory = check_factory.func + + if asyncio.iscoroutinefunction(check_factory): + return cast(Callable[..., bool], await factory(hass, config, config_validation)) return cast(Callable[..., bool], factory(config, config_validation)) -from_config = _threaded_factory(async_from_config) - - -def async_and_from_config( - config: ConfigType, config_validation: bool = True +async def async_and_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True ) -> Callable[..., bool]: """Create multi condition matcher using 'AND'.""" if config_validation: config = cv.AND_CONDITION_SCHEMA(config) - checks = None + checks = [ + await async_from_config(hass, entry, False) for entry in config["conditions"] + ] def if_and_condition( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: """Test and condition.""" - nonlocal checks - - if checks is None: - checks = [async_from_config(entry, False) for entry in config["conditions"]] - try: for check in checks: if not check(hass, variables): @@ -131,26 +105,20 @@ def async_and_from_config( return if_and_condition -and_from_config = _threaded_factory(async_and_from_config) - - -def async_or_from_config( - config: ConfigType, config_validation: bool = True +async def async_or_from_config( + hass: HomeAssistant, config: ConfigType, config_validation: bool = True ) -> Callable[..., bool]: """Create multi condition matcher using 'OR'.""" if config_validation: config = cv.OR_CONDITION_SCHEMA(config) - checks = None + checks = [ + await async_from_config(hass, entry, False) for entry in config["conditions"] + ] def if_or_condition( hass: HomeAssistant, variables: TemplateVarsType = None ) -> bool: """Test and condition.""" - nonlocal checks - - if checks is None: - checks = [async_from_config(entry, False) for entry in config["conditions"]] - try: for check in checks: if check(hass, variables): @@ -163,9 +131,6 @@ def async_or_from_config( return if_or_condition -or_from_config = _threaded_factory(async_or_from_config) - - def numeric_state( hass: HomeAssistant, entity: Union[None, str, State], @@ -263,9 +228,6 @@ def async_numeric_state_from_config( return if_numeric_state -numeric_state_from_config = _threaded_factory(async_numeric_state_from_config) - - def state( hass: HomeAssistant, entity: Union[None, str, State], @@ -423,9 +385,6 @@ def async_template_from_config( return template_if -template_from_config = _threaded_factory(async_template_from_config) - - def time( before: Optional[dt_util.dt.time] = None, after: Optional[dt_util.dt.time] = None, diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index db96f4a2d02..e53954a65dd 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -24,10 +24,14 @@ from homeassistant.const import ( CONF_ALIAS, CONF_BELOW, CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, CONF_ENTITY_NAMESPACE, + CONF_FOR, CONF_PLATFORM, CONF_SCAN_INTERVAL, + CONF_STATE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, @@ -746,8 +750,8 @@ STATE_CONDITION_SCHEMA = vol.All( { vol.Required(CONF_CONDITION): "state", vol.Required(CONF_ENTITY_ID): entity_id, - vol.Required("state"): str, - vol.Optional("for"): vol.All(time_period, positive_timedelta), + vol.Required(CONF_STATE): str, + vol.Optional(CONF_FOR): vol.All(time_period, positive_timedelta), # To support use_trigger_value in automation # Deprecated 2016/04/25 vol.Optional("from"): str, @@ -823,7 +827,12 @@ OR_CONDITION_SCHEMA = vol.Schema( } ) -CONDITION_SCHEMA = vol.Any( +DEVICE_CONDITION_SCHEMA = vol.Schema( + {vol.Required(CONF_CONDITION): "device", vol.Required(CONF_DOMAIN): str}, + extra=vol.ALLOW_EXTRA, +) + +CONDITION_SCHEMA: vol.Schema = vol.Any( NUMERIC_STATE_CONDITION_SCHEMA, STATE_CONDITION_SCHEMA, SUN_CONDITION_SCHEMA, @@ -832,7 +841,8 @@ CONDITION_SCHEMA = vol.Any( ZONE_CONDITION_SCHEMA, AND_CONDITION_SCHEMA, OR_CONDITION_SCHEMA, -) # type: vol.Schema + DEVICE_CONDITION_SCHEMA, +) _SCRIPT_DELAY_SCHEMA = vol.Schema( { @@ -852,6 +862,11 @@ _SCRIPT_WAIT_TEMPLATE_SCHEMA = vol.Schema( } ) +DEVICE_ACTION_SCHEMA = vol.Schema( + {vol.Required(CONF_DEVICE_ID): string, vol.Required(CONF_DOMAIN): str}, + extra=vol.ALLOW_EXTRA, +) + SCRIPT_SCHEMA = vol.All( ensure_list, [ @@ -861,6 +876,7 @@ SCRIPT_SCHEMA = vol.All( _SCRIPT_WAIT_TEMPLATE_SCHEMA, EVENT_SCHEMA, CONDITION_SCHEMA, + DEVICE_ACTION_SCHEMA, ) ], ) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 91562b9046d..af8d5589c8a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -91,7 +91,7 @@ class Entity: entity_id = None # type: str # Owning hass instance. Will be set by EntityPlatform - hass = None # type: Optional[HomeAssistant] + hass: Optional[HomeAssistant] = None # Owning platform instance. Will be set by EntityPlatform platform = None @@ -109,10 +109,10 @@ class Entity: parallel_updates = None # Entry in the entity registry - registry_entry = None # type: Optional[RegistryEntry] + registry_entry: Optional[RegistryEntry] = None # Hold list for functions to call on remove. - _on_remove = None # type: Optional[List[CALLBACK_TYPE]] + _on_remove: Optional[List[CALLBACK_TYPE]] = None # Context _context = None @@ -248,11 +248,11 @@ class Entity: This method must be run in the event loop. """ if self.hass is None: - raise RuntimeError("Attribute hass is None for {}".format(self)) + raise RuntimeError(f"Attribute hass is None for {self}") if self.entity_id is None: raise NoEntitySpecifiedError( - "No entity id specified for entity {}".format(self.name) + f"No entity id specified for entity {self.name}" ) # update entity data @@ -269,11 +269,11 @@ class Entity: def async_write_ha_state(self): """Write the state to the state machine.""" if self.hass is None: - raise RuntimeError("Attribute hass is None for {}".format(self)) + raise RuntimeError(f"Attribute hass is None for {self}") if self.entity_id is None: raise NoEntitySpecifiedError( - "No entity id specified for entity {}".format(self.name) + f"No entity id specified for entity {self.name}" ) self._async_write_ha_state() diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index b28beeaea72..42b19da889e 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -15,6 +15,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery +from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.service import async_extract_entity_ids from homeassistant.loader import bind_hass, async_get_integration from homeassistant.util import slugify @@ -202,10 +203,12 @@ class EntityComponent: @callback def async_register_entity_service(self, name, schema, func, required_features=None): """Register an entity service.""" + if isinstance(schema, dict): + schema = ENTITY_SERVICE_SCHEMA.extend(schema) async def handle_service(call): """Handle the service.""" - service_name = "{}.{}".format(self.domain, name) + service_name = f"{self.domain}.{name}" await self.hass.helpers.service.entity_service_call( self._platforms.values(), func, call, service_name, required_features ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 4a6a3038fd0..7d5debd484d 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -133,7 +133,7 @@ class EntityPlatform: current_platform.set(self) logger = self.logger hass = self.hass - full_name = "{}.{}".format(self.domain, self.platform_name) + full_name = f"{self.domain}.{self.platform_name}" logger.info("Setting up %s", full_name) warn_task = hass.loop.call_later( @@ -357,7 +357,7 @@ class EntityPlatform: "Not adding entity %s because it's disabled", entry.name or entity.name - or '"{} {}"'.format(self.platform_name, entity.unique_id), + or f'"{self.platform_name} {entity.unique_id}"', ) return @@ -386,12 +386,12 @@ class EntityPlatform: # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): - raise HomeAssistantError("Invalid entity id: {}".format(entity.entity_id)) + raise HomeAssistantError(f"Invalid entity id: {entity.entity_id}") if ( entity.entity_id in self.entities or entity.entity_id in self.hass.states.async_entity_ids(self.domain) ): - msg = "Entity id already exists: {}".format(entity.entity_id) + msg = f"Entity id already exists: {entity.entity_id}" if entity.unique_id is not None: msg += ". Platform {} does not generate unique IDs".format( self.platform_name diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 7d81f62fa1c..00671e9c776 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -53,7 +53,7 @@ class RegistryEntry: device_id = attr.ib(type=str, default=None) config_entry_id = attr.ib(type=str, default=None) disabled_by = attr.ib( - type=str, + type=Optional[str], default=None, validator=attr.validators.in_( ( @@ -64,7 +64,7 @@ class RegistryEntry: None, ) ), - ) # type: Optional[str] + ) domain = attr.ib(type=str, init=False, repr=False) @domain.default @@ -154,8 +154,8 @@ class EntityRegistry: if entity_id: return self._async_update_entity( entity_id, - config_entry_id=config_entry_id, - device_id=device_id, + config_entry_id=config_entry_id or _UNDEF, + device_id=device_id or _UNDEF, # 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 @@ -166,9 +166,7 @@ class EntityRegistry: ) entity_id = self.async_generate_entity_id( - domain, - suggested_object_id or "{}_{}".format(platform, unique_id), - known_object_ids, + domain, suggested_object_id or f"{platform}_{unique_id}", known_object_ids ) if ( diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py index 1b8e3c2fbec..de48219a8d1 100644 --- a/homeassistant/helpers/entity_values.py +++ b/homeassistant/helpers/entity_values.py @@ -2,7 +2,7 @@ from collections import OrderedDict import fnmatch import re -from typing import Any, Dict, Optional, Pattern # noqa: F401 +from typing import Any, Dict, Optional, Pattern from homeassistant.core import split_entity_id @@ -17,12 +17,12 @@ class EntityValues: glob: Optional[Dict] = None, ) -> None: """Initialize an EntityConfigDict.""" - self._cache = {} # type: Dict[str, Dict] + self._cache: Dict[str, Dict] = {} self._exact = exact self._domain = domain if glob is None: - compiled = None # type: Optional[Dict[Pattern[str], Any]] + compiled: Optional[Dict[Pattern[str], Any]] = None else: compiled = OrderedDict() for key, value in glob.items(): diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 3afb5cb88e4..b7707b844d4 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,5 +1,5 @@ """Helpers for listening to events.""" -from datetime import timedelta +from datetime import datetime, timedelta import functools as ft from typing import Callable @@ -21,8 +21,7 @@ from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name @@ -187,7 +186,9 @@ track_same_state = threaded_listener_factory(async_track_same_state) @callback @bind_hass -def async_track_point_in_time(hass, action, point_in_time) -> CALLBACK_TYPE: +def async_track_point_in_time( + hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime +) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in time.""" utc_point_in_time = dt_util.as_utc(point_in_time) @@ -204,7 +205,9 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @callback @bind_hass -def async_track_point_in_utc_time(hass, action, point_in_time) -> CALLBACK_TYPE: +def async_track_point_in_utc_time( + hass: HomeAssistant, action: Callable[..., None], point_in_time: datetime +) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" # Ensure point_in_time is UTC point_in_time = dt_util.as_utc(point_in_time) @@ -284,8 +287,8 @@ class SunListener: action = attr.ib(type=Callable) event = attr.ib(type=str) offset = attr.ib(type=timedelta) - _unsub_sun = attr.ib(default=None) - _unsub_config = attr.ib(default=None) + _unsub_sun: CALLBACK_TYPE = attr.ib(default=None) + _unsub_config: CALLBACK_TYPE = attr.ib(default=None) @callback def async_attach(self): diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index ffd5918810f..1fa0ec76a67 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -55,10 +55,10 @@ async def async_handle( text_input: Optional[str] = None, ) -> "IntentResponse": """Handle an intent.""" - handler = hass.data.get(DATA_KEY, {}).get(intent_type) # type: IntentHandler + handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type) if handler is None: - raise UnknownIntent("Unknown intent {}".format(intent_type)) + raise UnknownIntent(f"Unknown intent {intent_type}") intent = Intent(hass, platform, intent_type, slots or {}, text_input) @@ -68,13 +68,11 @@ async def async_handle( return result except vol.Invalid as err: _LOGGER.warning("Received invalid slot info for %s: %s", intent_type, err) - raise InvalidSlotInfo( - "Received invalid slot info for {}".format(intent_type) - ) from err + raise InvalidSlotInfo(f"Received invalid slot info for {intent_type}") from err except IntentHandleError: raise except Exception as err: - raise IntentUnexpectedError("Error handling {}".format(intent_type)) from err + raise IntentUnexpectedError(f"Error handling {intent_type}") from err class IntentError(HomeAssistantError): @@ -109,7 +107,7 @@ def async_match_state( state = _fuzzymatch(name, states, lambda state: state.name) if state is None: - raise IntentHandleError("Unable to find an entity called {}".format(name)) + raise IntentHandleError(f"Unable to find an entity called {name}") return state @@ -118,18 +116,16 @@ def async_match_state( def async_test_feature(state: State, feature: int, feature_name: str) -> None: """Test is state supports a feature.""" if state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & feature == 0: - raise IntentHandleError( - "Entity {} does not support {}".format(state.name, feature_name) - ) + raise IntentHandleError(f"Entity {state.name} does not support {feature_name}") class IntentHandler: """Intent handler registration.""" - intent_type = None # type: Optional[str] - slot_schema = None # type: Optional[vol.Schema] + intent_type: Optional[str] = None + slot_schema: Optional[vol.Schema] = None _slot_schema = None - platforms = [] # type: Optional[Iterable[str]] + platforms: Optional[Iterable[str]] = [] @callback def async_can_handle(self, intent_obj: "Intent") -> bool: @@ -240,8 +236,8 @@ class IntentResponse: def __init__(self, intent: Optional[Intent] = None) -> None: """Initialize an IntentResponse.""" self.intent = intent - self.speech = {} # type: Dict[str, Dict[str, Any]] - self.card = {} # type: Dict[str, Dict[str, str]] + self.speech: Dict[str, Dict[str, Any]] = {} + self.card: Dict[str, Dict[str, str]] = {} @callback def async_set_speech( diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index cf17186fc98..fdf52c99075 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -2,7 +2,7 @@ import asyncio import logging from datetime import timedelta, datetime -from typing import Any, Dict, List, Set, Optional # noqa pylint_disable=unused-import +from typing import Any, Dict, List, Set, Optional from homeassistant.core import ( HomeAssistant, @@ -17,7 +17,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.json import JSONEncoder -from homeassistant.helpers.storage import Store # noqa pylint_disable=unused-import +from homeassistant.helpers.storage import Store # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -108,12 +108,12 @@ class RestoreStateData: def __init__(self, hass: HomeAssistant) -> None: """Initialize the restore state data class.""" - self.hass = hass # type: HomeAssistant - self.store = Store( + self.hass: HomeAssistant = hass + self.store: Store = Store( hass, STORAGE_VERSION, STORAGE_KEY, encoder=JSONEncoder - ) # type: Store - self.last_states = {} # type: Dict[str, StoredState] - self.entity_ids = set() # type: Set[str] + ) + self.last_states: Dict[str, StoredState] = {} + self.entity_ids: Set[str] = set() def async_get_stored_states(self) -> List[StoredState]: """Get the set of states which should be stored. diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1fd5bb673d7..0b569e2d4ad 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -9,7 +9,12 @@ from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple import voluptuous as vol from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE -from homeassistant.const import CONF_CONDITION, CONF_TIMEOUT +from homeassistant.const import ( + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_TIMEOUT, +) from homeassistant import exceptions from homeassistant.helpers import ( service, @@ -22,6 +27,7 @@ from homeassistant.helpers.event import ( async_track_template, ) from homeassistant.helpers.typing import ConfigType +from homeassistant.loader import async_get_integration import homeassistant.util.dt as date_util from homeassistant.util.async_ import run_coroutine_threadsafe, run_callback_threadsafe @@ -48,6 +54,7 @@ ACTION_WAIT_TEMPLATE = "wait_template" ACTION_CHECK_CONDITION = "condition" ACTION_FIRE_EVENT = "event" ACTION_CALL_SERVICE = "call_service" +ACTION_DEVICE_AUTOMATION = "device" def _determine_action(action): @@ -64,6 +71,9 @@ def _determine_action(action): if CONF_EVENT in action: return ACTION_FIRE_EVENT + if CONF_DEVICE_ID in action: + return ACTION_DEVICE_AUTOMATION + return ACTION_CALL_SERVICE @@ -102,21 +112,22 @@ class Script: self.name = name self._change_listener = change_listener self._cur = -1 - self._exception_step = None # type: Optional[int] + self._exception_step: Optional[int] = None self.last_action = None - self.last_triggered = None # type: Optional[datetime] + self.last_triggered: Optional[datetime] = None self.can_cancel = any( CONF_DELAY in action or CONF_WAIT_TEMPLATE in action for action in self.sequence ) - self._async_listener = [] # type: List[CALLBACK_TYPE] - self._config_cache = {} # type: Dict[Set[Tuple], Callable[..., bool]] + self._async_listener: List[CALLBACK_TYPE] = [] + self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {} self._actions = { ACTION_DELAY: self._async_delay, ACTION_WAIT_TEMPLATE: self._async_wait_template, ACTION_CHECK_CONDITION: self._async_check_condition, ACTION_FIRE_EVENT: self._async_fire_event, ACTION_CALL_SERVICE: self._async_call_service, + ACTION_DEVICE_AUTOMATION: self._async_device_automation, } @property @@ -318,6 +329,19 @@ class Script: context=context, ) + async def _async_device_automation(self, action, variables, context): + """Perform the device automation specified in the action. + + This method is a coroutine. + """ + self.last_action = action.get(CONF_ALIAS, "device automation") + self._log("Executing step %s" % self.last_action) + integration = await async_get_integration(self.hass, action[CONF_DOMAIN]) + platform = integration.get_platform("device_automation") + await platform.async_call_action_from_config( + self.hass, action, variables, context + ) + async def _async_fire_event(self, action, variables, context): """Fire an event.""" self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT]) @@ -338,7 +362,7 @@ class Script: config_cache_key = frozenset((k, str(v)) for k, v in action.items()) config = self._config_cache.get(config_cache_key) if not config: - config = condition.async_from_config(action, False) + config = await condition.async_from_config(self.hass, action, False) self._config_cache[config_cache_key] = config self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION]) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 60aceee110f..7f9692b3380 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -5,16 +5,7 @@ import json import logging from collections import defaultdict from types import ModuleType, TracebackType -from typing import ( # noqa: F401 pylint: disable=unused-import - Awaitable, - Dict, - Iterable, - List, - Optional, - Tuple, - Type, - Union, -) +from typing import Awaitable, Dict, Iterable, List, Optional, Tuple, Type, Union from homeassistant.loader import bind_hass, async_get_integration, IntegrationNotFound import homeassistant.util.dt as dt_util @@ -99,7 +90,7 @@ class AsyncTrackStates: def __init__(self, hass: HomeAssistantType) -> None: """Initialize a TrackStates block.""" self.hass = hass - self.states = [] # type: List[State] + self.states: List[State] = [] # pylint: disable=attribute-defined-outside-init def __enter__(self) -> List[State]: @@ -147,7 +138,7 @@ async def async_reproduce_state( if isinstance(states, State): states = [states] - to_call = defaultdict(list) # type: Dict[str, List[State]] + to_call: Dict[str, List[State]] = defaultdict(list) for state in states: to_call[state.domain].append(state) @@ -191,7 +182,7 @@ async def async_reproduce_state_legacy( context: Optional[Context] = None, ) -> None: """Reproduce given state.""" - to_call = defaultdict(list) # type: Dict[Tuple[str, str], List[str]] + to_call: Dict[Tuple[str, str], List[str]] = defaultdict(list) if domain == GROUP_DOMAIN: service_domain = HASS_DOMAIN @@ -238,7 +229,7 @@ async def async_reproduce_state_legacy( key = (service, json.dumps(dict(state.attributes), sort_keys=True)) to_call[key].append(state.entity_id) - domain_tasks = [] # type: List[Awaitable[Optional[bool]]] + domain_tasks: List[Awaitable[Optional[bool]]] = [] for (service, service_data), entity_ids in to_call.items(): data = json.loads(service_data) data[ATTR_ENTITY_ID] = entity_ids diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 368753cd626..cd99a47cf57 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -70,11 +70,11 @@ class Store: self.key = key self.hass = hass self._private = private - self._data = None # type: Optional[Dict[str, Any]] + self._data: Optional[Dict[str, Any]] = None self._unsub_delay_listener = None self._unsub_stop_listener = None self._write_lock = asyncio.Lock() - self._load_task = None # type: Optional[asyncio.Future] + self._load_task: Optional[asyncio.Future] = None self._encoder = encoder @property diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index d6c7496317d..9fa6e074bdd 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -68,14 +68,14 @@ def get_location_astral_event_next( mod = -1 while True: try: - next_dt = ( + next_dt: datetime.datetime = ( getattr(location, event)( dt_util.as_local(utc_point_in_time).date() + datetime.timedelta(days=mod), local=False, ) + offset - ) # type: datetime.datetime + ) if next_dt > utc_point_in_time: return next_dt except AstralError: diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index 8b32b1355fa..30b428a9e17 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -20,7 +20,7 @@ def display_temp( # If the temperature is not a number this can cause issues # with Polymer components, so bail early there. if not isinstance(temperature, Number): - raise TypeError("Temperature is not a number: {}".format(temperature)) + raise TypeError(f"Temperature is not a number: {temperature}") # type ignore: https://github.com/python/mypy/issues/7207 if temperature_unit != ha_unit: # type: ignore diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index ca320cb1c33..98e3849bfb6 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -320,10 +320,10 @@ class AllStates: """Return the domain state.""" if "." in name: if not valid_entity_id(name): - raise TemplateError("Invalid entity ID '{}'".format(name)) + raise TemplateError(f"Invalid entity ID '{name}'") return _get_state(self._hass, name) if not valid_entity_id(name + ".entity"): - raise TemplateError("Invalid domain name '{}'".format(name)) + raise TemplateError(f"Invalid domain name '{name}'") return DomainStates(self._hass, name) def _collect_all(self): @@ -367,9 +367,9 @@ class DomainStates: def __getattr__(self, name): """Return the states.""" - entity_id = "{}.{}".format(self._domain, name) + entity_id = f"{self._domain}.{name}" if not valid_entity_id(entity_id): - raise TemplateError("Invalid entity ID '{}'".format(entity_id)) + raise TemplateError(f"Invalid entity ID '{entity_id}'") return _get_state(self._hass, entity_id) def _collect_domain(self): @@ -399,7 +399,7 @@ class DomainStates: def __repr__(self): """Representation of Domain States.""" - return "