diff --git a/.coveragerc b/.coveragerc index 7f519f8970a..05682c79744 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,7 +5,6 @@ omit = homeassistant/__main__.py homeassistant/helpers/signal.py homeassistant/helpers/typing.py - homeassistant/monkey_patch.py homeassistant/scripts/*.py homeassistant/util/async.py @@ -85,7 +84,6 @@ omit = homeassistant/components/blockchain/sensor.py homeassistant/components/bloomsky/* homeassistant/components/bluesound/* - homeassistant/components/bluetooth_le_tracker/device_tracker.py homeassistant/components/bluetooth_tracker/* homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py @@ -97,6 +95,9 @@ omit = homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py + homeassistant/components/brother/__init__.py + homeassistant/components/brother/sensor.py + homeassistant/components/brother/const.py homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* homeassistant/components/brunt/cover.py @@ -259,6 +260,9 @@ omit = homeassistant/components/geniushub/* homeassistant/components/gearbest/sensor.py homeassistant/components/geizhals/sensor.py + homeassistant/components/gios/__init__.py + homeassistant/components/gios/air_quality.py + homeassistant/components/gios/consts.py homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py @@ -319,7 +323,9 @@ omit = homeassistant/components/iaqualink/light.py homeassistant/components/iaqualink/sensor.py homeassistant/components/iaqualink/switch.py - homeassistant/components/icloud/* + homeassistant/components/icloud/__init__.py + homeassistant/components/icloud/device_tracker.py + homeassistant/components/icloud/sensor.py homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/izone/__init__.py @@ -332,6 +338,7 @@ omit = homeassistant/components/influxdb/sensor.py homeassistant/components/insteon/* homeassistant/components/incomfort/* + homeassistant/components/intesishome/* homeassistant/components/ios/* homeassistant/components/iota/* homeassistant/components/iperf3/* @@ -347,6 +354,7 @@ omit = homeassistant/components/kankun/switch.py homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/device_tracker.py + homeassistant/components/kef/* homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* homeassistant/components/kira/* @@ -601,6 +609,7 @@ omit = homeassistant/components/sensehat/light.py homeassistant/components/sensehat/sensor.py homeassistant/components/sensibo/climate.py + homeassistant/components/sentry/__init__.py homeassistant/components/serial/sensor.py homeassistant/components/serial_pm/sensor.py homeassistant/components/sesame/lock.py @@ -610,6 +619,8 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py + homeassistant/components/signal_messenger/__init__.py + homeassistant/components/signal_messenger/notify.py homeassistant/components/simplepush/notify.py homeassistant/components/simplisafe/__init__.py homeassistant/components/simplisafe/alarm_control_panel.py @@ -654,9 +665,11 @@ omit = homeassistant/components/starlingbank/sensor.py homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* + homeassistant/components/stookalert/* homeassistant/components/streamlabswater/* homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py + homeassistant/components/surepetcare/*.py homeassistant/components/swiss_hydrological_data/sensor.py homeassistant/components/swiss_public_transport/sensor.py homeassistant/components/swisscom/device_tracker.py @@ -684,7 +697,14 @@ omit = homeassistant/components/telnet/switch.py homeassistant/components/temper/sensor.py homeassistant/components/tensorflow/image_processing.py - homeassistant/components/tesla/* + homeassistant/components/tesla/__init__.py + homeassistant/components/tesla/binary_sensor.py + homeassistant/components/tesla/climate.py + homeassistant/components/tesla/const.py + homeassistant/components/tesla/device_tracker.py + homeassistant/components/tesla/lock.py + homeassistant/components/tesla/sensor.py + homeassistant/components/tesla/switch.py homeassistant/components/tfiac/climate.py homeassistant/components/thermoworks_smoke/sensor.py homeassistant/components/thethingsnetwork/* @@ -695,6 +715,7 @@ omit = homeassistant/components/tikteck/light.py homeassistant/components/tile/device_tracker.py homeassistant/components/time_date/sensor.py + homeassistant/components/tmb/sensor.py homeassistant/components/todoist/calendar.py homeassistant/components/todoist/const.py homeassistant/components/tof/sensor.py @@ -704,7 +725,6 @@ omit = homeassistant/components/totalconnect/* homeassistant/components/touchline/climate.py homeassistant/components/tplink/device_tracker.py - homeassistant/components/tplink/light.py homeassistant/components/tplink/switch.py homeassistant/components/tplink_lte/* homeassistant/components/traccar/device_tracker.py @@ -745,11 +765,11 @@ omit = homeassistant/components/velbus/climate.py homeassistant/components/velbus/const.py homeassistant/components/velbus/cover.py + homeassistant/components/velbus/light.py homeassistant/components/velbus/sensor.py homeassistant/components/velbus/switch.py homeassistant/components/velux/* homeassistant/components/venstar/climate.py - homeassistant/components/vera/* homeassistant/components/verisure/* homeassistant/components/versasense/* homeassistant/components/vesync/__init__.py @@ -759,7 +779,7 @@ omit = homeassistant/components/viaggiatreno/sensor.py homeassistant/components/vicare/* homeassistant/components/vivotek/camera.py - homeassistant/components/vizio/media_player.py + homeassistant/components/vizio/* homeassistant/components/vlc/media_player.py homeassistant/components/vlc_telnet/media_player.py homeassistant/components/volkszaehler/sensor.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 5bfd37fab36..7b56b66d0b5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,3 @@ -// See https://aka.ms/vscode-remote/devcontainer.json for format details. { "name": "Home Assistant Dev", "context": "..", diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 1af7fc0490e..f68fbbc800c 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,6 +3,7 @@ - Make sure you are running the latest version of Home Assistant before reporting an issue: https://github.com/home-assistant/home-assistant/releases - Frontend issues should be submitted to the home-assistant-polymer repository: https://github.com/home-assistant/home-assistant-polymer/issues - iOS issues should be submitted to the home-assistant-iOS repository: https://github.com/home-assistant/home-assistant-iOS/issues +- Android issues should be submitted to the home-assistant-android repository: https://github.com/home-assistant/home-assistant-android/issues - Do not report issues for integrations if you are using custom integration: files in /custom_components - This is for bugs only. Feature and enhancement requests should go in our community forum: https://community.home-assistant.io/c/feature-requests - Provide as many details as possible. Paste logs, configuration sample and code into the backticks. Do not delete any text from this template! diff --git a/.pre-commit-config-all.yaml b/.pre-commit-config-all.yaml index f7b29cddc4f..1eabfcb0017 100644 --- a/.pre-commit-config-all.yaml +++ b/.pre-commit-config-all.yaml @@ -24,7 +24,7 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==4.0.1 + - pydocstyle==5.0.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 @@ -35,6 +35,14 @@ repos: - --format=custom - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-json # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a # virtualenv of its own, meaning we'd need to install and maintain diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 216bac95f29..226708bb947 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,14 +20,22 @@ repos: - id: flake8 additional_dependencies: - flake8-docstrings==1.5.0 - - pydocstyle==4.0.1 + - pydocstyle==5.0.1 files: ^(homeassistant|script|tests)/.+\.py$ - repo: https://github.com/PyCQA/bandit rev: 1.6.2 hooks: - - id: bandit + - id: bandit args: - --quiet - --format=custom - --configfile=tests/bandit.yaml files: ^(homeassistant|script|tests)/.+\.py$ +- repo: https://github.com/pre-commit/mirrors-isort + rev: v4.3.21 + hooks: + - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-json diff --git a/.readthedocs.yml b/.readthedocs.yml index 923a03f03dd..0303f84d51c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ build: image: latest python: - version: 3.6 + version: 3.7 setup_py_install: true requirements_file: requirements_docs.txt diff --git a/.travis.yml b/.travis.yml index c9638b02a2f..6add8c15bfc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,7 @@ sudo: false -dist: xenial +dist: bionic addons: apt: - sources: - - sourceline: "ppa:jonathonf/ffmpeg-4" packages: - libudev-dev - libavformat-dev @@ -16,15 +14,13 @@ addons: matrix: fast_finish: true include: - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=lint - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30 - - python: "3.6.1" + - python: "3.7.0" env: TOXENV=typing - - python: "3.6.1" - env: TOXENV=py36 - - python: "3.7" + - python: "3.7.0" env: TOXENV=py37 cache: diff --git a/CODEOWNERS b/CODEOWNERS index 8078aadf641..6e4ea0e8b77 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -51,6 +51,7 @@ homeassistant/components/blink/* @fronzbot homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/braviatv/* @robbiet480 homeassistant/components/broadlink/* @danielhiversen @felipediel +homeassistant/components/brother/* @bieniu homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties @@ -85,6 +86,7 @@ homeassistant/components/ecobee/* @marthoc homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 +homeassistant/components/elgato/* @frenck homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emulated_hue/* @NobleKangaroo @@ -118,6 +120,7 @@ homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/geonetnz_volcano/* @exxamalte +homeassistant/components/gios/* @bieniu homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 @@ -151,6 +154,7 @@ homeassistant/components/huawei_lte/* @scop homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob homeassistant/components/iaqualink/* @flz +homeassistant/components/icloud/* @Quentame homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/incomfort/* @zxdavb homeassistant/components/influxdb/* @fabaff @@ -161,6 +165,7 @@ homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core homeassistant/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core +homeassistant/components/intesishome/* @jnimmo homeassistant/components/ios/* @robbiet480 homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/ipma/* @dgomes @@ -172,6 +177,7 @@ homeassistant/components/juicenet/* @jesserockz homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph homeassistant/components/keenetic_ndms2/* @foxel +homeassistant/components/kef/* @basnijholt homeassistant/components/keyboard_remote/* @bendavid homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills @@ -183,6 +189,7 @@ homeassistant/components/life360/* @pnbruckner homeassistant/components/linky/* @Quentame homeassistant/components/linux_battery/* @fabaff homeassistant/components/liveboxplaytv/* @pschmitt +homeassistant/components/local_ip/* @issacg homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend @@ -232,6 +239,7 @@ homeassistant/components/obihai/* @dshokouhi homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/onboarding/* @home-assistant/core +homeassistant/components/onewire/* @garbled1 homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff @@ -244,6 +252,7 @@ homeassistant/components/pcal9535a/* @Shulyaka homeassistant/components/persistent_notification/* @home-assistant/core homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff @johnluetke +homeassistant/components/pilight/* @trekky12 homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren @@ -265,6 +274,7 @@ homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen +homeassistant/components/ring/* @balloob homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt homeassistant/components/saj/* @fredericvl @@ -274,11 +284,13 @@ homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core homeassistant/components/sense/* @kbickar homeassistant/components/sensibo/* @andrey-git +homeassistant/components/sentry/* @dcramer homeassistant/components/serial/* @fabaff homeassistant/components/seventeentrack/* @bachya homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff +homeassistant/components/signal_messenger/* @bbernhard homeassistant/components/simplisafe/* @bachya homeassistant/components/sinch/* @bendikrb homeassistant/components/slide/* @ualex73 @@ -300,11 +312,13 @@ homeassistant/components/sql/* @dgomes homeassistant/components/starline/* @anonym-tsk homeassistant/components/statistics/* @fabaff homeassistant/components/stiebel_eltron/* @fucm +homeassistant/components/stookalert/* @fwestenberg homeassistant/components/stream/* @hunterjm homeassistant/components/stt/* @pvizeli homeassistant/components/suez_water/* @ooii homeassistant/components/sun/* @Swamp-Ig homeassistant/components/supla/* @mwegrzynek +homeassistant/components/surepetcare/* @benleb homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switchbot/* @danielhiversen @@ -317,14 +331,15 @@ homeassistant/components/tado/* @michaelarnauts homeassistant/components/tahoma/* @philklei homeassistant/components/tautulli/* @ludeeus homeassistant/components/tellduslive/* @fredrike -homeassistant/components/template/* @PhracturedBlue -homeassistant/components/tesla/* @zabuldon +homeassistant/components/template/* @PhracturedBlue @tetienne +homeassistant/components/tesla/* @zabuldon @alandtse homeassistant/components/tfiac/* @fredrike @mellado homeassistant/components/thethingsnetwork/* @fabaff homeassistant/components/threshold/* @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff +homeassistant/components/tmb/* @alemuro homeassistant/components/todoist/* @boralyl homeassistant/components/toon/* @frenck homeassistant/components/tplink/* @rytilahti @@ -358,10 +373,12 @@ homeassistant/components/waqi/* @andrey-git homeassistant/components/watson_tts/* @rutkai homeassistant/components/weather/* @fabaff homeassistant/components/weblink/* @home-assistant/core +homeassistant/components/webostv/* @bendavid homeassistant/components/websocket_api/* @home-assistant/core homeassistant/components/wemo/* @sqldiablo homeassistant/components/withings/* @vangorra homeassistant/components/wled/* @frenck +homeassistant/components/workday/* @fabaff homeassistant/components/worldclock/* @fabaff homeassistant/components/wwlln/* @bachya homeassistant/components/xbox_live/* @MartinHjelmare diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbe77c7756f..1921e5d38dd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ Everybody is invited and welcome to contribute to Home Assistant. There is a lot The process is straight-forward. - - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0) + - Read [How to get faster PR reviews](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#best-practices-for-faster-reviews) by Kubernetes (but skip step 0 and 1) - Fork the Home Assistant [git repository](https://github.com/home-assistant/home-assistant). - Write the code for your device, notification service, sensor, or IoT thing. - Ensure tests work. @@ -12,3 +12,7 @@ The process is straight-forward. Still interested? Then you should take a peek at the [developer documentation](https://developers.home-assistant.io/) to get more details. +## Feature suggestions + +If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests). +We use [GitHub for tracking issues](https://github.com/home-assistant/home-assistant/issues), not for tracking feature requests. \ No newline at end of file diff --git a/README.rst b/README.rst index ae9531456fd..0de30d43c65 100644 --- a/README.rst +++ b/README.rst @@ -1,14 +1,7 @@ Home Assistant |Chat Status| ================================================================================= -Home Assistant is a home automation platform running on Python 3. It is able to track and control all devices at home and offer a platform for automating control. - -To get started: - -.. code:: bash - - python3 -m pip install homeassistant - hass --open-ui +Open source home automation that puts local control and privacy first. Powered by a worldwide community of tinkerers and DIY enthusiasts. Perfect to run on a Raspberry Pi or a local server. Check out `home-assistant.io `__ for `a demo `__, `installation instructions `__, diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index cbad0c9af08..546b63950fe 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -14,8 +14,6 @@ pr: resources: containers: - - container: 36 - image: homeassistant/ci-azure:3.6 - container: 37 image: homeassistant/ci-azure:3.7 repositories: @@ -25,7 +23,7 @@ resources: endpoint: 'home-assistant' variables: - name: PythonMain - value: '36' + value: '37' - group: codecov stages: @@ -54,6 +52,14 @@ stages: . venv/bin/activate pre-commit run bandit --all-files displayName: 'Run bandit' + - script: | + . venv/bin/activate + pre-commit run isort --all-files --show-diff-on-failure + displayName: 'Run isort' + - script: | + . venv/bin/activate + pre-commit run check-json --all-files + displayName: 'Run check-json' - job: 'Validate' pool: vmImage: 'ubuntu-latest' @@ -91,7 +97,7 @@ stages: pre-commit install-hooks --config .pre-commit-config-all.yaml - script: | . venv/bin/activate - pre-commit run black --all-files + pre-commit run black --all-files --show-diff-on-failure displayName: 'Check Black formatting' - stage: 'Tests' @@ -104,8 +110,6 @@ stages: strategy: maxParallel: 3 matrix: - Python36: - python.container: '36' Python37: python.container: '37' container: $[ variables['python.container'] ] diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 60eff866676..ddf95d354c3 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -14,7 +14,7 @@ schedules: always: true variables: - name: versionBuilder - value: '6.3' + value: '6.9' - group: docker - group: github - group: twine @@ -94,7 +94,7 @@ stages: buildMachine: 'raspberrypi2,raspberrypi3,raspberrypi4,odroid-xu,tinker' aarch64: buildArch: 'aarch64' - buildMachine: 'qemuarm-64,raspberrypi3-64,raspberrypi4-64,odroid-c2,orangepi-prime' + buildMachine: 'qemuarm-64,raspberrypi3-64,raspberrypi4-64,odroid-c2,odroid-n2' steps: - template: templates/azp-step-ha-version.yaml@azure - script: | diff --git a/docs/screenshot-components.png b/docs/screenshot-components.png index a98b3d41ab9..44dc373e285 100644 Binary files a/docs/screenshot-components.png and b/docs/screenshot-components.png differ diff --git a/docs/source/_ext/edit_on_github.py b/docs/source/_ext/edit_on_github.py index eef249a3f01..a31fb13ebf1 100644 --- a/docs/source/_ext/edit_on_github.py +++ b/docs/source/_ext/edit_on_github.py @@ -8,7 +8,6 @@ Loosely based on https://github.com/astropy/astropy/pull/347 import os import warnings - __licence__ = 'BSD (3 clause)' diff --git a/docs/source/conf.py b/docs/source/conf.py index b5428ede8fa..f36b5b8124a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -17,11 +17,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -import sys -import os import inspect +import os +import sys -from homeassistant.const import __version__, __short_version__ +from homeassistant.const import __short_version__, __version__ PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 40336c19592..5ebdc71680e 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,14 +1,14 @@ """Start Home Assistant.""" import argparse +import asyncio import os import platform import subprocess import sys import threading -from typing import List, Dict, Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Dict, List -from homeassistant import monkey_patch -from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE +from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ if TYPE_CHECKING: from homeassistant import core @@ -16,7 +16,6 @@ if TYPE_CHECKING: def set_loop() -> None: """Attempt to use different loop.""" - import asyncio from asyncio.events import BaseDefaultEventLoopPolicy if sys.platform == "win32": @@ -56,10 +55,8 @@ def ensure_config_path(config_dir: str) -> None: if not os.path.isdir(config_dir): if config_dir != config_util.get_default_config_dir(): print( - ( - "Fatal Error: Specified configuration directory does " - "not exist {} " - ).format(config_dir) + f"Fatal Error: Specified configuration directory {config_dir} " + "does not exist" ) sys.exit(1) @@ -67,10 +64,8 @@ def ensure_config_path(config_dir: str) -> None: os.mkdir(config_dir) except OSError: print( - ( - "Fatal Error: Unable to create default configuration " - "directory {} " - ).format(config_dir) + "Fatal Error: Unable to create default configuration " + f"directory {config_dir}" ) sys.exit(1) @@ -79,11 +74,7 @@ def ensure_config_path(config_dir: str) -> None: try: os.mkdir(lib_dir) except OSError: - print( - ("Fatal Error: Unable to create library " "directory {} ").format( - lib_dir - ) - ) + print(f"Fatal Error: Unable to create library directory {lib_dir}") sys.exit(1) @@ -148,7 +139,7 @@ def get_arguments() -> argparse.Namespace: "--log-file", type=str, default=None, - help="Log file to write to. If not set, CONFIG/home-assistant.log " "is used", + help="Log file to write to. If not set, CONFIG/home-assistant.log is used", ) parser.add_argument( "--log-no-color", action="store_true", help="Disable color logs" @@ -217,7 +208,7 @@ def check_pid(pid_file: str) -> None: except OSError: # PID does not exist return - print("Fatal Error: HomeAssistant is already running.") + print("Fatal Error: Home Assistant is already running.") sys.exit(1) @@ -261,7 +252,7 @@ def cmdline() -> List[str]: async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: - """Set up HASS and run.""" + """Set up Home Assistant and run.""" from homeassistant import bootstrap, core hass = core.HomeAssistant() @@ -345,11 +336,6 @@ def main() -> int: """Start Home Assistant.""" validate_python() - monkey_patch_needed = sys.version_info[:3] < (3, 6, 3) - if monkey_patch_needed and os.environ.get("HASS_NO_MONKEY") != "1": - monkey_patch.disable_c_asyncio() - monkey_patch.patch_weakref_tasks() - set_loop() # Run a simple daemon runner process on Windows to handle restarts @@ -383,13 +369,11 @@ def main() -> int: if args.pid_file: write_pid(args.pid_file) - from homeassistant.util.async_ import asyncio_run - - exit_code = asyncio_run(setup_and_run_hass(config_dir, args)) + exit_code = asyncio.run(setup_and_run_hass(config_dir, args)) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() - return exit_code # type: ignore + return exit_code if __name__ == "__main__": diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 3f7dd570400..9b3cf49fa22 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -1,21 +1,21 @@ """Provide an authentication layer for Home Assistant.""" import asyncio -import logging from collections import OrderedDict from datetime import timedelta +import logging from typing import Any, Dict, List, Optional, Tuple, cast import jwt from homeassistant import data_entry_flow from homeassistant.auth.const import ACCESS_TOKEN_EXPIRATION -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.util import dt as dt_util from . import auth_store, models from .const import GROUP_ID_ADMIN -from .mfa_modules import auth_mfa_module_from_config, MultiFactorAuthModule -from .providers import auth_provider_from_config, AuthProvider, LoginFlow +from .mfa_modules import MultiFactorAuthModule, auth_mfa_module_from_config +from .providers import AuthProvider, LoginFlow, auth_provider_from_config EVENT_USER_ADDED = "user_added" EVENT_USER_REMOVED = "user_removed" @@ -67,6 +67,69 @@ async def auth_manager_from_config( return manager +class AuthManagerFlowManager(data_entry_flow.FlowManager): + """Manage authentication flows.""" + + def __init__(self, hass: HomeAssistant, auth_manager: "AuthManager"): + """Init auth manager flows.""" + super().__init__(hass) + self.auth_manager = auth_manager + + async def async_create_flow( + self, + handler_key: Any, + *, + context: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + ) -> data_entry_flow.FlowHandler: + """Create a login flow.""" + auth_provider = self.auth_manager.get_auth_provider(*handler_key) + if not auth_provider: + raise KeyError(f"Unknown auth provider {handler_key}") + return await auth_provider.async_login_flow(context) + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any] + ) -> Dict[str, Any]: + """Return a user as result of login flow.""" + flow = cast(LoginFlow, flow) + + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return result + + # we got final result + if isinstance(result["data"], models.User): + result["result"] = result["data"] + return result + + auth_provider = self.auth_manager.get_auth_provider(*result["handler"]) + if not auth_provider: + raise KeyError(f"Unknown auth provider {result['handler']}") + + credentials = await auth_provider.async_get_or_create_credentials( + result["data"] + ) + + if flow.context.get("credential_only"): + result["result"] = credentials + return result + + # multi-factor module cannot enabled for new credential + # which has not linked to a user yet + if auth_provider.support_mfa and not credentials.is_new: + user = await self.auth_manager.async_get_user_by_credentials(credentials) + if user is not None: + modules = await self.auth_manager.async_get_enabled_mfa(user) + + if modules: + flow.user = user + flow.available_mfa_modules = modules + return await flow.async_step_select_mfa_module() + + result["result"] = await self.auth_manager.async_get_or_create_user(credentials) + return result + + class AuthManager: """Manage the authentication for Home Assistant.""" @@ -82,9 +145,7 @@ class AuthManager: self._store = store self._providers = providers self._mfa_modules = mfa_modules - self.login_flow = data_entry_flow.FlowManager( - hass, self._async_create_login_flow, self._async_finish_login_flow - ) + self.login_flow = AuthManagerFlowManager(hass, self) @property def auth_providers(self) -> List[AuthProvider]: @@ -417,50 +478,6 @@ class AuthManager: return refresh_token - async def _async_create_login_flow( - self, handler: _ProviderKey, *, context: Optional[Dict], data: Optional[Any] - ) -> data_entry_flow.FlowHandler: - """Create a login flow.""" - auth_provider = self._providers[handler] - - return await auth_provider.async_login_flow(context) - - async def _async_finish_login_flow( - self, flow: LoginFlow, result: Dict[str, Any] - ) -> Dict[str, Any]: - """Return a user as result of login flow.""" - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return result - - # we got final result - if isinstance(result["data"], models.User): - result["result"] = result["data"] - return result - - auth_provider = self._providers[result["handler"]] - credentials = await auth_provider.async_get_or_create_credentials( - result["data"] - ) - - if flow.context.get("credential_only"): - result["result"] = credentials - return result - - # multi-factor module cannot enabled for new credential - # which has not linked to a user yet - if auth_provider.support_mfa and not credentials.is_new: - user = await self.async_get_user_by_credentials(credentials) - if user is not None: - modules = await self.async_get_enabled_mfa(user) - - if modules: - flow.user = user - flow.available_mfa_modules = modules - return await flow.async_step_select_mfa_module() - - result["result"] = await self.async_get_or_create_user(credentials) - return result - @callback def _async_get_auth_provider( self, credentials: models.Credentials diff --git a/homeassistant/auth/auth_store.py b/homeassistant/auth/auth_store.py index 4c64730edda..57ec9ee63dc 100644 --- a/homeassistant/auth/auth_store.py +++ b/homeassistant/auth/auth_store.py @@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant, callback 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 .const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY, GROUP_ID_USER from .permissions import PermissionLookup, system_policies from .permissions.types import PolicyType diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index 9020b0b321e..fd9e61b9d17 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -7,7 +7,7 @@ from typing import Any, Dict, Optional import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant import requirements, data_entry_flow +from homeassistant import data_entry_flow, requirements from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError diff --git a/homeassistant/auth/mfa_modules/insecure_example.py b/homeassistant/auth/mfa_modules/insecure_example.py index a3f0d58c6b3..45cc07ae581 100644 --- a/homeassistant/auth/mfa_modules/insecure_example.py +++ b/homeassistant/auth/mfa_modules/insecure_example.py @@ -7,9 +7,9 @@ import voluptuous as vol from homeassistant.core import HomeAssistant from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index b14f5fedc22..46cc634bcae 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -3,9 +3,9 @@ Sending HOTP through notify service """ import asyncio -import logging from collections import OrderedDict -from typing import Any, Dict, Optional, List +import logging +from typing import Any, Dict, List, Optional import attr import voluptuous as vol @@ -16,9 +16,9 @@ from homeassistant.exceptions import ServiceNotFound from homeassistant.helpers import config_validation as cv from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/mfa_modules/totp.py b/homeassistant/auth/mfa_modules/totp.py index 9b0f3910e92..6abddd2123f 100644 --- a/homeassistant/auth/mfa_modules/totp.py +++ b/homeassistant/auth/mfa_modules/totp.py @@ -1,7 +1,7 @@ """Time-based One Time Password auth module.""" import asyncio -import logging from io import BytesIO +import logging from typing import Any, Dict, Optional, Tuple import voluptuous as vol @@ -10,9 +10,9 @@ from homeassistant.auth.models import User from homeassistant.core import HomeAssistant from . import ( - MultiFactorAuthModule, - MULTI_FACTOR_AUTH_MODULES, MULTI_FACTOR_AUTH_MODULE_SCHEMA, + MULTI_FACTOR_AUTH_MODULES, + MultiFactorAuthModule, SetupFlow, ) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 6889d17a25f..08f2f375b41 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -1,5 +1,6 @@ """Auth models.""" from datetime import datetime, timedelta +import secrets from typing import Dict, List, NamedTuple, Optional import uuid @@ -9,7 +10,6 @@ from homeassistant.util import dt as dt_util from . import permissions as perm_mdl from .const import GROUP_ID_ADMIN -from .util import generate_secret TOKEN_TYPE_NORMAL = "normal" TOKEN_TYPE_SYSTEM = "system" @@ -96,8 +96,8 @@ class RefreshToken: ) id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) created_at = attr.ib(type=datetime, factory=dt_util.utcnow) - token = attr.ib(type=str, factory=lambda: generate_secret(64)) - jwt_key = attr.ib(type=str, factory=lambda: generate_secret(64)) + token = attr.ib(type=str, factory=lambda: secrets.token_hex(64)) + jwt_key = attr.ib(type=str, factory=lambda: secrets.token_hex(64)) last_used_at = attr.ib(type=Optional[datetime], default=None) last_used_ip = attr.ib(type=Optional[str], default=None) diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 36cf9c4f420..92d02c75b91 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -5,13 +5,12 @@ from typing import Any, Callable, Optional import voluptuous as vol from .const import CAT_ENTITIES -from .models import PermissionLookup -from .types import PolicyType from .entities import ENTITY_POLICY_SCHEMA, compile_entities from .merge import merge_policies # noqa: F401 +from .models import PermissionLookup +from .types import PolicyType from .util import test_all - POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA}) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/auth/permissions/entities.py b/homeassistant/auth/permissions/entities.py index add9913abf3..be30c7bf69a 100644 --- a/homeassistant/auth/permissions/entities.py +++ b/homeassistant/auth/permissions/entities.py @@ -4,11 +4,10 @@ from typing import Callable, Optional import voluptuous as vol -from .const import SUBCAT_ALL, POLICY_READ, POLICY_CONTROL, POLICY_EDIT +from .const import POLICY_CONTROL, POLICY_EDIT, POLICY_READ, SUBCAT_ALL from .models import PermissionLookup from .types import CategoryType, SubCategoryDict, ValueType - -from .util import SubCatLookupType, lookup_all, compile_policy +from .util import SubCatLookupType, compile_policy, lookup_all SINGLE_ENTITY_SCHEMA = vol.Any( True, diff --git a/homeassistant/auth/permissions/merge.py b/homeassistant/auth/permissions/merge.py index 3cf02e05771..fad98b3f22a 100644 --- a/homeassistant/auth/permissions/merge.py +++ b/homeassistant/auth/permissions/merge.py @@ -1,7 +1,7 @@ """Merging of policies.""" -from typing import cast, Dict, List, Set +from typing import Dict, List, Set, cast -from .types import PolicyType, CategoryType +from .types import CategoryType, PolicyType def merge_policies(policies: List[PolicyType]) -> PolicyType: diff --git a/homeassistant/auth/permissions/system_policies.py b/homeassistant/auth/permissions/system_policies.py index b40400304cc..b45984653fb 100644 --- a/homeassistant/auth/permissions/system_policies.py +++ b/homeassistant/auth/permissions/system_policies.py @@ -1,5 +1,5 @@ """System policies.""" -from .const import CAT_ENTITIES, SUBCAT_ALL, POLICY_READ +from .const import CAT_ENTITIES, POLICY_READ, SUBCAT_ALL ADMIN_POLICY = {CAT_ENTITIES: True} diff --git a/homeassistant/auth/permissions/util.py b/homeassistant/auth/permissions/util.py index 4d38e0a639c..11bbd878eb2 100644 --- a/homeassistant/auth/permissions/util.py +++ b/homeassistant/auth/permissions/util.py @@ -1,6 +1,5 @@ """Helpers to deal with permissions.""" from functools import wraps - from typing import Callable, Dict, List, Optional, cast from .const import SUBCAT_ALL diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index cbce3152902..bb0fc55b5c4 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -8,8 +8,8 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import data_entry_flow, requirements -from homeassistant.core import callback, HomeAssistant from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.util import dt as dt_util from homeassistant.util.decorator import Registry diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index 58a2cac1fc5..12e27c01504 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -1,20 +1,18 @@ """Auth provider that validates credentials via an external command.""" -from typing import Any, Dict, Optional, cast - import asyncio.subprocess import collections import logging import os +from typing import Any, Dict, Optional, cast import voluptuous as vol from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - CONF_COMMAND = "command" CONF_ARGS = "args" CONF_META = "meta" @@ -78,7 +76,7 @@ class CommandLineAuthProvider(AuthProvider): if process.returncode != 0: _LOGGER.error( - "User %r failed to authenticate, command exited " "with code %d.", + "User %r failed to authenticate, command exited with code %d.", username, process.returncode, ) diff --git a/homeassistant/auth/providers/homeassistant.py b/homeassistant/auth/providers/homeassistant.py index 265a24a4b28..b3acaaa6352 100644 --- a/homeassistant/auth/providers/homeassistant.py +++ b/homeassistant/auth/providers/homeassistant.py @@ -3,21 +3,18 @@ import asyncio import base64 from collections import OrderedDict import logging - from typing import Any, Dict, List, Optional, Set, cast import bcrypt import voluptuous as vol from homeassistant.const import CONF_ID -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow - +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - STORAGE_VERSION = 1 STORAGE_KEY = "auth_provider.homeassistant" @@ -203,7 +200,7 @@ class Data: @AUTH_PROVIDERS.register("homeassistant") class HassAuthProvider(AuthProvider): - """Auth provider based on a local storage of users in HASS config dir.""" + """Auth provider based on a local storage of users in Home Assistant config dir.""" DEFAULT_TITLE = "Home Assistant Local" diff --git a/homeassistant/auth/providers/insecure_example.py b/homeassistant/auth/providers/insecure_example.py index 37859f5ed0e..70014a236cd 100644 --- a/homeassistant/auth/providers/insecure_example.py +++ b/homeassistant/auth/providers/insecure_example.py @@ -5,13 +5,12 @@ from typing import Any, Dict, Optional, cast import voluptuous as vol -from homeassistant.exceptions import HomeAssistantError from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta - USER_SCHEMA = vol.Schema( { vol.Required("username"): str, diff --git a/homeassistant/auth/providers/legacy_api_password.py b/homeassistant/auth/providers/legacy_api_password.py index 018886388df..15ba1dfc14c 100644 --- a/homeassistant/auth/providers/legacy_api_password.py +++ b/homeassistant/auth/providers/legacy_api_password.py @@ -12,9 +12,9 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from .. import AuthManager -from ..models import Credentials, UserMeta, User +from ..models import Credentials, User, UserMeta AUTH_PROVIDER_TYPE = "legacy_api_password" CONF_API_PASSWORD = "api_password" diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index f71be436acf..bc995368fec 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -3,15 +3,16 @@ It shows list of users if access from trusted network. Abort login flow if not access from trusted network. """ -from ipaddress import ip_network, IPv4Address, IPv6Address, IPv4Network, IPv6Network +from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_network from typing import Any, Dict, List, Optional, Union, cast import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from . import AuthProvider, AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, LoginFlow +import homeassistant.helpers.config_validation as cv + +from . import AUTH_PROVIDER_SCHEMA, AUTH_PROVIDERS, AuthProvider, LoginFlow from ..models import Credentials, UserMeta IPAddress = Union[IPv4Address, IPv6Address] diff --git a/homeassistant/auth/util.py b/homeassistant/auth/util.py deleted file mode 100644 index 83834fa7683..00000000000 --- a/homeassistant/auth/util.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Auth utils.""" -import binascii -import os - - -def generate_secret(entropy: int = 32) -> str: - """Generate a secret. - - Backport of secrets.token_hex from Python 3.6 - - Event loop friendly. - """ - return binascii.hexlify(os.urandom(entropy)).decode("ascii") diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 312c739cd72..7ceedba5bd5 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -1,22 +1,26 @@ """Provide methods to bootstrap a Home Assistant instance.""" import asyncio +from collections import OrderedDict import logging import logging.handlers import os import sys from time import time -from collections import OrderedDict -from typing import Any, Optional, Dict, Set +from typing import Any, Dict, Optional, Set import voluptuous as vol -from homeassistant import core, config as conf_util, config_entries, loader -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE +from homeassistant import config as conf_util, config_entries, core, loader +from homeassistant.const import ( + EVENT_HOMEASSISTANT_CLOSE, + REQUIRED_NEXT_PYTHON_DATE, + REQUIRED_NEXT_PYTHON_VER, +) +from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util.logging import AsyncHandler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache -from homeassistant.exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) @@ -27,7 +31,7 @@ DATA_LOGGING = "logging" DEBUGGER_INTEGRATIONS = {"ptvsd"} CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") -LOGGING_INTEGRATIONS = {"logger", "system_log"} +LOGGING_INTEGRATIONS = {"logger", "system_log", "sentry"} STAGE_1_INTEGRATIONS = { # To record data "recorder", @@ -62,7 +66,7 @@ async def async_from_config_dict( hass.config.skip_pip = skip_pip if skip_pip: _LOGGER.warning( - "Skipping pip installation of required modules. " "This may cause issues" + "Skipping pip installation of required modules. This may cause issues" ) core_config = config.get(core.DOMAIN, {}) @@ -95,11 +99,14 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) - if sys.version_info[:3] < (3, 7, 0): + if REQUIRED_NEXT_PYTHON_DATE and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER: msg = ( - "Python 3.6 support is deprecated and will " - "be removed in the first release after December 15, 2019. Please " - "upgrade Python to 3.7.0 or higher." + "Support for the running Python version " + f"{'.'.join(str(x) for x in sys.version_info[:3])} is deprecated and will " + f"be removed in the first release after {REQUIRED_NEXT_PYTHON_DATE}. " + "Please upgrade Python to " + f"{'.'.join(str(x) for x in REQUIRED_NEXT_PYTHON_VER)} or " + "higher." ) _LOGGER.warning(msg) hass.components.persistent_notification.async_create( @@ -161,7 +168,7 @@ def async_enable_logging( This method must be run in the event loop. """ - fmt = "%(asctime)s %(levelname)s (%(threadName)s) " "[%(name)s] %(message)s" + fmt = "%(asctime)s %(levelname)s (%(threadName)s) [%(name)s] %(message)s" datefmt = "%Y-%m-%d %H:%M:%S" if not log_no_color: diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 7bb572dcf6b..90e0f32226c 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -11,7 +11,6 @@ import logging from homeassistant.core import split_entity_id - # mypy: allow-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/abode/.translations/da.json b/homeassistant/components/abode/.translations/da.json index 3f094cb93bd..4a5fa763ea1 100644 --- a/homeassistant/components/abode/.translations/da.json +++ b/homeassistant/components/abode/.translations/da.json @@ -12,7 +12,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Email adresse" + "username": "Email-adresse" }, "title": "Udfyld dine Abode-loginoplysninger" } diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 1689576bc7f..a5f3e6116a4 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -20,14 +20,14 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity import Entity from .const import ( ATTRIBUTION, - DOMAIN, DEFAULT_CACHEDB, + DOMAIN, SIGNAL_CAPTURE_IMAGE, SIGNAL_TRIGGER_QUICK_ACTION, ) @@ -170,7 +170,7 @@ async def async_unload_entry(hass, config_entry): def setup_hass_services(hass): - """Home assistant services.""" + """Home Assistant services.""" def change_setting(call): """Change an Abode system setting.""" diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py index b8e8548db31..89b389798f6 100644 --- a/homeassistant/components/abode/config_flow.py +++ b/homeassistant/components/abode/config_flow.py @@ -10,7 +10,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from .const import DOMAIN, DEFAULT_CACHEDB # pylint: disable=unused-import +from .const import DEFAULT_CACHEDB, DOMAIN # pylint: disable=unused-import CONF_POLLING = "polling" diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index f29270d264c..c02019e6bcc 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -55,7 +55,7 @@ class AbodeLight(AbodeDevice, Light): self._device.set_color(kwargs[ATTR_HS_COLOR]) if ATTR_BRIGHTNESS in kwargs and self._device.is_dimmable: - # Convert HASS brightness (0-255) to Abode brightness (0-99) + # Convert Home Assistant brightness (0-255) to Abode brightness (0-99) # If 100 is sent to Abode, response is 99 causing an error self._device.set_level(ceil(kwargs[ATTR_BRIGHTNESS] * 99 / 255.0)) else: @@ -78,7 +78,7 @@ class AbodeLight(AbodeDevice, Light): # Abode returns 100 during device initialization and device refresh if brightness == 100: return 255 - # Convert Abode brightness (0-99) to HASS brightness (0-255) + # Convert Abode brightness (0-99) to Home Assistant brightness (0-255) return ceil(brightness * 255 / 99.0) @property diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 0a4307ff737..ce71906dfcc 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -3,11 +3,7 @@ "name": "Abode", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", - "requirements": [ - "abodepy==0.16.7" - ], + "requirements": ["abodepy==0.16.7"], "dependencies": [], - "codeowners": [ - "@shred86" - ] -} \ No newline at end of file + "codeowners": ["@shred86"] +} diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 9f712ba5c7e..eac1c36401a 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -1,10 +1,8 @@ { "domain": "acer_projector", - "name": "Acer projector", + "name": "Acer Projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector", - "requirements": [ - "pyserial==3.1.1" - ], + "requirements": ["pyserial==3.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 39a79636c93..b28d67562d4 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -1,17 +1,17 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" import logging import re -import serial +import serial import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( - STATE_ON, - STATE_OFF, - STATE_UNKNOWN, - CONF_NAME, CONF_FILENAME, + CONF_NAME, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/actiontec/device_tracker.py b/homeassistant/components/actiontec/device_tracker.py index e07dd2622be..302a8d56173 100644 --- a/homeassistant/components/actiontec/device_tracker.py +++ b/homeassistant/components/actiontec/device_tracker.py @@ -1,18 +1,19 @@ """Support for Actiontec MI424WR (Verizon FIOS) routers.""" +from collections import namedtuple import logging import re import telnetlib -from collections import namedtuple + import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/adguard/.translations/da.json b/homeassistant/components/adguard/.translations/da.json index 813405cec62..e9e6415518d 100644 --- a/homeassistant/components/adguard/.translations/da.json +++ b/homeassistant/components/adguard/.translations/da.json @@ -1,16 +1,18 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}. Opdater venligst din Hass.io AdGuard Home-tilf\u00f8jelse.", + "adguard_home_outdated": "Denne integration kr\u00e6ver AdGuard Home {minimal_version} eller h\u00f8jere, du har {current_version}.", "existing_instance_updated": "Opdaterede eksisterende konfiguration.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af AdGuard Home." + "single_instance_allowed": "Kun en enkelt konfiguration af AdGuard Home er tilladt." }, "error": { "connection_error": "Forbindelse mislykkedes." }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home, der leveres af Hass.io add-on: {addon}?", - "title": "AdGuard Home via Hass.io add-on" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til AdGuard Home leveret af Hass.io-tilf\u00f8jelsen: {addon}?", + "title": "AdGuard Home via Hass.io-tilf\u00f8jelse" }, "user": { "data": { @@ -21,8 +23,8 @@ "username": "Brugernavn", "verify_ssl": "AdGuard Home bruger et korrekt certifikat" }, - "description": "Konfigurer din AdGuard Home instans for at tillade overv\u00e5gning og kontrol.", - "title": "Link AdGuard Home." + "description": "Konfigurer din AdGuard Home-instans for at tillade overv\u00e5gning og kontrol.", + "title": "Forbind din AdGuard Home." } }, "title": "AdGuard Home" diff --git a/homeassistant/components/adguard/.translations/de.json b/homeassistant/components/adguard/.translations/de.json index b1fbbfa85ab..3434b6feac6 100644 --- a/homeassistant/components/adguard/.translations/de.json +++ b/homeassistant/components/adguard/.translations/de.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Diese Integration erfordert AdGuard Home {minimal_version} oder h\u00f6her, Sie haben {current_version}. Bitte aktualisieren Sie Ihr Hass.io AdGuard Home Add-on.", + "adguard_home_outdated": "Diese Integration erfordert AdGuard Home {minimal_version} oder h\u00f6her, Sie haben {current_version}.", "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig." }, diff --git a/homeassistant/components/adguard/.translations/it.json b/homeassistant/components/adguard/.translations/it.json index 1b3ce014d90..6dc6ae18d81 100644 --- a/homeassistant/components/adguard/.translations/it.json +++ b/homeassistant/components/adguard/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo Hass.io AdGuard Home.", + "adguard_home_addon_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}. Aggiorna il componente aggiuntivo AdGuard Home di Hass.io.", "adguard_home_outdated": "Questa integrazione richiede AdGuard Home {minimal_version} o versione successiva, si dispone di {current_version}.", "existing_instance_updated": "Configurazione esistente aggiornata.", "single_instance_allowed": "\u00c8 consentita solo una singola configurazione di AdGuard Home." @@ -11,7 +11,7 @@ }, "step": { "hassio_confirm": { - "description": "Vuoi configurare Home Assistant per connettersi alla AdGuard Home fornita dal componente aggiuntivo di Hass.io: {addon} ?", + "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": { diff --git a/homeassistant/components/adguard/.translations/ko.json b/homeassistant/components/adguard/.translations/ko.json index e1f39259292..02bbb75cd2b 100644 --- a/homeassistant/components/adguard/.translations/ko.json +++ b/homeassistant/components/adguard/.translations/ko.json @@ -11,7 +11,7 @@ }, "step": { "hassio_confirm": { - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c AdGuard Home \uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Hass.io \uc560\ub4dc\uc628\uc758 AdGuard Home" }, "user": { diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index bb53d00aab8..1f4d63d627b 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -61,7 +61,6 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool password=entry.data[CONF_PASSWORD], tls=entry.data[CONF_SSL], verify_ssl=entry.data[CONF_VERIFY_SSL], - loop=hass.loop, session=session, ) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 9f5645edb8d..3657d4ee3ad 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -79,7 +79,6 @@ class AdGuardHomeFlowHandler(ConfigFlow): password=user_input.get(CONF_PASSWORD), tls=user_input[CONF_SSL], verify_ssl=user_input[CONF_VERIFY_SSL], - loop=self.hass.loop, session=session, ) @@ -161,7 +160,6 @@ class AdGuardHomeFlowHandler(ConfigFlow): self._hassio_discovery[CONF_HOST], port=self._hassio_discovery[CONF_PORT], tls=False, - loop=self.hass.loop, session=session, ) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index 45fd21f4fc8..bdfec1f254b 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -1,13 +1,9 @@ { - "domain": "adguard", - "name": "AdGuard Home", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/adguard", - "requirements": [ - "adguardhome==0.3.0" - ], - "dependencies": [], - "codeowners": [ - "@frenck" - ] -} \ No newline at end of file + "domain": "adguard", + "name": "AdGuard Home", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/adguard", + "requirements": ["adguardhome==0.4.0"], + "dependencies": [], + "codeowners": ["@frenck"] +} diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py index e0c86e42d26..c818752ad2f 100644 --- a/homeassistant/components/adguard/sensor.py +++ b/homeassistant/components/adguard/sensor.py @@ -178,7 +178,7 @@ class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor): """Initialize AdGuard Home sensor.""" super().__init__( adguard, - "Searches Safe Search Enforced", + "AdGuard Safe Searches Enforced", "mdi:shield-search", "enforced_safesearch", "requests", diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index ba4762da84a..adaaaa08b7f 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -1,14 +1,13 @@ """Support for Automation Device Specification (ADS).""" -import threading -import struct -import logging -import ctypes -from collections import namedtuple import asyncio +from collections import namedtuple +import ctypes +import logging +import struct +import threading + import async_timeout - import pyads - import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/ads/binary_sensor.py b/homeassistant/components/ads/binary_sensor.py index 2afb163fc32..fd6d77873b5 100644 --- a/homeassistant/components/ads/binary_sensor.py +++ b/homeassistant/components/ads/binary_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/cover.py b/homeassistant/components/ads/cover.py index b21c064e941..0fdcbc16ef8 100644 --- a/homeassistant/components/ads/cover.py +++ b/homeassistant/components/ads/cover.py @@ -4,25 +4,25 @@ import logging import voluptuous as vol from homeassistant.components.cover import ( - PLATFORM_SCHEMA, - SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_STOP, - SUPPORT_SET_POSITION, ATTR_POSITION, DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, CoverDevice, ) -from homeassistant.const import CONF_NAME, CONF_DEVICE_CLASS +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME import homeassistant.helpers.config_validation as cv from . import ( CONF_ADS_VAR, CONF_ADS_VAR_POSITION, DATA_ADS, - AdsEntity, - STATE_KEY_STATE, STATE_KEY_POSITION, + STATE_KEY_STATE, + AdsEntity, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/light.py b/homeassistant/components/ads/light.py index f1e78ea132e..b9626b9e969 100644 --- a/homeassistant/components/ads/light.py +++ b/homeassistant/components/ads/light.py @@ -16,9 +16,9 @@ from . import ( CONF_ADS_VAR, CONF_ADS_VAR_BRIGHTNESS, DATA_ADS, - AdsEntity, STATE_KEY_BRIGHTNESS, STATE_KEY_STATE, + AdsEntity, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index cf3e621fd07..1509402d720 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -1,10 +1,8 @@ { "domain": "ads", - "name": "Ads", + "name": "ADS", "documentation": "https://www.home-assistant.io/integrations/ads", - "requirements": [ - "pyads==3.0.7" - ], + "requirements": ["pyads==3.0.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ads/sensor.py b/homeassistant/components/ads/sensor.py index 1d956b1fba2..3bdcbfc95f8 100644 --- a/homeassistant/components/ads/sensor.py +++ b/homeassistant/components/ads/sensor.py @@ -8,7 +8,7 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_FACTOR, CONF_ADS_TYPE, CONF_ADS_VAR, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ads/switch.py b/homeassistant/components/ads/switch.py index 1b5a40debb6..3590b6af88e 100644 --- a/homeassistant/components/ads/switch.py +++ b/homeassistant/components/ads/switch.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from . import CONF_ADS_VAR, DATA_ADS, AdsEntity, STATE_KEY_STATE +from . import CONF_ADS_VAR, DATA_ADS, STATE_KEY_STATE, AdsEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aftership/manifest.json b/homeassistant/components/aftership/manifest.json index a7e8aeb8deb..80dc959c136 100644 --- a/homeassistant/components/aftership/manifest.json +++ b/homeassistant/components/aftership/manifest.json @@ -1,10 +1,8 @@ { "domain": "aftership", - "name": "Aftership", + "name": "AfterShip", "documentation": "https://www.home-assistant.io/integrations/aftership", - "requirements": [ - "pyaftership==0.1.2" - ], + "requirements": ["pyaftership==0.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index ff5edc92ba0..eb0236cf3be 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -77,7 +77,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([instance], True) async def handle_add_tracking(call): - """Call when a user adds a new Aftership tracking from HASS.""" + """Call when a user adds a new Aftership tracking from Home Assistant.""" title = call.data.get(CONF_TITLE) slug = call.data.get(CONF_SLUG) tracking_number = call.data[CONF_TRACKING_NUMBER] @@ -93,7 +93,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) async def handle_remove_tracking(call): - """Call when a user removes an Aftership tracking from HASS.""" + """Call when a user removes an Aftership tracking from Home Assistant.""" slug = call.data[CONF_SLUG] tracking_number = call.data[CONF_TRACKING_NUMBER] diff --git a/homeassistant/components/air_quality/__init__.py b/homeassistant/components/air_quality/__init__.py index 00308c40b36..29c6756260c 100644 --- a/homeassistant/components/air_quality/__init__.py +++ b/homeassistant/components/air_quality/__init__.py @@ -2,12 +2,12 @@ from datetime import timedelta import logging -from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/air_quality/manifest.json b/homeassistant/components/air_quality/manifest.json index 17fa14a9cfa..4c36cc0dd22 100644 --- a/homeassistant/components/air_quality/manifest.json +++ b/homeassistant/components/air_quality/manifest.json @@ -1,6 +1,6 @@ { "domain": "air_quality", - "name": "Air quality", + "name": "Air Quality", "documentation": "https://www.home-assistant.io/integrations/air_quality", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/airly/.translations/da.json b/homeassistant/components/airly/.translations/da.json index 652cc46a7b3..c2c14d1d101 100644 --- a/homeassistant/components/airly/.translations/da.json +++ b/homeassistant/components/airly/.translations/da.json @@ -3,7 +3,7 @@ "error": { "auth": "API-n\u00f8glen er ikke korrekt.", "name_exists": "Navnet findes allerede.", - "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." + "wrong_location": "Ingen Airly-m\u00e5lestationer i dette omr\u00e5de." }, "step": { "user": { @@ -13,7 +13,7 @@ "longitude": "L\u00e6ngdegrad", "name": "Integrationens navn" }, - "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "description": "Konfigurer Airly luftkvalitetsintegration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", "title": "Airly" } }, diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index ce165918ac2..17e1d27e571 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -10,7 +10,6 @@ import async_timeout from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle @@ -48,9 +47,6 @@ async def async_setup_entry(hass, config_entry): await airly.async_update() - if not airly.data: - raise ConfigEntryNotReady() - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly hass.async_create_task( diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 082344c14e3..b48a360da28 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -1,11 +1,11 @@ """Support for the Airly air_quality service.""" from homeassistant.components.air_quality import ( - AirQualityEntity, ATTR_AQI, - ATTR_PM_10, ATTR_PM_2_5, + ATTR_PM_10, + AirQualityEntity, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from .const import ( ATTR_API_ADVICE, @@ -35,10 +35,13 @@ LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly air_quality entity based on a config entry.""" name = config_entry.data[CONF_NAME] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + unique_id = f"{latitude}-{longitude}" data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] - async_add_entities([AirlyAirQuality(data, name)], True) + async_add_entities([AirlyAirQuality(data, name, unique_id)], True) def round_state(func): @@ -56,11 +59,12 @@ def round_state(func): class AirlyAirQuality(AirQualityEntity): """Define an Airly air quality.""" - def __init__(self, airly, name): + def __init__(self, airly, name, unique_id): """Initialize.""" self.airly = airly self.data = airly.data self._name = name + self._unique_id = unique_id self._pm_2_5 = None self._pm_10 = None self._aqi = None @@ -108,12 +112,12 @@ class AirlyAirQuality(AirQualityEntity): @property def unique_id(self): """Return a unique_id for this entity.""" - return f"{self.airly.latitude}-{self.airly.longitude}" + return self._unique_id @property def available(self): """Return True if entity is available.""" - return bool(self.airly.data) + return bool(self.data) @property def device_state_attributes(self): @@ -132,7 +136,6 @@ class AirlyAirQuality(AirQualityEntity): if self.airly.data: self.data = self.airly.data - - self._pm_10 = self.data[ATTR_API_PM10] - self._pm_2_5 = self.data[ATTR_API_PM25] - self._aqi = self.data[ATTR_API_CAQI] + self._pm_10 = self.data[ATTR_API_PM10] + self._pm_2_5 = self.data[ATTR_API_PM25] + self._aqi = self.data[ATTR_API_CAQI] diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index b361930fa7d..31cfec7e7aa 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -1,14 +1,14 @@ """Adds config flow for Airly.""" -import async_timeout -import voluptuous as vol from airly import Airly from airly.exceptions import AirlyError +import async_timeout +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index bce32d64041..af0eac39cdc 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -2,6 +2,8 @@ from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + CONF_LATITUDE, + CONF_LONGITUDE, CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -60,12 +62,16 @@ SENSOR_TYPES = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly sensor entities based on a config entry.""" name = config_entry.data[CONF_NAME] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] sensors = [] for sensor in SENSOR_TYPES: - sensors.append(AirlySensor(data, name, sensor)) + unique_id = f"{latitude}-{longitude}-{sensor.lower()}" + sensors.append(AirlySensor(data, name, sensor, unique_id)) + async_add_entities(sensors, True) @@ -84,11 +90,12 @@ def round_state(func): class AirlySensor(Entity): """Define an Airly sensor.""" - def __init__(self, airly, name, kind): + def __init__(self, airly, name, kind, unique_id): """Initialize.""" self.airly = airly self.data = airly.data self._name = name + self._unique_id = unique_id self.kind = kind self._device_class = None self._state = None @@ -130,7 +137,7 @@ class AirlySensor(Entity): @property def unique_id(self): """Return a unique_id for this entity.""" - return f"{self.airly.latitude}-{self.airly.longitude}-{self.kind.lower()}" + return self._unique_id @property def unit_of_measurement(self): @@ -140,7 +147,7 @@ class AirlySensor(Entity): @property def available(self): """Return True if entity is available.""" - return bool(self.airly.data) + return bool(self.data) async def async_update(self): """Update the sensor.""" diff --git a/homeassistant/components/airvisual/manifest.json b/homeassistant/components/airvisual/manifest.json index e7ea23a43a1..a689ee6acf0 100644 --- a/homeassistant/components/airvisual/manifest.json +++ b/homeassistant/components/airvisual/manifest.json @@ -1,12 +1,8 @@ { "domain": "airvisual", - "name": "Airvisual", + "name": "AirVisual", "documentation": "https://www.home-assistant.io/integrations/airvisual", - "requirements": [ - "pyairvisual==3.0.1" - ], + "requirements": ["pyairvisual==3.0.1"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 888d6ae6ec9..3b177c4ce67 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 a unique, Home Assistant friendly identifier for this entity.""" return f"{self._location_id}_{self._locale}_{self._type}" @property diff --git a/homeassistant/components/aladdin_connect/manifest.json b/homeassistant/components/aladdin_connect/manifest.json index 7d440407427..ca38f26ff1f 100644 --- a/homeassistant/components/aladdin_connect/manifest.json +++ b/homeassistant/components/aladdin_connect/manifest.json @@ -1,10 +1,8 @@ { "domain": "aladdin_connect", - "name": "Aladdin connect", + "name": "Aladdin Connect", "documentation": "https://www.home-assistant.io/integrations/aladdin_connect", - "requirements": [ - "aladdin_connect==0.3" - ], + "requirements": ["aladdin_connect==0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/alarm_control_panel/.translations/da.json b/homeassistant/components/alarm_control_panel/.translations/da.json index 74e02e10de4..220034d23e1 100644 --- a/homeassistant/components/alarm_control_panel/.translations/da.json +++ b/homeassistant/components/alarm_control_panel/.translations/da.json @@ -1,7 +1,18 @@ { "device_automation": { "action_type": { + "arm_away": "Tilkobl {entity_name} ude", + "arm_home": "Tilkobl {entity_name} hjemme", + "arm_night": "Tilkobl {entity_name} nat", + "disarm": "Frakobl {entity_name}", "trigger": "Udl\u00f8s {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} tilkoblet ude", + "armed_home": "{entity_name} tilkoblet hjemme", + "armed_night": "{entity_name} tilkoblet nat", + "disarmed": "{entity_name} frakoblet", + "triggered": "{entity_name} udl\u00f8st" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/de.json b/homeassistant/components/alarm_control_panel/.translations/de.json new file mode 100644 index 00000000000..3e94345138a --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/de.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "trigger_type": { + "armed_away": "{entity_name} Unterwegs", + "armed_home": "{entity_name} Zuhause", + "armed_night": "{entity_name} Nacht-Modus", + "disarmed": "{entity_name} deaktiviert", + "triggered": "{entity_name} ausgel\u00f6st" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json index 273efeeaba5..8200755de0f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/es.json +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -6,6 +6,13 @@ "arm_night": "Armar {entity_name} por la noche", "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} armado fuera", + "armed_home": "{entity_name} armado en casa", + "armed_night": "{entity_name} armado modo noche", + "disarmed": "{entity_name} desarmado", + "triggered": "{entity_name} activado" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/fr.json b/homeassistant/components/alarm_control_panel/.translations/fr.json index c3ba6db0c62..fbdc6a5605f 100644 --- a/homeassistant/components/alarm_control_panel/.translations/fr.json +++ b/homeassistant/components/alarm_control_panel/.translations/fr.json @@ -1,11 +1,18 @@ { "device_automation": { "action_type": { - "arm_away": "Armer {entity_name} mode sortie", - "arm_home": "Armer {entity_name} mode \u00e0 la maison", - "arm_night": "Armer {entity_name} mode nuit", + "arm_away": "Armer {entity_name} en mode \"sortie\"", + "arm_home": "Armer {entity_name} en mode \"maison\"", + "arm_night": "Armer {entity_name} en mode \"nuit\"", "disarm": "D\u00e9sarmer {entity_name}", "trigger": "D\u00e9clencheur {entity_name}" + }, + "trigger_type": { + "armed_away": "Armer {entity_name} en mode \"sortie\"", + "armed_home": "Armer {entity_name} en mode \"maison\"", + "armed_night": "Armer {entity_name} en mode \"nuit\"", + "disarmed": "{entity_name} d\u00e9sarm\u00e9", + "triggered": "{entity_name} d\u00e9clench\u00e9" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/hu.json b/homeassistant/components/alarm_control_panel/.translations/hu.json new file mode 100644 index 00000000000..b249a16c9f1 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/hu.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \u00e9les\u00edt\u00e9se t\u00e1voz\u00f3 m\u00f3dban", + "arm_home": "{entity_name} \u00e9les\u00edt\u00e9se otthon marad\u00f3 m\u00f3dban", + "arm_night": "{entity_name} \u00e9les\u00edt\u00e9se \u00e9jszakai m\u00f3dban", + "disarm": "{entity_name} hat\u00e1stalan\u00edt\u00e1sa", + "trigger": "{entity_name} riaszt\u00e1si esem\u00e9ny ind\u00edt\u00e1sa" + }, + "trigger_type": { + "armed_away": "{entity_name} t\u00e1voz\u00f3 m\u00f3dban lett \u00e9les\u00edtve", + "armed_home": "{entity_name} otthon marad\u00f3 m\u00f3dban lett \u00e9les\u00edtve", + "armed_night": "{entity_name} \u00e9jszakai m\u00f3dban lett \u00e9les\u00edtve", + "disarmed": "{entity_name} hat\u00e1stalan\u00edtva lett", + "triggered": "{entity_name} riaszt\u00e1sba ker\u00fclt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ko.json b/homeassistant/components/alarm_control_panel/.translations/ko.json index 5d6caa5fe12..b70ae8dc025 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ko.json +++ b/homeassistant/components/alarm_control_panel/.translations/ko.json @@ -6,6 +6,13 @@ "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44", "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c", "trigger": "{entity_name} \ud2b8\ub9ac\uac70" + }, + "trigger_type": { + "armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", + "armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", + "armed_night": "{entity_name} \uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", + "disarmed": "{entity_name} \uc774(\uac00) \ud574\uc81c\ub420 \ub54c", + "triggered": "{entity_name} \uc774(\uac00) \ud2b8\ub9ac\uac70\ub420 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json index 9329a089d32..6f26cc99e21 100644 --- a/homeassistant/components/alarm_control_panel/.translations/nl.json +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -6,6 +6,13 @@ "arm_night": "Inschakelen {entity_name} nacht", "disarm": "Uitschakelen {entity_name}", "trigger": "Trigger {entity_name}" + }, + "trigger_type": { + "armed_away": "{entity_name} afwezig ingeschakeld", + "armed_home": "{entity_name} thuis ingeschakeld", + "armed_night": "{entity_name} nachtstand ingeschakeld", + "disarmed": "{entity_name} uitgeschakeld", + "triggered": "{entity_name} geactiveerd" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/no.json b/homeassistant/components/alarm_control_panel/.translations/no.json index 108d273a0f0..0b58064fe09 100644 --- a/homeassistant/components/alarm_control_panel/.translations/no.json +++ b/homeassistant/components/alarm_control_panel/.translations/no.json @@ -8,9 +8,9 @@ "trigger": "Utl\u00f8ser {entity_name}" }, "trigger_type": { - "armed_away": "{entity_name} borte sikkring ", - "armed_home": "{entity_name} hjemme sikkring", - "armed_night": "{entity_name} natt sikkring", + "armed_away": "{entity_name} aktivert borte", + "armed_home": "{entity_name} aktivert hjemme", + "armed_night": "{entity_name} aktivert natt", "disarmed": "{entity_name} deaktivert", "triggered": "{entity_name} utl\u00f8st" } diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json index 156ede8851b..274aa8cb4c2 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -6,6 +6,13 @@ "arm_night": "Armar {entity_name} noite", "disarm": "Desarmar {entity_name}", "trigger": "Disparar {entidade_nome}" + }, + "trigger_type": { + "armed_away": "{entity_name} armado modo longe", + "armed_home": "{entity_name} armadado modo casa", + "armed_night": "{entity_name} armadado para noite", + "disarmed": "{entity_name} desarmado", + "triggered": "{entity_name} acionado" } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index dfac0fd192f..5fb44a18a0b 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -17,9 +17,9 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/alarm_control_panel/manifest.json b/homeassistant/components/alarm_control_panel/manifest.json index e877fe90a17..80c245b8d8f 100644 --- a/homeassistant/components/alarm_control_panel/manifest.json +++ b/homeassistant/components/alarm_control_panel/manifest.json @@ -1,8 +1,9 @@ { "domain": "alarm_control_panel", - "name": "Alarm control panel", + "name": "Alarm Control Panel", "documentation": "https://www.home-assistant.io/integrations/alarm_control_panel", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 93c0746a812..833156e98b2 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -24,6 +24,7 @@ CONF_DEVICE_BAUD = "baudrate" CONF_DEVICE_PATH = "path" CONF_DEVICE_PORT = "port" CONF_DEVICE_TYPE = "type" +CONF_AUTO_BYPASS = "autobypass" CONF_PANEL_DISPLAY = "panel_display" CONF_ZONE_NAME = "name" CONF_ZONE_TYPE = "type" @@ -39,6 +40,7 @@ DEFAULT_DEVICE_PORT = 10000 DEFAULT_DEVICE_PATH = "/dev/ttyUSB0" DEFAULT_DEVICE_BAUD = 115200 +DEFAULT_AUTO_BYPASS = False DEFAULT_PANEL_DISPLAY = False DEFAULT_ZONE_TYPE = "opening" @@ -102,6 +104,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional( CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY ): cv.boolean, + vol.Optional(CONF_AUTO_BYPASS, default=DEFAULT_AUTO_BYPASS): cv.boolean, vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, } ) diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index d2e9fd136a8..70f3e67e15b 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, FORMAT_NUMBER, + AlarmControlPanel, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -35,7 +35,7 @@ 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.""" - device = AlarmDecoderAlarmPanel() + device = AlarmDecoderAlarmPanel(discovery_info["autobypass"]) add_entities([device]) def alarm_toggle_chime_handler(service): @@ -66,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AlarmDecoderAlarmPanel(AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" - def __init__(self): + def __init__(self, auto_bypass): """Initialize the alarm panel.""" self._display = "" self._name = "Alarm Panel" @@ -80,6 +80,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanel): self._programming_mode = None self._ready = None self._zone_bypassed = None + self._auto_bypass = auto_bypass async def async_added_to_hass(self): """Register callbacks.""" @@ -158,11 +159,15 @@ class AlarmDecoderAlarmPanel(AlarmControlPanel): def alarm_arm_away(self, code=None): """Send arm away command.""" if code: + if self._auto_bypass: + self.hass.data[DATA_AD].send(f"{code!s}6#") self.hass.data[DATA_AD].send(f"{code!s}2") def alarm_arm_home(self, code=None): """Send arm home command.""" if code: + if self._auto_bypass: + self.hass.data[DATA_AD].send(f"{code!s}6#") self.hass.data[DATA_AD].send(f"{code!s}3") def alarm_arm_night(self, code=None): diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index 5ab69d94cf2..fd0e79cef8a 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -1,10 +1,8 @@ { "domain": "alarmdecoder", - "name": "Alarmdecoder", + "name": "AlarmDecoder", "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": [ - "alarmdecoder==1.13.2" - ], + "requirements": ["alarmdecoder==1.13.9"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/alarmdotcom/manifest.json b/homeassistant/components/alarmdotcom/manifest.json index fd5c3010ab6..9468649171a 100644 --- a/homeassistant/components/alarmdotcom/manifest.json +++ b/homeassistant/components/alarmdotcom/manifest.json @@ -1,10 +1,8 @@ { "domain": "alarmdotcom", - "name": "Alarmdotcom", + "name": "Alarm.com", "documentation": "https://www.home-assistant.io/integrations/alarmdotcom", - "requirements": [ - "pyalarmdotcom==0.3.2" - ], + "requirements": ["pyalarmdotcom==0.3.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index 420d730933c..3a473b17f17 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -1,30 +1,30 @@ """Support for repeating alerts when conditions are met.""" import asyncio -import logging from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( + ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE, - ATTR_DATA, DOMAIN as DOMAIN_NOTIFY, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_ENTITY_ID, - STATE_IDLE, CONF_NAME, CONF_STATE, - STATE_ON, - STATE_OFF, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, ) -from homeassistant.helpers import service, event +from homeassistant.helpers import event, service +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.util.dt import now @@ -213,7 +213,7 @@ class Alert(ToggleEntity): @property def should_poll(self): - """HASS need not poll these entities.""" + """Home Assistant need not poll these entities.""" return False @property diff --git a/homeassistant/components/alert/manifest.json b/homeassistant/components/alert/manifest.json index 06269390753..93c88655d34 100644 --- a/homeassistant/components/alert/manifest.json +++ b/homeassistant/components/alert/manifest.json @@ -4,8 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/alert", "requirements": [], "dependencies": [], - "after_dependencies": [ - "notify" - ], - "codeowners": [] + "after_dependencies": ["notify"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index cb0d093bb48..5861c4cc985 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -3,31 +3,33 @@ import logging import voluptuous as vol -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import entityfilter from homeassistant.const import CONF_NAME +from homeassistant.helpers import config_validation as cv, entityfilter from . import flash_briefings, intent, smart_home_http from .const import ( CONF_AUDIO, CONF_CLIENT_ID, CONF_CLIENT_SECRET, + CONF_DESCRIPTION, + CONF_DISPLAY_CATEGORIES, CONF_DISPLAY_URL, CONF_ENDPOINT, + CONF_ENTITY_CONFIG, + CONF_FILTER, + CONF_LOCALE, + CONF_SUPPORTED_LOCALES, CONF_TEXT, CONF_TITLE, CONF_UID, DOMAIN, - CONF_FILTER, - CONF_ENTITY_CONFIG, - CONF_DESCRIPTION, - CONF_DISPLAY_CATEGORIES, ) _LOGGER = logging.getLogger(__name__) CONF_FLASH_BRIEFINGS = "flash_briefings" CONF_SMART_HOME = "smart_home" +DEFAULT_LOCALE = "en-US" ALEXA_ENTITY_SCHEMA = vol.Schema( { @@ -42,6 +44,9 @@ SMART_HOME_SCHEMA = vol.Schema( vol.Optional(CONF_ENDPOINT): cv.string, vol.Optional(CONF_CLIENT_ID): cv.string, vol.Optional(CONF_CLIENT_SECRET): cv.string, + vol.Optional(CONF_LOCALE, default=DEFAULT_LOCALE): vol.In( + CONF_SUPPORTED_LOCALES + ), vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ALEXA_ENTITY_SCHEMA}, } diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 9f87a6d954e..94789c33305 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -1,8 +1,9 @@ """Support for Alexa skill auth.""" import asyncio +from datetime import timedelta import json import logging -from datetime import timedelta + import aiohttp import async_timeout @@ -50,7 +51,7 @@ class Auth: "client_secret": self.client_secret, } _LOGGER.debug( - "Calling LWA to get the access token (first time), " "with: %s", + "Calling LWA to get the access token (first time), with: %s", json.dumps(lwa_params), ) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 1f18cb7a590..26d07760747 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1,6 +1,10 @@ """Alexa capabilities.""" import logging +from homeassistant.components import cover, fan, image_processing, input_number, light +from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER +import homeassistant.components.climate.const as climate +import homeassistant.components.media_player.const as media_player from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, @@ -9,26 +13,19 @@ from homeassistant.const import ( STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, - STATE_CLOSED, STATE_LOCKED, STATE_OFF, STATE_ON, - STATE_OPEN, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNLOCKED, ) -import homeassistant.components.climate.const as climate -import homeassistant.components.media_player.const as media_player -from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER -from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util import homeassistant.util.dt as dt_util from .const import ( - Catalog, API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, @@ -38,6 +35,13 @@ from .const import ( Inputs, ) from .errors import UnsupportedProperty +from .resources import ( + AlexaCapabilityResource, + AlexaGlobalCatalog, + AlexaModeResource, + AlexaPresetResource, + AlexaSemantics, +) _LOGGER = logging.getLogger(__name__) @@ -52,6 +56,8 @@ class AlexaCapability: https://developer.amazon.com/docs/device-apis/message-guide.html """ + supported_locales = {"en-US"} + def __init__(self, entity, instance=None): """Initialize an Alexa capability.""" self.entity = entity @@ -108,12 +114,28 @@ class AlexaCapability: @staticmethod def capability_resources(): - """Applicable to ToggleController, RangeController, and ModeController interfaces.""" + """Return the capability object. + + Applicable to ToggleController, RangeController, and ModeController interfaces. + """ return [] @staticmethod def configuration(): - """Return the Configuration object.""" + """Return the configuration object. + + Applicable to the ThermostatController, SecurityControlPanel, ModeController, RangeController, + and EventDetectionSensor. + """ + return [] + + @staticmethod + def configurations(): + """Return the configurations object. + + The plural configurations object is different that the singular configuration object. + Applicable to EqualizerController interface. + """ return [] @staticmethod @@ -121,6 +143,14 @@ class AlexaCapability: """Applicable only to media players.""" return [] + @staticmethod + def semantics(): + """Return the semantics object. + + Applicable to ToggleController, RangeController, and ModeController interfaces. + """ + return [] + @staticmethod def supported_operations(): """Return the supportedOperations object.""" @@ -130,6 +160,10 @@ class AlexaCapability: """Serialize according to the Discovery API.""" result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} + instance = self.instance + if instance is not None: + result["instance"] = instance + properties_supported = self.properties_supported() if properties_supported: result["properties"] = { @@ -138,22 +172,19 @@ class AlexaCapability: "retrievable": self.properties_retrievable(), } - # pylint: disable=assignment-from-none proactively_reported = self.capability_proactively_reported() if proactively_reported is not None: result["proactivelyReported"] = proactively_reported - # pylint: disable=assignment-from-none non_controllable = self.properties_non_controllable() if non_controllable is not None: result["properties"]["nonControllable"] = non_controllable - # pylint: disable=assignment-from-none supports_deactivation = self.supports_deactivation() if supports_deactivation is not None: result["supportsDeactivation"] = supports_deactivation - capability_resources = self.serialize_capability_resources() + capability_resources = self.capability_resources() if capability_resources: result["capabilityResources"] = capability_resources @@ -161,10 +192,14 @@ class AlexaCapability: if configuration: result["configuration"] = configuration - # pylint: disable=assignment-from-none - instance = self.instance - if instance is not None: - result["instance"] = instance + # The plural configurations object is different than the singular configuration object above. + configurations = self.configurations() + if configurations: + result["configurations"] = configurations + + semantics = self.semantics() + if semantics: + result["semantics"] = semantics supported_operations = self.supported_operations() if supported_operations: @@ -196,36 +231,6 @@ class AlexaCapability: yield result - def serialize_capability_resources(self): - """Return capabilityResources friendlyNames serialized for an API response.""" - resources = self.capability_resources() - if resources: - return {"friendlyNames": self.serialize_friendly_names(resources)} - - return None - - @staticmethod - def serialize_friendly_names(resources): - """Return capabilityResources, ModeResources, or presetResources friendlyNames serialized for an API response.""" - friendly_names = [] - for resource in resources: - if resource["type"] == Catalog.LABEL_ASSET: - friendly_names.append( - { - "@type": Catalog.LABEL_ASSET, - "value": {"assetId": resource["value"]}, - } - ) - else: - friendly_names.append( - { - "@type": Catalog.LABEL_TEXT, - "value": {"text": resource["value"], "locale": "en-US"}, - } - ) - - return friendly_names - class Alexa(AlexaCapability): """Implements Alexa Interface. @@ -236,6 +241,21 @@ class Alexa(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-interface.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + } + def name(self): """Return the Alexa API name of this interface.""" return "Alexa" @@ -247,6 +267,19 @@ class AlexaEndpointHealth(AlexaCapability): https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def __init__(self, hass, entity): """Initialize the entity.""" super().__init__(entity) @@ -262,7 +295,7 @@ class AlexaEndpointHealth(AlexaCapability): def properties_proactively_reported(self): """Return True if properties asynchronously reported.""" - return False + return True def properties_retrievable(self): """Return True if properties can be retrieved.""" @@ -284,6 +317,19 @@ class AlexaPowerController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.PowerController" @@ -320,6 +366,17 @@ class AlexaLockController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-US", + "es-ES", + "it-IT", + "ja-JP", + } + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.LockController" @@ -354,6 +411,17 @@ class AlexaSceneController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html """ + supported_locales = { + "de-DE", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + } + def __init__(self, entity, supports_deactivation): """Initialize the entity.""" super().__init__(entity) @@ -370,6 +438,19 @@ class AlexaBrightnessController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.BrightnessController" @@ -401,6 +482,19 @@ class AlexaColorController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.ColorController" @@ -433,6 +527,19 @@ class AlexaColorTemperatureController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.ColorTemperatureController" @@ -462,6 +569,19 @@ class AlexaPercentageController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.PercentageController" @@ -496,6 +616,8 @@ class AlexaSpeaker(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-speaker.html """ + supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.Speaker" @@ -507,6 +629,8 @@ class AlexaStepSpeaker(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html """ + supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.StepSpeaker" @@ -518,6 +642,8 @@ class AlexaPlaybackController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html """ + supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US", "fr-FR"} + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.PlaybackController" @@ -551,6 +677,8 @@ class AlexaInputController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html """ + supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.InputController" @@ -579,6 +707,19 @@ class AlexaTemperatureSensor(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def __init__(self, hass, entity): """Initialize the entity.""" super().__init__(entity) @@ -634,6 +775,8 @@ class AlexaContactSensor(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-contactsensor.html """ + supported_locales = {"en-CA", "en-US"} + def __init__(self, hass, entity): """Initialize the entity.""" super().__init__(entity) @@ -671,6 +814,8 @@ class AlexaMotionSensor(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html """ + supported_locales = {"en-CA", "en-US"} + def __init__(self, hass, entity): """Initialize the entity.""" super().__init__(entity) @@ -708,6 +853,19 @@ class AlexaThermostatController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def __init__(self, hass, entity): """Initialize the entity.""" super().__init__(entity) @@ -816,6 +974,19 @@ class AlexaPowerLevelController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.PowerLevelController" @@ -851,6 +1022,8 @@ class AlexaSecurityPanelController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html """ + supported_locales = {"en-AU", "en-CA", "en-IN", "en-US"} + def __init__(self, hass, entity): """Initialize the entity.""" super().__init__(entity) @@ -903,9 +1076,26 @@ class AlexaModeController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + } + def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -922,108 +1112,102 @@ class AlexaModeController(AlexaCapability): def properties_retrievable(self): """Return True if properties can be retrieved.""" + return True def get_property(self, name): """Read and return a property.""" if name != "mode": raise UnsupportedProperty(name) + # Fan Direction if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - return self.entity.attributes.get(fan.ATTR_DIRECTION) + mode = self.entity.attributes.get(fan.ATTR_DIRECTION, None) + if mode in (fan.DIRECTION_FORWARD, fan.DIRECTION_REVERSE, STATE_UNKNOWN): + return f"{fan.ATTR_DIRECTION}.{mode}" + # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - return self.entity.attributes.get(cover.ATTR_POSITION) + # Return state instead of position when using ModeController. + mode = self.entity.state + if mode in ( + cover.STATE_OPEN, + cover.STATE_OPENING, + cover.STATE_CLOSED, + cover.STATE_CLOSING, + STATE_UNKNOWN, + ): + return f"{cover.ATTR_POSITION}.{mode}" return None def configuration(self): """Return configuration with modeResources.""" - return self.serialize_mode_resources() + if isinstance(self._resource, AlexaCapabilityResource): + return self._resource.serialize_configuration() + + return None def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Direction Resource if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - capability_resources = [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION} - ] + self._resource = AlexaModeResource( + [AlexaGlobalCatalog.SETTING_DIRECTION], False + ) + self._resource.add_mode( + f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", [fan.DIRECTION_FORWARD] + ) + self._resource.add_mode( + f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", [fan.DIRECTION_REVERSE] + ) + return self._resource.serialize_capability_resources() + # Cover Position Resources if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - capability_resources = [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_MODE}, - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_PRESET}, - ] + self._resource = AlexaModeResource( + ["Position", AlexaGlobalCatalog.SETTING_OPENING], False + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + [AlexaGlobalCatalog.VALUE_OPEN], + ) + self._resource.add_mode( + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + [AlexaGlobalCatalog.VALUE_CLOSE], + ) + self._resource.add_mode(f"{cover.ATTR_POSITION}.custom", ["Custom"]) + return self._resource.serialize_capability_resources() - return capability_resources + return None - def mode_resources(self): - """Return modeResources object.""" - mode_resources = None - if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - mode_resources = { - "ordered": False, - "resources": [ - { - "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_FORWARD} - ], - }, - { - "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_REVERSE} - ], - }, - ], - } + def semantics(self): + """Build and return semantics object.""" + # Cover Position if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - mode_resources = { - "ordered": False, - "resources": [ - { - "value": f"{cover.ATTR_POSITION}.{STATE_OPEN}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": "open"}, - {"type": Catalog.LABEL_TEXT, "value": "opened"}, - {"type": Catalog.LABEL_TEXT, "value": "raise"}, - {"type": Catalog.LABEL_TEXT, "value": "raised"}, - ], - }, - { - "value": f"{cover.ATTR_POSITION}.{STATE_CLOSED}", - "friendly_names": [ - {"type": Catalog.LABEL_TEXT, "value": "close"}, - {"type": Catalog.LABEL_TEXT, "value": "closed"}, - {"type": Catalog.LABEL_TEXT, "value": "shut"}, - {"type": Catalog.LABEL_TEXT, "value": "lower"}, - {"type": Catalog.LABEL_TEXT, "value": "lowered"}, - ], - }, - ], - } + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_CLOSE, AlexaSemantics.ACTION_LOWER], + "SetMode", + {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}"}, + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_OPEN, AlexaSemantics.ACTION_RAISE], + "SetMode", + {"mode": f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}"}, + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_CLOSED], + f"{cover.ATTR_POSITION}.{cover.STATE_CLOSED}", + ) + self._semantics.add_states_to_value( + [AlexaSemantics.STATES_OPEN], + f"{cover.ATTR_POSITION}.{cover.STATE_OPEN}", + ) + return self._semantics.serialize_semantics() - return mode_resources - - def serialize_mode_resources(self): - """Return ModeResources, friendlyNames serialized for an API response.""" - mode_resources = [] - resources = self.mode_resources() - ordered = resources["ordered"] - for resource in resources["resources"]: - mode_value = resource["value"] - friendly_names = resource["friendly_names"] - result = { - "value": mode_value, - "modeResources": { - "friendlyNames": self.serialize_friendly_names(friendly_names) - }, - } - mode_resources.append(result) - - return {"ordered": ordered, "supportedModes": mode_resources} + return None class AlexaRangeController(AlexaCapability): @@ -1032,9 +1216,26 @@ class AlexaRangeController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + } + def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -1058,88 +1259,137 @@ class AlexaRangeController(AlexaCapability): if name != "rangeValue": raise UnsupportedProperty(name) + # Fan Speed if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": speed = self.entity.attributes.get(fan.ATTR_SPEED) return RANGE_FAN_MAP.get(speed, 0) + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + return self.entity.attributes.get(cover.ATTR_CURRENT_POSITION) + + # Cover Tilt Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + return self.entity.attributes.get(cover.ATTR_CURRENT_TILT_POSITION) + + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + return float(self.entity.state) + return None def configuration(self): """Return configuration with presetResources.""" - return self.serialize_preset_resources() + if isinstance(self._resource, AlexaCapabilityResource): + return self._resource.serialize_configuration() + + return None def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Speed Resources if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}] - - return capability_resources - - def preset_resources(self): - """Return presetResources object.""" - preset_resources = [] - - if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": - preset_resources = { - "minimumValue": 1, - "maximumValue": 3, - "precision": 1, - "presets": [ - { - "rangeValue": 1, - "names": [ - { - "type": Catalog.LABEL_ASSET, - "value": Catalog.VALUE_MINIMUM, - }, - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_LOW}, - ], - }, - { - "rangeValue": 2, - "names": [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_MEDIUM} - ], - }, - { - "rangeValue": 3, - "names": [ - { - "type": Catalog.LABEL_ASSET, - "value": Catalog.VALUE_MAXIMUM, - }, - {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_HIGH}, - ], - }, - ], - } - - return preset_resources - - def serialize_preset_resources(self): - """Return PresetResources, friendlyNames serialized for an API response.""" - preset_resources = [] - resources = self.preset_resources() - for preset in resources["presets"]: - preset_resources.append( - { - "rangeValue": preset["rangeValue"], - "presetResources": { - "friendlyNames": self.serialize_friendly_names(preset["names"]) - }, - } + self._resource = AlexaPresetResource( + labels=[AlexaGlobalCatalog.SETTING_FAN_SPEED], + min_value=1, + max_value=3, + precision=1, ) + self._resource.add_preset( + value=1, + labels=[AlexaGlobalCatalog.VALUE_LOW, AlexaGlobalCatalog.VALUE_MINIMUM], + ) + self._resource.add_preset(value=2, labels=[AlexaGlobalCatalog.VALUE_MEDIUM]) + self._resource.add_preset( + value=3, + labels=[ + AlexaGlobalCatalog.VALUE_HIGH, + AlexaGlobalCatalog.VALUE_MAXIMUM, + ], + ) + return self._resource.serialize_capability_resources() - return { - "supportedRange": { - "minimumValue": resources["minimumValue"], - "maximumValue": resources["maximumValue"], - "precision": resources["precision"], - }, - "presets": preset_resources, - } + # Cover Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._resource = AlexaPresetResource( + ["Position", AlexaGlobalCatalog.SETTING_OPENING], + min_value=0, + max_value=100, + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() + + # Cover Tilt Position Resources + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + self._resource = AlexaPresetResource( + ["Tilt Position", AlexaGlobalCatalog.SETTING_OPENING], + min_value=0, + max_value=100, + precision=1, + unit=AlexaGlobalCatalog.UNIT_PERCENT, + ) + return self._resource.serialize_capability_resources() + + # Input Number Value + if self.instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + min_value = float(self.entity.attributes[input_number.ATTR_MIN]) + max_value = float(self.entity.attributes[input_number.ATTR_MAX]) + precision = float(self.entity.attributes.get(input_number.ATTR_STEP, 1)) + unit = self.entity.attributes.get(input_number.ATTR_UNIT_OF_MEASUREMENT) + + self._resource = AlexaPresetResource( + ["Value"], + min_value=min_value, + max_value=max_value, + precision=precision, + unit=unit, + ) + self._resource.add_preset( + value=min_value, labels=[AlexaGlobalCatalog.VALUE_MINIMUM] + ) + self._resource.add_preset( + value=max_value, labels=[AlexaGlobalCatalog.VALUE_MAXIMUM] + ) + return self._resource.serialize_capability_resources() + + return None + + def semantics(self): + """Build and return semantics object.""" + + # Cover Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_LOWER], "SetRangeValue", {"rangeValue": 0} + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_RAISE], "SetRangeValue", {"rangeValue": 100} + ) + self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + return self._semantics.serialize_semantics() + + # Cover Tilt Position + if self.instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + self._semantics = AlexaSemantics() + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_CLOSE], "SetRangeValue", {"rangeValue": 0} + ) + self._semantics.add_action_to_directive( + [AlexaSemantics.ACTION_OPEN], "SetRangeValue", {"rangeValue": 100} + ) + self._semantics.add_states_to_value([AlexaSemantics.STATES_CLOSED], value=0) + self._semantics.add_states_to_range( + [AlexaSemantics.STATES_OPEN], min_value=1, max_value=100 + ) + return self._semantics.serialize_semantics() + + return None class AlexaToggleController(AlexaCapability): @@ -1148,9 +1398,26 @@ class AlexaToggleController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html """ + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", + } + def __init__(self, entity, instance, non_controllable=False): """Initialize the entity.""" super().__init__(entity, instance) + self._resource = None + self._semantics = None self.properties_non_controllable = lambda: non_controllable def name(self): @@ -1174,6 +1441,7 @@ class AlexaToggleController(AlexaCapability): if name != "toggleState": raise UnsupportedProperty(name) + # Fan Oscillating if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING)) return "ON" if is_on else "OFF" @@ -1182,16 +1450,15 @@ class AlexaToggleController(AlexaCapability): def capability_resources(self): """Return capabilityResources object.""" - capability_resources = [] + # Fan Oscillating Resource if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": - capability_resources = [ - {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE}, - {"type": Catalog.LABEL_TEXT, "value": "Rotate"}, - {"type": Catalog.LABEL_TEXT, "value": "Rotation"}, - ] + self._resource = AlexaCapabilityResource( + [AlexaGlobalCatalog.SETTING_OSCILLATE, "Rotate", "Rotation"] + ) + return self._resource.serialize_capability_resources() - return capability_resources + return None class AlexaChannelController(AlexaCapability): @@ -1200,6 +1467,8 @@ class AlexaChannelController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html """ + supported_locales = {"de-DE", "en-AU", "en-CA", "en-GB", "en-IN", "en-US"} + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.ChannelController" @@ -1211,6 +1480,8 @@ class AlexaDoorbellEventSource(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html """ + supported_locales = {"en-US"} + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.DoorbellEventSource" @@ -1226,6 +1497,8 @@ class AlexaPlaybackStateReporter(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-playbackstatereporter.html """ + supported_locales = {"de-DE", "en-GB", "en-US", "fr-FR"} + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.PlaybackStateReporter" @@ -1262,6 +1535,121 @@ class AlexaSeekController(AlexaCapability): https://developer.amazon.com/docs/device-apis/alexa-seekcontroller.html """ + supported_locales = {"de-DE", "en-GB", "en-US"} + def name(self): """Return the Alexa API name of this interface.""" return "Alexa.SeekController" + + +class AlexaEventDetectionSensor(AlexaCapability): + """Implements Alexa.EventDetectionSensor. + + https://developer.amazon.com/docs/device-apis/alexa-eventdetectionsensor.html + """ + + supported_locales = {"en-US"} + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EventDetectionSensor" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "humanPresenceDetectionState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "humanPresenceDetectionState": + raise UnsupportedProperty(name) + + human_presence = "NOT_DETECTED" + state = self.entity.state + + # Return None for unavailable and unknown states. + # Allows the Alexa.EndpointHealth Interface to handle the unavailable state in a stateReport. + if state in (STATE_UNAVAILABLE, STATE_UNKNOWN, None): + return None + + if self.entity.domain == image_processing.DOMAIN: + if int(state): + human_presence = "DETECTED" + elif state == STATE_ON: + human_presence = "DETECTED" + + return {"value": human_presence} + + def configuration(self): + """Return supported detection types.""" + return { + "detectionMethods": ["AUDIO", "VIDEO"], + "detectionModes": { + "humanPresence": { + "featureAvailability": "ENABLED", + "supportsNotDetected": True, + } + }, + } + + +class AlexaEqualizerController(AlexaCapability): + """Implements Alexa.EqualizerController. + + https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-equalizercontroller.html + """ + + supported_locales = {"en-US"} + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.EqualizerController" + + def properties_supported(self): + """Return what properties this entity supports. + + Either bands, mode or both can be specified. Only mode is supported at this time. + """ + return [{"name": "mode"}] + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + sound_mode = self.entity.attributes.get(media_player.ATTR_SOUND_MODE) + if sound_mode and sound_mode.upper() in ( + "MOVIE", + "MUSIC", + "NIGHT", + "SPORT", + "TV", + ): + return sound_mode.upper() + + return None + + def configurations(self): + """Return the sound modes supported in the configurations object. + + Valid Values for modes are: MOVIE, MUSIC, NIGHT, SPORT, TV. + """ + configurations = None + sound_mode_list = self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) + if sound_mode_list: + supported_sound_modes = [] + for sound_mode in sound_mode_list: + if sound_mode.upper() in ("MOVIE", "MUSIC", "NIGHT", "SPORT", "TV"): + supported_sound_modes.append({"name": sound_mode.upper()}) + + configurations = {"modes": {"supported": supported_sound_modes}} + + return configurations diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index f98337d71c5..bd579dc4dad 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -1,10 +1,12 @@ """Config helpers for Alexa.""" +from abc import ABC, abstractmethod + from homeassistant.core import callback from .state_report import async_enable_proactive_mode -class AbstractConfig: +class AbstractConfig(ABC): """Hold the configuration for Alexa.""" _unsub_proactive_report = None @@ -28,6 +30,11 @@ class AbstractConfig: """Endpoint for report state.""" return None + @property + @abstractmethod + def locale(self): + """Return config locale.""" + @property def entity_config(self): """Return entity config.""" diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 1aa9d4f2c1d..f5f19bbf955 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -1,9 +1,9 @@ """Constants for the Alexa integration.""" from collections import OrderedDict -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT -from homeassistant.components.climate import const as climate from homeassistant.components import fan +from homeassistant.components.climate import const as climate +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT DOMAIN = "alexa" @@ -19,6 +19,7 @@ CONF_ENTITY_CONFIG = "entity_config" CONF_ENDPOINT = "endpoint" CONF_CLIENT_ID = "client_id" CONF_CLIENT_SECRET = "client_secret" +CONF_LOCALE = "locale" ATTR_UID = "uid" ATTR_UPDATE_DATE = "updateDate" @@ -42,6 +43,20 @@ API_CHANGE = "change" CONF_DESCRIPTION = "description" CONF_DISPLAY_CATEGORIES = "display_categories" +CONF_SUPPORTED_LOCALES = ( + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "es-MX", + "fr-CA", + "fr-FR", + "it-IT", + "ja-JP", +) API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} @@ -117,163 +132,6 @@ class Cause: VOICE_INTERACTION = "VOICE_INTERACTION" -class Catalog: - """The Global Alexa catalog. - - https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog - - You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. - This catalog is localized into all the languages that Alexa supports. - - You can reference the following catalog of pre-defined friendly names. - Each item in the following list is an asset identifier followed by its supported friendly names. - The first friendly name for each identifier is the one displayed in the Alexa mobile app. - """ - - LABEL_ASSET = "asset" - LABEL_TEXT = "text" - - # Shower - DEVICENAME_SHOWER = "Alexa.DeviceName.Shower" - - # Washer, Washing Machine - DEVICENAME_WASHER = "Alexa.DeviceName.Washer" - - # Router, Internet Router, Network Router, Wifi Router, Net Router - DEVICENAME_ROUTER = "Alexa.DeviceName.Router" - - # Fan, Blower - DEVICENAME_FAN = "Alexa.DeviceName.Fan" - - # Air Purifier, Air Cleaner,Clean Air Machine - DEVICENAME_AIRPURIFIER = "Alexa.DeviceName.AirPurifier" - - # Space Heater, Portable Heater - DEVICENAME_SPACEHEATER = "Alexa.DeviceName.SpaceHeater" - - # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet - SHOWER_RAINHEAD = "Alexa.Shower.RainHead" - - # Handheld Shower, Shower Wand, Hand Shower - SHOWER_HANDHELD = "Alexa.Shower.HandHeld" - - # Water Temperature, Water Temp, Water Heat - SETTING_WATERTEMPERATURE = "Alexa.Setting.WaterTemperature" - - # Temperature, Temp - SETTING_TEMPERATURE = "Alexa.Setting.Temperature" - - # Wash Cycle, Wash Preset, Wash setting - SETTING_WASHCYCLE = "Alexa.Setting.WashCycle" - - # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi - SETTING_2GGUESTWIFI = "Alexa.Setting.2GGuestWiFi" - - # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi - SETTING_5GGUESTWIFI = "Alexa.Setting.5GGuestWiFi" - - # Guest Wi-fi, Guest Network, Guest Net - SETTING_GUESTWIFI = "Alexa.Setting.GuestWiFi" - - # Auto, Automatic, Automatic Mode, Auto Mode - SETTING_AUTO = "Alexa.Setting.Auto" - - # #Night, Night Mode - SETTING_NIGHT = "Alexa.Setting.Night" - - # Quiet, Quiet Mode, Noiseless, Silent - SETTING_QUIET = "Alexa.Setting.Quiet" - - # Oscillate, Swivel, Oscillation, Spin, Back and forth - SETTING_OSCILLATE = "Alexa.Setting.Oscillate" - - # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity - SETTING_FANSPEED = "Alexa.Setting.FanSpeed" - - # Preset, Setting - SETTING_PRESET = "Alexa.Setting.Preset" - - # Mode - SETTING_MODE = "Alexa.Setting.Mode" - - # Direction - SETTING_DIRECTION = "Alexa.Setting.Direction" - - # Delicates, Delicate - VALUE_DELICATE = "Alexa.Value.Delicate" - - # Quick Wash, Fast Wash, Wash Quickly, Speed Wash - VALUE_QUICKWASH = "Alexa.Value.QuickWash" - - # Maximum, Max - VALUE_MAXIMUM = "Alexa.Value.Maximum" - - # Minimum, Min - VALUE_MINIMUM = "Alexa.Value.Minimum" - - # High - VALUE_HIGH = "Alexa.Value.High" - - # Low - VALUE_LOW = "Alexa.Value.Low" - - # Medium, Mid - VALUE_MEDIUM = "Alexa.Value.Medium" - - -class Unit: - """Alexa Units of Measure. - - https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#units-of-measure - """ - - ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" - - ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" - - DISTANCE_FEET = "Alexa.Unit.Distance.Feet" - - DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" - - DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" - - DISTANCE_METERS = "Alexa.Unit.Distance.Meters" - - DISTANCE_MILES = "Alexa.Unit.Distance.Miles" - - DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" - - MASS_GRAMS = "Alexa.Unit.Mass.Grams" - - MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" - - PERCENT = "Alexa.Unit.Percent" - - TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" - - TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" - - TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" - - TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" - - VOLUME_CUBICFEET = "Alexa.Unit.Volume.CubicFeet" - - VOLUME_CUBICMETERS = "Alexa.Unit.Volume.CubicMeters" - - VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" - - VOLUME_LITERS = "Alexa.Unit.Volume.Liters" - - VOLUME_PINTS = "Alexa.Unit.Volume.Pints" - - VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" - - WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" - - WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" - - class Inputs: """Valid names for the InputController. @@ -353,3 +211,11 @@ class Inputs: "video3": "VIDEO 3", "xbox": "XBOX", } + + VALID_SOUND_MODE_MAP = { + "movie": "MOVIE", + "music": "MUSIC", + "night": "NIGHT", + "sport": "SPORT", + "tv": "TV", + } diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f9463949b58..d6fa0415640 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -1,7 +1,26 @@ """Alexa entity adapters.""" from typing import List -from homeassistant.core import callback +from homeassistant.components import ( + alarm_control_panel, + alert, + automation, + binary_sensor, + cover, + fan, + group, + image_processing, + input_boolean, + input_number, + light, + lock, + media_player, + scene, + script, + sensor, + switch, +) +from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_SUPPORTED_FEATURES, @@ -11,27 +30,9 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import callback from homeassistant.util.decorator import Registry -from homeassistant.components.climate import const as climate -from homeassistant.components import ( - alarm_control_panel, - alert, - automation, - binary_sensor, - cover, - fan, - group, - input_boolean, - light, - lock, - media_player, - scene, - script, - sensor, - switch, -) -from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( Alexa, AlexaBrightnessController, @@ -41,6 +42,8 @@ from .capabilities import ( AlexaContactSensor, AlexaDoorbellEventSource, AlexaEndpointHealth, + AlexaEqualizerController, + AlexaEventDetectionSensor, AlexaInputController, AlexaLockController, AlexaModeController, @@ -60,6 +63,7 @@ from .capabilities import ( AlexaThermostatController, AlexaToggleController, ) +from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES ENTITY_ADAPTERS = Registry() @@ -81,6 +85,9 @@ class DisplayCategory: # Indicates media devices with video or photo capabilities. CAMERA = "CAMERA" + # Indicates a non-mobile computer, such as a desktop computer. + COMPUTER = "COMPUTER" + # Indicates an endpoint that detects and reports contact. CONTACT_SENSOR = "CONTACT_SENSOR" @@ -90,27 +97,60 @@ class DisplayCategory: # Indicates a doorbell. DOORBELL = "DOORBELL" + # Indicates a window covering on the outside of a structure. + EXTERIOR_BLIND = "EXTERIOR_BLIND" + # Indicates a fan. FAN = "FAN" + # Indicates a game console, such as Microsoft Xbox or Nintendo Switch + GAME_CONSOLE = "GAME_CONSOLE" + + # Indicates a garage door. Garage doors must implement the ModeController interface to open and close the door. + GARAGE_DOOR = "GARAGE_DOOR" + + # Indicates a window covering on the inside of a structure. + INTERIOR_BLIND = "INTERIOR_BLIND" + + # Indicates a laptop or other mobile computer. + LAPTOP = "LAPTOP" + # Indicates light sources or fixtures. LIGHT = "LIGHT" # Indicates a microwave oven. MICROWAVE = "MICROWAVE" + # Indicates a mobile phone. + MOBILE_PHONE = "MOBILE_PHONE" + # Indicates an endpoint that detects and reports motion. MOTION_SENSOR = "MOTION_SENSOR" + # Indicates a network-connected music system. + MUSIC_SYSTEM = "MUSIC_SYSTEM" + # An endpoint that cannot be described in on of the other categories. OTHER = "OTHER" + # Indicates a network router. + NETWORK_HARDWARE = "NETWORK_HARDWARE" + + # Indicates an oven cooking appliance. + OVEN = "OVEN" + + # Indicates a non-mobile phone, such as landline or an IP phone. + PHONE = "PHONE" + # Describes a combination of devices set to a specific state, when the # order of the state change is not important. For example a bedtime scene # might include turning off lights and lowering the thermostat, but the # order is unimportant. Applies to Scenes SCENE_TRIGGER = "SCENE_TRIGGER" + # Indicates a projector screen. + SCREEN = "SCREEN" + # Indicates a security panel. SECURITY_PANEL = "SECURITY_PANEL" @@ -124,10 +164,16 @@ class DisplayCategory: # Indicates the endpoint is a speaker or speaker system. SPEAKER = "SPEAKER" + # Indicates a streaming device such as Apple TV, Chromecast, or Roku. + STREAMING_DEVICE = "STREAMING_DEVICE" + # Indicates in-wall switches wired to the electrical system. Can control a # variety of devices. SWITCH = "SWITCH" + # Indicates a tablet computer. + TABLET = "TABLET" + # Indicates endpoints that report the temperature only. TEMPERATURE_SENSOR = "TEMPERATURE_SENSOR" @@ -138,6 +184,9 @@ class DisplayCategory: # Indicates the endpoint is a television. TV = "TV" + # Indicates a network-connected wearable device, such as an Apple Watch, Fitbit, or Samsung Gear. + WEARABLE = "WEARABLE" + class AlexaEntity: """An adaptation of an entity, expressed in Alexa's terms. @@ -211,16 +260,24 @@ class AlexaEntity: def serialize_discovery(self): """Serialize the entity for discovery.""" - return { + result = { "displayCategories": self.display_categories(), "cookie": {}, "endpointId": self.alexa_id(), "friendlyName": self.friendly_name(), "description": self.description(), "manufacturerName": "Home Assistant", - "capabilities": [i.serialize_discovery() for i in self.interfaces()], } + locale = self.config.locale + capabilities = [] + for i in self.interfaces(): + if locale in i.supported_locales: + capabilities.append(i.serialize_discovery()) + result["capabilities"] = capabilities + + return result + @callback def async_get_entities(hass, config) -> List[AlexaEntity]: @@ -316,20 +373,40 @@ class CoverCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_DOOR): + if device_class == cover.DEVICE_CLASS_GARAGE: + return [DisplayCategory.GARAGE_DOOR] + if device_class == cover.DEVICE_CLASS_DOOR: return [DisplayCategory.DOOR] + if device_class in ( + cover.DEVICE_CLASS_BLIND, + cover.DEVICE_CLASS_SHADE, + cover.DEVICE_CLASS_CURTAIN, + ): + return [DisplayCategory.INTERIOR_BLIND] + if device_class in ( + cover.DEVICE_CLASS_WINDOW, + cover.DEVICE_CLASS_AWNING, + cover.DEVICE_CLASS_SHUTTER, + ): + return [DisplayCategory.EXTERIOR_BLIND] + return [DisplayCategory.OTHER] def interfaces(self): """Yield the supported interfaces.""" - yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & cover.SUPPORT_SET_POSITION: - yield AlexaPercentageController(self.entity) - if supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN): + yield AlexaRangeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" + ) + elif supported & (cover.SUPPORT_CLOSE | cover.SUPPORT_OPEN): yield AlexaModeController( self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_POSITION}" ) + if supported & cover.SUPPORT_SET_TILT_POSITION: + yield AlexaRangeController( + self.entity, instance=f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}" + ) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) @@ -353,6 +430,7 @@ class LightCapabilities(AlexaEntity): yield AlexaColorController(self.entity) if supported & light.SUPPORT_COLOR_TEMP: yield AlexaColorTemperatureController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) @@ -368,6 +446,7 @@ class FanCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" yield AlexaPowerController(self.entity) + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) @@ -375,7 +454,6 @@ class FanCapabilities(AlexaEntity): yield AlexaRangeController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}" ) - if supported & fan.SUPPORT_OSCILLATE: yield AlexaToggleController( self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" @@ -453,6 +531,9 @@ class MediaPlayerCapabilities(AlexaEntity): if supported & media_player.const.SUPPORT_PLAY_MEDIA: yield AlexaChannelController(self.entity) + if supported & media_player.const.SUPPORT_SELECT_SOUND_MODE: + yield AlexaEqualizerController(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) @@ -522,6 +603,7 @@ class BinarySensorCapabilities(AlexaEntity): TYPE_CONTACT = "contact" TYPE_MOTION = "motion" + TYPE_PRESENCE = "presence" def default_display_categories(self): """Return the display categories for this entity.""" @@ -530,6 +612,8 @@ class BinarySensorCapabilities(AlexaEntity): return [DisplayCategory.CONTACT_SENSOR] if sensor_type is self.TYPE_MOTION: return [DisplayCategory.MOTION_SENSOR] + if sensor_type is self.TYPE_PRESENCE: + return [DisplayCategory.CAMERA] def interfaces(self): """Yield the supported interfaces.""" @@ -538,7 +622,10 @@ class BinarySensorCapabilities(AlexaEntity): yield AlexaContactSensor(self.hass, self.entity) elif sensor_type is self.TYPE_MOTION: yield AlexaMotionSensor(self.hass, self.entity) + elif sensor_type is self.TYPE_PRESENCE: + yield AlexaEventDetectionSensor(self.hass, self.entity) + # yield additional interfaces based on specified display category in config. entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) if CONF_DISPLAY_CATEGORIES in entity_conf: if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: @@ -547,6 +634,8 @@ class BinarySensorCapabilities(AlexaEntity): yield AlexaContactSensor(self.hass, self.entity) elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.MOTION_SENSOR: yield AlexaMotionSensor(self.hass, self.entity) + elif entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.CAMERA: + yield AlexaEventDetectionSensor(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) @@ -554,11 +643,20 @@ class BinarySensorCapabilities(AlexaEntity): def get_type(self): """Return the type of binary sensor.""" attrs = self.entity.attributes - if attrs.get(ATTR_DEVICE_CLASS) in ("door", "garage_door", "opening", "window"): + if attrs.get(ATTR_DEVICE_CLASS) in ( + binary_sensor.DEVICE_CLASS_DOOR, + binary_sensor.DEVICE_CLASS_GARAGE_DOOR, + binary_sensor.DEVICE_CLASS_OPENING, + binary_sensor.DEVICE_CLASS_WINDOW, + ): return self.TYPE_CONTACT - if attrs.get(ATTR_DEVICE_CLASS) == "motion": + + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_MOTION: return self.TYPE_MOTION + if attrs.get(ATTR_DEVICE_CLASS) == binary_sensor.DEVICE_CLASS_PRESENCE: + return self.TYPE_PRESENCE + @ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN) class AlarmControlPanelCapabilities(AlexaEntity): @@ -574,3 +672,36 @@ class AlarmControlPanelCapabilities(AlexaEntity): yield AlexaSecurityPanelController(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(image_processing.DOMAIN) +class ImageProcessingCapabilities(AlexaEntity): + """Class to represent image_processing capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.CAMERA] + + def interfaces(self): + """Yield the supported interfaces.""" + yield AlexaEventDetectionSensor(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(input_number.DOMAIN) +class InputNumberCapabilities(AlexaEntity): + """Class to represent input_number capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.OTHER] + + def interfaces(self): + """Yield the supported interfaces.""" + + yield AlexaRangeController( + self.entity, instance=f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}" + ) + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) diff --git a/homeassistant/components/alexa/flash_briefings.py b/homeassistant/components/alexa/flash_briefings.py index 0b5c1243764..45d31d6088a 100644 --- a/homeassistant/components/alexa/flash_briefings.py +++ b/homeassistant/components/alexa/flash_briefings.py @@ -3,10 +3,10 @@ import copy import logging import uuid -import homeassistant.util.dt as dt_util from homeassistant.components import http from homeassistant.core import callback from homeassistant.helpers import template +import homeassistant.util.dt as dt_util from .const import ( ATTR_MAIN_TEXT, diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index f1aa260e88e..74c1b24d42b 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -3,7 +3,14 @@ import logging import math from homeassistant import core as ha -from homeassistant.components import cover, fan, group, light, media_player +from homeassistant.components import ( + cover, + fan, + group, + input_number, + light, + media_player, +) from homeassistant.components.climate import const as climate from homeassistant.const import ( ATTR_ENTITY_ID, @@ -20,6 +27,7 @@ from homeassistant.const import ( SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, SERVICE_UNLOCK, @@ -28,26 +36,24 @@ from homeassistant.const import ( SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, STATE_ALARM_DISARMED, - STATE_CLOSED, - STATE_OPEN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) import homeassistant.util.color as color_util -import homeassistant.util.dt as dt_util from homeassistant.util.decorator import Registry +import homeassistant.util.dt as dt_util from homeassistant.util.temperature import convert as convert_temperature from .const import ( API_TEMP_UNITS, - API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_MODES, + API_THERMOSTAT_MODES_CUSTOM, API_THERMOSTAT_PRESETS, - Cause, - Inputs, PERCENTAGE_FAN_MAP, RANGE_FAN_MAP, SPEED_FAN_MAP, + Cause, + Inputs, ) from .entities import async_get_entities from .errors import ( @@ -113,9 +119,7 @@ async def async_api_turn_on(hass, config, directive, context): domain = ha.DOMAIN service = SERVICE_TURN_ON - if domain == cover.DOMAIN: - service = cover.SERVICE_OPEN_COVER - elif domain == media_player.DOMAIN: + if domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: @@ -141,9 +145,7 @@ async def async_api_turn_off(hass, config, directive, context): domain = ha.DOMAIN service = SERVICE_TURN_OFF - if entity.domain == cover.DOMAIN: - service = cover.SERVICE_CLOSE_COVER - elif domain == media_player.DOMAIN: + if domain == media_player.DOMAIN: supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) power_features = media_player.SUPPORT_TURN_ON | media_player.SUPPORT_TURN_OFF if not supported & power_features: @@ -348,10 +350,6 @@ async def async_api_set_percentage(hass, config, directive, context): speed = "high" data[fan.ATTR_SPEED] = speed - elif entity.domain == cover.DOMAIN: - service = SERVICE_SET_COVER_POSITION - data[cover.ATTR_POSITION] = percentage - await hass.services.async_call( entity.domain, service, data, blocking=False, context=context ) @@ -385,13 +383,6 @@ async def async_api_adjust_percentage(hass, config, directive, context): data[fan.ATTR_SPEED] = speed - elif entity.domain == cover.DOMAIN: - service = SERVICE_SET_COVER_POSITION - - current = entity.attributes.get(cover.ATTR_POSITION) - - data[cover.ATTR_POSITION] = max(0, percentage_delta + current) - await hass.services.async_call( entity.domain, service, data, blocking=False, context=context ) @@ -421,6 +412,10 @@ async def async_api_lock(hass, config, directive, context): @HANDLERS.register(("Alexa.LockController", "Unlock")) async def async_api_unlock(hass, config, directive, context): """Process an unlock request.""" + if config.locale not in {"de-DE", "en-US", "ja-JP"}: + msg = f"The unlock directive is not supported for the following locales: {config.locale}" + raise AlexaInvalidDirectiveError(msg) + entity = directive.entity await hass.services.async_call( entity.domain, @@ -960,32 +955,35 @@ async def async_api_disarm(hass, config, directive, context): @HANDLERS.register(("Alexa.ModeController", "SetMode")) async def async_api_set_mode(hass, config, directive, context): - """Process a next request.""" + """Process a SetMode directive.""" entity = directive.entity instance = directive.instance domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - capability_mode = directive.payload["mode"] - - if domain not in (fan.DOMAIN, cover.DOMAIN): - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + mode = directive.payload["mode"] + # Fan Direction if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": - _, direction = capability_mode.split(".") + _, direction = mode.split(".") if direction in (fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD): service = fan.SERVICE_SET_DIRECTION data[fan.ATTR_DIRECTION] = direction - if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": - _, position = capability_mode.split(".") + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + _, position = mode.split(".") - if position == STATE_CLOSED: + if position == cover.STATE_CLOSED: service = cover.SERVICE_CLOSE_COVER - - if position == STATE_OPEN: + elif position == cover.STATE_OPEN: service = cover.SERVICE_OPEN_COVER + elif position == "custom": + service = cover.SERVICE_STOP_COVER + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context @@ -997,7 +995,7 @@ async def async_api_set_mode(hass, config, directive, context): "namespace": "Alexa.ModeController", "instance": instance, "name": "mode", - "value": capability_mode, + "value": mode, } ) @@ -1008,24 +1006,13 @@ async def async_api_set_mode(hass, config, directive, context): async def async_api_adjust_mode(hass, config, directive, context): """Process a AdjustMode request. - Requires modeResources to be ordered. - Only modes that are ordered support the adjustMode directive. + Requires capabilityResources supportedModes to be ordered. + Only supportedModes with ordered=True support the adjustMode directive. """ - entity = directive.entity - instance = directive.instance - domain = entity.domain - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - - if instance is None: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - - # No modeResources are currently ordered to support this request. - - return directive.response() + # Currently no supportedModes are configured with ordered=True to support this request. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) @HANDLERS.register(("Alexa.ToggleController", "TurnOn")) @@ -1037,19 +1024,29 @@ async def async_api_toggle_on(hass, config, directive, context): service = None data = {ATTR_ENTITY_ID: entity.entity_id} - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - + # Fan Oscillating if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": service = fan.SERVICE_OSCILLATE data[fan.ATTR_OSCILLATING] = True + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ToggleController", + "instance": instance, + "name": "toggleState", + "value": "ON", + } + ) + + return response @HANDLERS.register(("Alexa.ToggleController", "TurnOff")) @@ -1061,19 +1058,29 @@ async def async_api_toggle_off(hass, config, directive, context): service = None data = {ATTR_ENTITY_ID: entity.entity_id} - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) - + # Fan Oscillating if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": service = fan.SERVICE_OSCILLATE data[fan.ATTR_OSCILLATING] = False + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.ToggleController", + "instance": instance, + "name": "toggleState", + "value": "OFF", + } + ) + + return response @HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) @@ -1084,15 +1091,12 @@ async def async_api_set_range(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - range_value = int(directive.payload["rangeValue"]) - - if domain != fan.DOMAIN: - msg = "Entity does not support directive" - raise AlexaInvalidDirectiveError(msg) + range_value = directive.payload["rangeValue"] + # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": service = fan.SERVICE_SET_SPEED - speed = SPEED_FAN_MAP.get(range_value, None) + speed = SPEED_FAN_MAP.get(int(range_value)) if not speed: msg = "Entity does not support value" @@ -1103,11 +1107,55 @@ async def async_api_set_range(hass, config, directive, context): data[fan.ATTR_SPEED] = speed + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_value = int(range_value) + if range_value == 0: + service = cover.SERVICE_CLOSE_COVER + elif range_value == 100: + service = cover.SERVICE_OPEN_COVER + else: + service = cover.SERVICE_SET_COVER_POSITION + data[cover.ATTR_POSITION] = range_value + + # Cover Tilt Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + range_value = int(range_value) + if range_value == 0: + service = cover.SERVICE_CLOSE_COVER_TILT + elif range_value == 100: + service = cover.SERVICE_OPEN_COVER_TILT + else: + service = cover.SERVICE_SET_COVER_TILT_POSITION + data[cover.ATTR_POSITION] = range_value + + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_value = float(range_value) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + data[input_number.ATTR_VALUE] = min(max_value, max(min_value, range_value)) + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.RangeController", + "instance": instance, + "name": "rangeValue", + "value": range_value, + } + ) + + return response @HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) @@ -1118,25 +1166,71 @@ async def async_api_adjust_range(hass, config, directive, context): domain = entity.domain service = None data = {ATTR_ENTITY_ID: entity.entity_id} - range_delta = int(directive.payload["rangeValueDelta"]) + range_delta = directive.payload["rangeValueDelta"] + response_value = 0 + # Fan Speed if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + range_delta = int(range_delta) service = fan.SERVICE_SET_SPEED - - # adjust range current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) - speed = SPEED_FAN_MAP.get(max(0, range_delta + current_range), fan.SPEED_OFF) + speed = SPEED_FAN_MAP.get( + min(3, max(0, range_delta + current_range)), fan.SPEED_OFF + ) if speed == fan.SPEED_OFF: service = fan.SERVICE_TURN_OFF - data[fan.ATTR_SPEED] = speed + data[fan.ATTR_SPEED] = response_value = speed + + # Cover Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}": + range_delta = int(range_delta) + service = SERVICE_SET_COVER_POSITION + current = entity.attributes.get(cover.ATTR_POSITION) + data[cover.ATTR_POSITION] = response_value = min( + 100, max(0, range_delta + current) + ) + + # Cover Tilt Position + elif instance == f"{cover.DOMAIN}.{cover.ATTR_TILT_POSITION}": + range_delta = int(range_delta) + service = SERVICE_SET_COVER_TILT_POSITION + current = entity.attributes.get(cover.ATTR_TILT_POSITION) + data[cover.ATTR_TILT_POSITION] = response_value = min( + 100, max(0, range_delta + current) + ) + + # Input Number Value + elif instance == f"{input_number.DOMAIN}.{input_number.ATTR_VALUE}": + range_delta = float(range_delta) + service = input_number.SERVICE_SET_VALUE + min_value = float(entity.attributes[input_number.ATTR_MIN]) + max_value = float(entity.attributes[input_number.ATTR_MAX]) + current = float(entity.state) + data[input_number.ATTR_VALUE] = response_value = min( + max_value, max(min_value, range_delta + current) + ) + + else: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) await hass.services.async_call( domain, service, data, blocking=False, context=context ) - return directive.response() + response = directive.response() + response.add_context_property( + { + "namespace": "Alexa.RangeController", + "instance": instance, + "name": "rangeValue", + "value": response_value, + } + ) + + return response @HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) @@ -1262,3 +1356,43 @@ async def async_api_seek(hass, config, directive, context): return directive.response( name="StateReport", namespace="Alexa.SeekController", payload=payload ) + + +@HANDLERS.register(("Alexa.EqualizerController", "SetMode")) +async def async_api_set_eq_mode(hass, config, directive, context): + """Process a SetMode request for EqualizerController.""" + mode = directive.payload["mode"] + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST) + if sound_mode_list and mode.lower() in sound_mode_list: + data[media_player.const.ATTR_SOUND_MODE] = mode.lower() + else: + msg = "failed to map sound mode {} to a mode on {}".format( + mode, entity.entity_id + ) + raise AlexaInvalidValueError(msg) + + await hass.services.async_call( + entity.domain, + media_player.SERVICE_SELECT_SOUND_MODE, + data, + blocking=False, + context=context, + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.EqualizerController", "AdjustBands")) +@HANDLERS.register(("Alexa.EqualizerController", "ResetBands")) +@HANDLERS.register(("Alexa.EqualizerController", "SetBands")) +async def async_api_bands_directive(hass, config, directive, context): + """Handle an AdjustBands, ResetBands, SetBands request. + + Only mode directives are currently supported for the EqualizerController. + """ + # Currently bands directives are not supported. + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index ad0f1c33d49..5334cf765b8 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -1,11 +1,8 @@ { "domain": "alexa", - "name": "Alexa", + "name": "Amazon Alexa", "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], "dependencies": ["http"], - "codeowners": [ - "@home-assistant/cloud", - "@ochlocracy" - ] + "codeowners": ["@home-assistant/cloud", "@ochlocracy"] } diff --git a/homeassistant/components/alexa/resources.py b/homeassistant/components/alexa/resources.py new file mode 100644 index 00000000000..09927321c36 --- /dev/null +++ b/homeassistant/components/alexa/resources.py @@ -0,0 +1,387 @@ +"""Alexa Resources and Assets.""" + + +class AlexaGlobalCatalog: + """The Global Alexa catalog. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog + + You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. + This catalog is localized into all the languages that Alexa supports. + + You can reference the following catalog of pre-defined friendly names. + Each item in the following list is an asset identifier followed by its supported friendly names. + The first friendly name for each identifier is the one displayed in the Alexa mobile app. + """ + + # Air Purifier, Air Cleaner,Clean Air Machine + DEVICE_NAME_AIR_PURIFIER = "Alexa.DeviceName.AirPurifier" + + # Fan, Blower + DEVICE_NAME_FAN = "Alexa.DeviceName.Fan" + + # Router, Internet Router, Network Router, Wifi Router, Net Router + DEVICE_NAME_ROUTER = "Alexa.DeviceName.Router" + + # Shade, Blind, Curtain, Roller, Shutter, Drape, Awning, Window shade, Interior blind + DEVICE_NAME_SHADE = "Alexa.DeviceName.Shade" + + # Shower + DEVICE_NAME_SHOWER = "Alexa.DeviceName.Shower" + + # Space Heater, Portable Heater + DEVICE_NAME_SPACE_HEATER = "Alexa.DeviceName.SpaceHeater" + + # Washer, Washing Machine + DEVICE_NAME_WASHER = "Alexa.DeviceName.Washer" + + # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi + SETTING_2G_GUEST_WIFI = "Alexa.Setting.2GGuestWiFi" + + # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi + SETTING_5G_GUEST_WIFI = "Alexa.Setting.5GGuestWiFi" + + # Auto, Automatic, Automatic Mode, Auto Mode + SETTING_AUTO = "Alexa.Setting.Auto" + + # Direction + SETTING_DIRECTION = "Alexa.Setting.Direction" + + # Dry Cycle, Dry Preset, Dry Setting, Dryer Cycle, Dryer Preset, Dryer Setting + SETTING_DRY_CYCLE = "Alexa.Setting.DryCycle" + + # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity + SETTING_FAN_SPEED = "Alexa.Setting.FanSpeed" + + # Guest Wi-fi, Guest Network, Guest Net + SETTING_GUEST_WIFI = "Alexa.Setting.GuestWiFi" + + # Heat + SETTING_HEAT = "Alexa.Setting.Heat" + + # Mode + SETTING_MODE = "Alexa.Setting.Mode" + + # Night, Night Mode + SETTING_NIGHT = "Alexa.Setting.Night" + + # Opening, Height, Lift, Width + SETTING_OPENING = "Alexa.Setting.Opening" + + # Oscillate, Swivel, Oscillation, Spin, Back and forth + SETTING_OSCILLATE = "Alexa.Setting.Oscillate" + + # Preset, Setting + SETTING_PRESET = "Alexa.Setting.Preset" + + # Quiet, Quiet Mode, Noiseless, Silent + SETTING_QUIET = "Alexa.Setting.Quiet" + + # Temperature, Temp + SETTING_TEMPERATURE = "Alexa.Setting.Temperature" + + # Wash Cycle, Wash Preset, Wash setting + SETTING_WASH_CYCLE = "Alexa.Setting.WashCycle" + + # Water Temperature, Water Temp, Water Heat + SETTING_WATER_TEMPERATURE = "Alexa.Setting.WaterTemperature" + + # Handheld Shower, Shower Wand, Hand Shower + SHOWER_HAND_HELD = "Alexa.Shower.HandHeld" + + # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet + SHOWER_RAIN_HEAD = "Alexa.Shower.RainHead" + + # Degrees, Degree + UNIT_ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" + + # Radians, Radian + UNIT_ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" + + # Feet, Foot + UNIT_DISTANCE_FEET = "Alexa.Unit.Distance.Feet" + + # Inches, Inch + UNIT_DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" + + # Kilometers + UNIT_DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" + + # Meters, Meter, m + UNIT_DISTANCE_METERS = "Alexa.Unit.Distance.Meters" + + # Miles, Mile + UNIT_DISTANCE_MILES = "Alexa.Unit.Distance.Miles" + + # Yards, Yard + UNIT_DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" + + # Grams, Gram, g + UNIT_MASS_GRAMS = "Alexa.Unit.Mass.Grams" + + # Kilograms, Kilogram, kg + UNIT_MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" + + # Percent + UNIT_PERCENT = "Alexa.Unit.Percent" + + # Celsius, Degrees Celsius, Degrees, C, Centigrade, Degrees Centigrade + UNIT_TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" + + # Degrees, Degree + UNIT_TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" + + # Fahrenheit, Degrees Fahrenheit, Degrees F, Degrees, F + UNIT_TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" + + # Kelvin, Degrees Kelvin, Degrees K, Degrees, K + UNIT_TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" + + # Cubic Feet, Cubic Foot + UNIT_VOLUME_CUBIC_FEET = "Alexa.Unit.Volume.CubicFeet" + + # Cubic Meters, Cubic Meter, Meters Cubed + UNIT_VOLUME_CUBIC_METERS = "Alexa.Unit.Volume.CubicMeters" + + # Gallons, Gallon + UNIT_VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" + + # Liters, Liter, L + UNIT_VOLUME_LITERS = "Alexa.Unit.Volume.Liters" + + # Pints, Pint + UNIT_VOLUME_PINTS = "Alexa.Unit.Volume.Pints" + + # Quarts, Quart + UNIT_VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" + + # Ounces, Ounce, oz + UNIT_WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" + + # Pounds, Pound, lbs + UNIT_WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" + + # Close + VALUE_CLOSE = "Alexa.Value.Close" + + # Delicates, Delicate + VALUE_DELICATE = "Alexa.Value.Delicate" + + # High + VALUE_HIGH = "Alexa.Value.High" + + # Low + VALUE_LOW = "Alexa.Value.Low" + + # Maximum, Max + VALUE_MAXIMUM = "Alexa.Value.Maximum" + + # Medium, Mid + VALUE_MEDIUM = "Alexa.Value.Medium" + + # Minimum, Min + VALUE_MINIMUM = "Alexa.Value.Minimum" + + # Open + VALUE_OPEN = "Alexa.Value.Open" + + # Quick Wash, Fast Wash, Wash Quickly, Speed Wash + VALUE_QUICK_WASH = "Alexa.Value.QuickWash" + + +class AlexaCapabilityResource: + """Base class for Alexa capabilityResources, ModeResources, and presetResources objects. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources + """ + + def __init__(self, labels): + """Initialize an Alexa resource.""" + self._resource_labels = [] + for label in labels: + self._resource_labels.append(label) + + def serialize_capability_resources(self): + """Return capabilityResources object serialized for an API response.""" + return self.serialize_labels(self._resource_labels) + + @staticmethod + def serialize_configuration(): + """Return ModeResources, PresetResources friendlyNames serialized for an API response.""" + return [] + + @staticmethod + def serialize_labels(resources): + """Return resource label objects for friendlyNames serialized for an API response.""" + labels = [] + for label in resources: + if label in AlexaGlobalCatalog.__dict__.values(): + label = {"@type": "asset", "value": {"assetId": label}} + else: + label = {"@type": "text", "value": {"text": label, "locale": "en-US"}} + + labels.append(label) + + return {"friendlyNames": labels} + + +class AlexaModeResource(AlexaCapabilityResource): + """Implements Alexa ModeResources. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#capability-resources + """ + + def __init__(self, labels, ordered=False): + """Initialize an Alexa modeResource.""" + super().__init__(labels) + self._supported_modes = [] + self._mode_ordered = ordered + + def add_mode(self, value, labels): + """Add mode to the supportedModes object.""" + self._supported_modes.append({"value": value, "labels": labels}) + + def serialize_configuration(self): + """Return configuration for ModeResources friendlyNames serialized for an API response.""" + mode_resources = [] + for mode in self._supported_modes: + result = { + "value": mode["value"], + "modeResources": self.serialize_labels(mode["labels"]), + } + mode_resources.append(result) + + return {"ordered": self._mode_ordered, "supportedModes": mode_resources} + + +class AlexaPresetResource(AlexaCapabilityResource): + """Implements Alexa PresetResources. + + Use presetResources with RangeController to provide a set of friendlyNames for each RangeController preset. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources + """ + + def __init__(self, labels, min_value, max_value, precision, unit=None): + """Initialize an Alexa presetResource.""" + super().__init__(labels) + self._presets = [] + self._minimum_value = min_value + self._maximum_value = max_value + self._precision = precision + self._unit_of_measure = None + if unit in AlexaGlobalCatalog.__dict__.values(): + self._unit_of_measure = unit + + def add_preset(self, value, labels): + """Add preset to configuration presets array.""" + self._presets.append({"value": value, "labels": labels}) + + def serialize_configuration(self): + """Return configuration for PresetResources friendlyNames serialized for an API response.""" + configuration = { + "supportedRange": { + "minimumValue": self._minimum_value, + "maximumValue": self._maximum_value, + "precision": self._precision, + } + } + + if self._unit_of_measure: + configuration["unitOfMeasure"] = self._unit_of_measure + + if self._presets: + preset_resources = [] + for preset in self._presets: + preset_resources.append( + { + "rangeValue": preset["value"], + "presetResources": self.serialize_labels(preset["labels"]), + } + ) + configuration["presets"] = preset_resources + + return configuration + + +class AlexaSemantics: + """Class for Alexa Semantics Object. + + You can optionally enable additional utterances by using semantics. When you use semantics, + you manually map the phrases "open", "close", "raise", and "lower" to directives. + + Semantics is supported for the following interfaces only: ModeController, RangeController, and ToggleController. + + https://developer.amazon.com/docs/device-apis/alexa-discovery.html#semantics-object + """ + + MAPPINGS_ACTION = "actionMappings" + MAPPINGS_STATE = "stateMappings" + + ACTIONS_TO_DIRECTIVE = "ActionsToDirective" + STATES_TO_VALUE = "StatesToValue" + STATES_TO_RANGE = "StatesToRange" + + ACTION_CLOSE = "Alexa.Actions.Close" + ACTION_LOWER = "Alexa.Actions.Lower" + ACTION_OPEN = "Alexa.Actions.Open" + ACTION_RAISE = "Alexa.Actions.Raise" + + STATES_OPEN = "Alexa.States.Open" + STATES_CLOSED = "Alexa.States.Closed" + + DIRECTIVE_RANGE_SET_VALUE = "SetRangeValue" + DIRECTIVE_RANGE_ADJUST_VALUE = "AdjustRangeValue" + DIRECTIVE_TOGGLE_TURN_ON = "TurnOn" + DIRECTIVE_TOGGLE_TURN_OFF = "TurnOff" + DIRECTIVE_MODE_SET_MODE = "SetMode" + DIRECTIVE_MODE_ADJUST_MODE = "AdjustMode" + + def __init__(self): + """Initialize an Alexa modeResource.""" + self._action_mappings = [] + self._state_mappings = [] + + def _add_action_mapping(self, semantics): + """Add action mapping between actions and interface directives.""" + self._action_mappings.append(semantics) + + def _add_state_mapping(self, semantics): + """Add state mapping between states and interface directives.""" + self._state_mappings.append(semantics) + + def add_states_to_value(self, states, value): + """Add StatesToValue stateMappings.""" + self._add_state_mapping( + {"@type": self.STATES_TO_VALUE, "states": states, "value": value} + ) + + def add_states_to_range(self, states, min_value, max_value): + """Add StatesToRange stateMappings.""" + self._add_state_mapping( + { + "@type": self.STATES_TO_RANGE, + "states": states, + "range": {"minimumValue": min_value, "maximumValue": max_value}, + } + ) + + def add_action_to_directive(self, actions, directive, payload): + """Add ActionsToDirective actionMappings.""" + self._add_action_mapping( + { + "@type": self.ACTIONS_TO_DIRECTIVE, + "actions": actions, + "directive": {"name": directive, "payload": payload}, + } + ) + + def serialize_semantics(self): + """Return semantics object serialized for an API response.""" + semantics = {} + if self._action_mappings: + semantics[self.MAPPINGS_ACTION] = self._action_mappings + if self._state_mappings: + semantics[self.MAPPINGS_STATE] = self._state_mappings + + return semantics diff --git a/homeassistant/components/alexa/smart_home.py b/homeassistant/components/alexa/smart_home.py index 2c34542e25c..9b0955f8fca 100644 --- a/homeassistant/components/alexa/smart_home.py +++ b/homeassistant/components/alexa/smart_home.py @@ -4,7 +4,7 @@ import logging import homeassistant.core as ha from .const import API_DIRECTIVE, API_HEADER -from .errors import AlexaError, AlexaBridgeUnreachableError +from .errors import AlexaBridgeUnreachableError, AlexaError from .handlers import HANDLERS from .messages import AlexaDirective diff --git a/homeassistant/components/alexa/smart_home_http.py b/homeassistant/components/alexa/smart_home_http.py index ada00e8a326..7c745f8afdd 100644 --- a/homeassistant/components/alexa/smart_home_http.py +++ b/homeassistant/components/alexa/smart_home_http.py @@ -12,9 +12,10 @@ from .const import ( CONF_ENDPOINT, CONF_ENTITY_CONFIG, CONF_FILTER, + CONF_LOCALE, ) -from .state_report import async_enable_proactive_mode from .smart_home import async_handle_message +from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) SMART_HOME_HTTP_ENDPOINT = "/api/alexa/smart_home" @@ -53,6 +54,11 @@ class AlexaConfig(AbstractConfig): """Return entity config.""" return self._config.get(CONF_ENTITY_CONFIG) or {} + @property + def locale(self): + """Return config locale.""" + return self._config.get(CONF_LOCALE) + def should_expose(self, entity_id): """If an entity should be exposed.""" return self._config[CONF_FILTER](entity_id) diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index b5e1b741f0c..44e1b7f4f55 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,8 +6,8 @@ import logging import aiohttp import async_timeout -import homeassistant.util.dt as dt_util from homeassistant.const import MATCH_ALL, STATE_ON +import homeassistant.util.dt as dt_util from .const import API_CHANGE, Cause from .entities import ENTITY_ADAPTERS diff --git a/homeassistant/components/almond/.translations/da.json b/homeassistant/components/almond/.translations/da.json new file mode 100644 index 00000000000..93158cee94f --- /dev/null +++ b/homeassistant/components/almond/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_setup": "Du kan kun konfigurere en Almond-konto.", + "cannot_connect": "Kan ikke oprette forbindelse til Almond-serveren.", + "missing_configuration": "Tjek venligst dokumentationen om, hvordan man indstiller Almond." + }, + "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + }, + "title": "Almond" + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/ko.json b/homeassistant/components/almond/.translations/ko.json index 2a2346aaf50..9f1e71163d6 100644 --- a/homeassistant/components/almond/.translations/ko.json +++ b/homeassistant/components/almond/.translations/ko.json @@ -5,6 +5,11 @@ "cannot_connect": "Almond \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "missing_configuration": "Almond \uc124\uc815 \ubc29\ubc95\uc5d0 \ub300\ud55c \uc124\uba85\uc11c\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694." }, + "step": { + "pick_implementation": { + "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd" + } + }, "title": "Almond" } } \ No newline at end of file diff --git a/homeassistant/components/almond/.translations/pt-BR.json b/homeassistant/components/almond/.translations/pt-BR.json new file mode 100644 index 00000000000..94dfbefb86a --- /dev/null +++ b/homeassistant/components/almond/.translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 66d4b2fc9af..8877107b984 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -5,26 +5,26 @@ import logging import time from typing import Optional +from aiohttp import ClientError, ClientSession import async_timeout -from aiohttp import ClientSession, ClientError -from pyalmond import AlmondLocalAuth, AbstractAlmondWebAuth, WebAlmondAPI +from pyalmond import AbstractAlmondWebAuth, AlmondLocalAuth, WebAlmondAPI import voluptuous as vol -from homeassistant.core import HomeAssistant, CoreState, Context -from homeassistant.const import CONF_TYPE, CONF_HOST, EVENT_HOMEASSISTANT_START -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant import config_entries from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.components import conversation +from homeassistant.const import CONF_HOST, CONF_TYPE, EVENT_HOMEASSISTANT_START +from homeassistant.core import Context, CoreState, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import ( - config_validation as cv, + aiohttp_client, config_entry_oauth2_flow, + config_validation as cv, event, intent, - aiohttp_client, - storage, network, + storage, ) -from homeassistant import config_entries -from homeassistant.components import conversation from . import config_flow from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 diff --git a/homeassistant/components/almond/config_flow.py b/homeassistant/components/almond/config_flow.py index d79bf6bd605..42f9318a06f 100644 --- a/homeassistant/components/almond/config_flow.py +++ b/homeassistant/components/almond/config_flow.py @@ -2,14 +2,14 @@ import asyncio import logging -import async_timeout from aiohttp import ClientError -from yarl import URL -import voluptuous as vol +import async_timeout from pyalmond import AlmondLocalAuth, WebAlmondAPI +import voluptuous as vol +from yarl import URL -from homeassistant import data_entry_flow, config_entries, core -from homeassistant.helpers import config_entry_oauth2_flow, aiohttp_client +from homeassistant import config_entries, core, data_entry_flow +from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow from .const import DOMAIN, TYPE_LOCAL, TYPE_OAUTH2 diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 99498991be2..33348c9d7b3 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -2,11 +2,7 @@ "domain": "alpha_vantage", "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", - "requirements": [ - "alpha_vantage==2.1.2" - ], + "requirements": ["alpha_vantage==2.1.2"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index da29e4e25e1..7d871c286e5 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -2,9 +2,9 @@ from datetime import timedelta import logging -import voluptuous as vol -from alpha_vantage.timeseries import TimeSeries from alpha_vantage.foreignexchange import ForeignExchange +from alpha_vantage.timeseries import TimeSeries +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index c07aad079e4..4bfcff4ce76 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -1,12 +1,8 @@ { "domain": "amazon_polly", - "name": "Amazon polly", + "name": "Amazon Polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly", - "requirements": [ - "boto3==1.9.252" - ], + "requirements": ["boto3==1.9.252"], "dependencies": [], - "codeowners": [ - "@robbiet480" - ] -} \ No newline at end of file + "codeowners": ["@robbiet480"] +} diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 3d05236935f..bcb4a24e95b 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -1,7 +1,7 @@ """Support for the Amazon Polly text to speech service.""" import logging -import boto3 +import boto3 import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/ambiclimate/__init__.py b/homeassistant/components/ambiclimate/__init__.py index 962c8c8a82d..e15f6dea2ec 100644 --- a/homeassistant/components/ambiclimate/__init__.py +++ b/homeassistant/components/ambiclimate/__init__.py @@ -4,10 +4,10 @@ import logging import voluptuous as vol from homeassistant.helpers import config_validation as cv + from . import config_flow from .const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN - _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( diff --git a/homeassistant/components/ambiclimate/climate.py b/homeassistant/components/ambiclimate/climate.py index bb3e5ab2b25..a8ed166903e 100644 --- a/homeassistant/components/ambiclimate/climate.py +++ b/homeassistant/components/ambiclimate/climate.py @@ -7,13 +7,14 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, - HVAC_MODE_OFF, HVAC_MODE_HEAT, + HVAC_MODE_OFF, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_NAME, ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession + from .const import ( ATTR_VALUE, CONF_CLIENT_ID, diff --git a/homeassistant/components/ambiclimate/config_flow.py b/homeassistant/components/ambiclimate/config_flow.py index 99563dcb97d..4996a458a1f 100644 --- a/homeassistant/components/ambiclimate/config_flow.py +++ b/homeassistant/components/ambiclimate/config_flow.py @@ -7,14 +7,15 @@ from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession + from .const import ( AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, CONF_CLIENT_ID, CONF_CLIENT_SECRET, DOMAIN, - STORAGE_VERSION, STORAGE_KEY, + STORAGE_VERSION, ) DATA_AMBICLIMATE_IMPL = "ambiclimate_flow_implementation" diff --git a/homeassistant/components/ambiclimate/manifest.json b/homeassistant/components/ambiclimate/manifest.json index 3d175165abd..151b761dff8 100644 --- a/homeassistant/components/ambiclimate/manifest.json +++ b/homeassistant/components/ambiclimate/manifest.json @@ -3,11 +3,7 @@ "name": "Ambiclimate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambiclimate", - "requirements": [ - "ambiclimate==0.2.1" - ], - "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "requirements": ["ambiclimate==0.2.1"], + "dependencies": ["http"], + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/ambient_station/.translations/da.json b/homeassistant/components/ambient_station/.translations/da.json index ac3d86a995b..6cec31eca29 100644 --- a/homeassistant/components/ambient_station/.translations/da.json +++ b/homeassistant/components/ambient_station/.translations/da.json @@ -8,7 +8,7 @@ "step": { "user": { "data": { - "api_key": "API n\u00f8gle", + "api_key": "API-n\u00f8gle", "app_key": "Applikationsn\u00f8gle" }, "title": "Udfyld dine oplysninger" diff --git a/homeassistant/components/ambient_station/.translations/ko.json b/homeassistant/components/ambient_station/.translations/ko.json index 541b8699dc8..eb9209a6c37 100644 --- a/homeassistant/components/ambient_station/.translations/ko.json +++ b/homeassistant/components/ambient_station/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "error": { - "identifier_exists": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "invalid_key": "Application \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "identifier_exists": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, "step": { "user": { "data": { "api_key": "API \ud0a4", - "app_key": "Application \ud0a4" + "app_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4" }, "title": "\uc0ac\uc6a9\uc790 \uc815\ubcf4 \uc785\ub825" } diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 7a805d6b867..58389dd1831 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -8,8 +8,8 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - ATTR_NAME, ATTR_LOCATION, + ATTR_NAME, CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, ) diff --git a/homeassistant/components/ambient_station/config_flow.py b/homeassistant/components/ambient_station/config_flow.py index 256e55ba402..c20b43598ca 100644 --- a/homeassistant/components/ambient_station/config_flow.py +++ b/homeassistant/components/ambient_station/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the Ambient PWS component.""" +from aioambient import Client +from aioambient.errors import AmbientError import voluptuous as vol from homeassistant import config_entries @@ -40,8 +42,6 @@ class AmbientStationFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from aioambient import Client - from aioambient.errors import AmbientError if not user_input: return await self._show_form() diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 1e6c06f260a..25f60f63abf 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -1,13 +1,9 @@ { "domain": "ambient_station", - "name": "Ambient station", + "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": [ - "aioambient==1.0.2" - ], + "requirements": ["aioambient==1.0.2"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index d49104a0b26..63daeb04731 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -1,6 +1,6 @@ """Support for Amcrest IP cameras.""" -import logging from datetime import timedelta +import logging import threading import aiohttp @@ -11,7 +11,6 @@ from homeassistant.auth.permissions.const import POLICY_CONTROL from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.camera import DOMAIN as CAMERA from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( ATTR_ENTITY_ID, CONF_AUTHENTICATION, @@ -22,7 +21,6 @@ from homeassistant.const import ( CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, - CONF_SWITCHES, CONF_USERNAME, ENTITY_MATCH_ALL, HTTP_BASIC_AUTHENTICATION, @@ -34,12 +32,11 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_s from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.service import async_extract_entity_ids -from .binary_sensor import BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSORS +from .binary_sensor import BINARY_SENSORS from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST -from .const import CAMERAS, DOMAIN, DATA_AMCREST, DEVICES, SERVICE_UPDATE +from .const import CAMERAS, DATA_AMCREST, DEVICES, DOMAIN, SERVICE_UPDATE from .helpers import service_signal -from .sensor import SENSOR_MOTION_DETECTOR, SENSORS -from .switch import SWITCHES +from .sensor import SENSORS _LOGGER = logging.getLogger(__name__) @@ -65,68 +62,36 @@ SCAN_INTERVAL = timedelta(seconds=10) AUTHENTICATION_LIST = {"basic": "basic"} -def _deprecated_sensor_values(sensors): - if SENSOR_MOTION_DETECTOR in sensors: - _LOGGER.warning( - "The '%s' option value '%s' is deprecated, " - "please remove it from your configuration and use " - "the '%s' option with value '%s' instead", - CONF_SENSORS, - SENSOR_MOTION_DETECTOR, - CONF_BINARY_SENSORS, - BINARY_SENSOR_MOTION_DETECTED, - ) - return sensors - - -def _deprecated_switches(config): - if CONF_SWITCHES in config: - _LOGGER.warning( - "The '%s' option (with value %s) is deprecated, " - "please remove it from your configuration and use " - "services and attributes instead", - CONF_SWITCHES, - config[CONF_SWITCHES], - ) - return config - - def _has_unique_names(devices): names = [device[CONF_NAME] for device in devices] vol.Schema(vol.Unique())(names) return devices -AMCREST_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional( - CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION - ): vol.All(vol.In(AUTHENTICATION_LIST)), - vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All( - vol.In(RESOLUTION_LIST) - ), - vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All( - vol.In(STREAM_SOURCE_LIST) - ), - vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)] - ), - vol.Optional(CONF_SENSORS): vol.All( - cv.ensure_list, [vol.In(SENSORS)], _deprecated_sensor_values - ), - vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [vol.In(SWITCHES)]), - vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, - } - ), - _deprecated_switches, +AMCREST_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.All( + vol.In(AUTHENTICATION_LIST) + ), + vol.Optional(CONF_RESOLUTION, default=DEFAULT_RESOLUTION): vol.All( + vol.In(RESOLUTION_LIST) + ), + vol.Optional(CONF_STREAM_SOURCE, default=STREAM_SOURCE_LIST[0]): vol.All( + vol.In(STREAM_SOURCE_LIST) + ), + vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + vol.Optional(CONF_BINARY_SENSORS): vol.All( + cv.ensure_list, [vol.In(BINARY_SENSORS)] + ), + vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), + vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, + } ) CONFIG_SCHEMA = vol.Schema( @@ -167,8 +132,6 @@ class AmcrestChecker(Http): offline = not self.available if offline and was_online: _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) - with self._token_lock: - self._token = None dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) @@ -216,7 +179,6 @@ def setup(hass, config): resolution = RESOLUTION_LIST[device[CONF_RESOLUTION]] binary_sensors = device.get(CONF_BINARY_SENSORS) sensors = device.get(CONF_SENSORS) - switches = device.get(CONF_SWITCHES) stream_source = device[CONF_STREAM_SOURCE] control_light = device.get(CONF_CONTROL_LIGHT) @@ -252,11 +214,6 @@ def setup(hass, config): hass, SENSOR, DOMAIN, {CONF_NAME: name, CONF_SENSORS: sensors}, config ) - if switches: - discovery.load_platform( - hass, SWITCH, DOMAIN, {CONF_NAME: name, CONF_SWITCHES: switches}, config - ) - if not hass.data[DATA_AMCREST][DEVICES]: return False diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index f8b50d1114e..ac16f0664aa 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -5,11 +5,11 @@ import logging from amcrest import AmcrestError from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION, + BinarySensorDevice, ) -from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS +from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 4453687b895..ee5b97b8579 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -2,11 +2,7 @@ "domain": "amcrest", "name": "Amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest", - "requirements": [ - "amcrest==1.5.3" - ], - "dependencies": [ - "ffmpeg" - ], + "requirements": ["amcrest==1.5.3"], + "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/homeassistant/components/amcrest/sensor.py b/homeassistant/components/amcrest/sensor.py index b53f05273fa..be03b3bedff 100644 --- a/homeassistant/components/amcrest/sensor.py +++ b/homeassistant/components/amcrest/sensor.py @@ -15,12 +15,10 @@ _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=SENSOR_SCAN_INTERVAL_SECS) -SENSOR_MOTION_DETECTOR = "motion_detector" SENSOR_PTZ_PRESET = "ptz_preset" SENSOR_SDCARD = "sdcard" # Sensor types are defined like: Name, units, icon SENSORS = { - SENSOR_MOTION_DETECTOR: ["Motion Detected", None, "mdi:run"], SENSOR_PTZ_PRESET: ["PTZ Preset", None, "mdi:camera-iris"], SENSOR_SDCARD: ["SD Used", "%", "mdi:sd"], } @@ -94,11 +92,7 @@ class AmcrestSensor(Entity): _LOGGER.debug("Updating %s sensor", self._name) try: - if self._sensor_type == SENSOR_MOTION_DETECTOR: - self._state = self._api.is_motion_detected - self._attrs["Record Mode"] = self._api.record_mode - - elif self._sensor_type == SENSOR_PTZ_PRESET: + if self._sensor_type == SENSOR_PTZ_PRESET: self._state = self._api.ptz_presets_count elif self._sensor_type == SENSOR_SDCARD: diff --git a/homeassistant/components/amcrest/switch.py b/homeassistant/components/amcrest/switch.py deleted file mode 100644 index 0c3390c16f9..00000000000 --- a/homeassistant/components/amcrest/switch.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Support for toggling Amcrest IP camera settings.""" -import logging - -from amcrest import AmcrestError - -from homeassistant.const import CONF_NAME, CONF_SWITCHES -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import ToggleEntity - -from .const import DATA_AMCREST, DEVICES, SERVICE_UPDATE -from .helpers import log_update_error, service_signal - -_LOGGER = logging.getLogger(__name__) - -MOTION_DETECTION = "motion_detection" -MOTION_RECORDING = "motion_recording" -# Switch types are defined like: Name, icon -SWITCHES = { - MOTION_DETECTION: ["Motion Detection", "mdi:run-fast"], - MOTION_RECORDING: ["Motion Recording", "mdi:record-rec"], -} - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the IP Amcrest camera switch platform.""" - if discovery_info is None: - return - - name = discovery_info[CONF_NAME] - device = hass.data[DATA_AMCREST][DEVICES][name] - async_add_entities( - [ - AmcrestSwitch(name, device, setting) - for setting in discovery_info[CONF_SWITCHES] - ], - True, - ) - - -class AmcrestSwitch(ToggleEntity): - """Representation of an Amcrest IP camera switch.""" - - def __init__(self, name, device, setting): - """Initialize the Amcrest switch.""" - self._name = "{} {}".format(name, SWITCHES[setting][0]) - self._signal_name = name - self._api = device.api - self._setting = setting - self._state = False - self._icon = SWITCHES[setting][1] - self._unsub_dispatcher = None - - @property - def name(self): - """Return the name of the switch if any.""" - return self._name - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - def turn_on(self, **kwargs): - """Turn setting on.""" - if not self.available: - return - try: - if self._setting == MOTION_DETECTION: - self._api.motion_detection = "true" - elif self._setting == MOTION_RECORDING: - self._api.motion_recording = "true" - except AmcrestError as error: - log_update_error(_LOGGER, "turn on", self.name, "switch", error) - - def turn_off(self, **kwargs): - """Turn setting off.""" - if not self.available: - return - try: - if self._setting == MOTION_DETECTION: - self._api.motion_detection = "false" - elif self._setting == MOTION_RECORDING: - self._api.motion_recording = "false" - except AmcrestError as error: - log_update_error(_LOGGER, "turn off", self.name, "switch", error) - - @property - def available(self): - """Return True if entity is available.""" - return self._api.available - - def update(self): - """Update setting state.""" - if not self.available: - return - _LOGGER.debug("Updating %s switch", self._name) - - try: - if self._setting == MOTION_DETECTION: - detection = self._api.is_motion_detector_on() - elif self._setting == MOTION_RECORDING: - detection = self._api.is_record_on_motion_detection() - self._state = detection - except AmcrestError as error: - log_update_error(_LOGGER, "update", self.name, "switch", error) - - @property - def icon(self): - """Return the icon for the switch.""" - return self._icon - - async def async_on_demand_update(self): - """Update state.""" - self.async_schedule_update_ha_state(True) - - async def async_added_to_hass(self): - """Subscribe to update signal.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, - service_signal(SERVICE_UPDATE, self._signal_name), - self.async_on_demand_update, - ) - - async def async_will_remove_from_hass(self): - """Disconnect from update signal.""" - self._unsub_dispatcher() diff --git a/homeassistant/components/ampio/manifest.json b/homeassistant/components/ampio/manifest.json index 6bf79e27f85..99c84da6334 100644 --- a/homeassistant/components/ampio/manifest.json +++ b/homeassistant/components/ampio/manifest.json @@ -1,10 +1,8 @@ { "domain": "ampio", - "name": "Ampio", + "name": "Ampio Smart Smog System", "documentation": "https://www.home-assistant.io/integrations/ampio", - "requirements": [ - "asmog==0.0.6" - ], + "requirements": ["asmog==0.0.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/android_ip_webcam/manifest.json b/homeassistant/components/android_ip_webcam/manifest.json index c9602d75751..f5181a7d33f 100644 --- a/homeassistant/components/android_ip_webcam/manifest.json +++ b/homeassistant/components/android_ip_webcam/manifest.json @@ -1,10 +1,8 @@ { "domain": "android_ip_webcam", - "name": "Android ip webcam", + "name": "Android IP Webcam", "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", - "requirements": [ - "pydroid-ipcam==0.8" - ], + "requirements": ["pydroid-ipcam==0.8"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 8b68f089617..d81e7863503 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -1,10 +1,10 @@ { "domain": "androidtv", - "name": "Androidtv", + "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.8", - "androidtv==0.0.34", + "adb-shell==0.1.1", + "androidtv==0.0.38", "pure-python-adb==0.2.2.dev0" ], "dependencies": [], diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index b1cb86f7633..63b27f17bb2 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -2,7 +2,6 @@ import functools import logging import os -import voluptuous as vol from adb_shell.auth.keygen import keygen from adb_shell.exceptions import ( @@ -11,10 +10,12 @@ from adb_shell.exceptions import ( InvalidResponseError, TcpTimeoutException, ) -from androidtv import setup, ha_state_detection_rules_validator +from androidtv import ha_state_detection_rules_validator, setup from androidtv.constants import APPS, KEYS +from androidtv.exceptions import LockNotAcquiredException +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -55,6 +56,7 @@ SUPPORT_ANDROIDTV = ( | SUPPORT_TURN_OFF | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK + | SUPPORT_SELECT_SOURCE | SUPPORT_STOP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_STEP @@ -71,6 +73,9 @@ SUPPORT_FIRETV = ( | SUPPORT_STOP ) +ATTR_DEVICE_PATH = "device_path" +ATTR_LOCAL_PATH = "local_path" + CONF_ADBKEY = "adbkey" CONF_ADB_SERVER_IP = "adb_server_ip" CONF_ADB_SERVER_PORT = "adb_server_port" @@ -91,11 +96,29 @@ DEVICE_FIRETV = "firetv" DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] SERVICE_ADB_COMMAND = "adb_command" +SERVICE_DOWNLOAD = "download" +SERVICE_UPLOAD = "upload" SERVICE_ADB_COMMAND_SCHEMA = vol.Schema( {vol.Required(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_COMMAND): cv.string} ) +SERVICE_DOWNLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_id, + vol.Required(ATTR_DEVICE_PATH): cv.string, + vol.Required(ATTR_LOCAL_PATH): cv.string, + } +) + +SERVICE_UPLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_DEVICE_PATH): cv.string, + vol.Required(ATTR_LOCAL_PATH): cv.string, + } +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -132,7 +155,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Android TV / Fire TV platform.""" hass.data.setdefault(ANDROIDTV_DOMAIN, {}) - host = f"{config[CONF_HOST]}:{config[CONF_PORT]}" + address = f"{config[CONF_HOST]}:{config[CONF_PORT]}" + + if address in hass.data[ANDROIDTV_DOMAIN]: + _LOGGER.warning("Platform already setup on %s, skipping", address) + return if CONF_ADB_SERVER_IP not in config: # Use "adb_shell" (Python ADB implementation) @@ -145,7 +172,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], adbkey, device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], @@ -158,7 +186,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], @@ -170,7 +199,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" aftv = setup( - host, + config[CONF_HOST], + config[CONF_PORT], adb_server_ip=config[CONF_ADB_SERVER_IP], adb_server_port=config[CONF_ADB_SERVER_PORT], device_class=config[CONF_DEVICE_CLASS], @@ -188,43 +218,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: device_name = "Android TV / Fire TV device" - _LOGGER.warning("Could not connect to %s at %s %s", device_name, host, adb_log) + _LOGGER.warning( + "Could not connect to %s at %s %s", device_name, address, adb_log + ) raise PlatformNotReady - if host in hass.data[ANDROIDTV_DOMAIN]: - _LOGGER.warning("Platform already setup on %s, skipping", host) - else: - if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV: - device = AndroidTVDevice( - aftv, - config[CONF_NAME], - config[CONF_APPS], - config.get(CONF_TURN_ON_COMMAND), - config.get(CONF_TURN_OFF_COMMAND), - ) - device_name = config[CONF_NAME] if CONF_NAME in config else "Android TV" - else: - device = FireTVDevice( - aftv, - config[CONF_NAME], - config[CONF_APPS], - config[CONF_GET_SOURCES], - config.get(CONF_TURN_ON_COMMAND), - config.get(CONF_TURN_OFF_COMMAND), - ) - device_name = config[CONF_NAME] if CONF_NAME in config else "Fire TV" + device_args = [ + aftv, + config[CONF_NAME], + config[CONF_APPS], + config[CONF_GET_SOURCES], + config.get(CONF_TURN_ON_COMMAND), + config.get(CONF_TURN_OFF_COMMAND), + ] - add_entities([device]) - _LOGGER.debug("Setup %s at %s %s", device_name, host, adb_log) - hass.data[ANDROIDTV_DOMAIN][host] = device + if aftv.DEVICE_CLASS == DEVICE_ANDROIDTV: + device = AndroidTVDevice(*device_args) + device_name = config.get(CONF_NAME, "Android TV") + else: + device = FireTVDevice(*device_args) + device_name = config.get(CONF_NAME, "Fire TV") + + add_entities([device]) + _LOGGER.debug("Setup %s at %s %s", device_name, address, adb_log) + hass.data[ANDROIDTV_DOMAIN][address] = device if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): return def service_adb_command(service): """Dispatch service calls to target entities.""" - cmd = service.data.get(ATTR_COMMAND) - entity_id = service.data.get(ATTR_ENTITY_ID) + cmd = service.data[ATTR_COMMAND] + entity_id = service.data[ATTR_ENTITY_ID] target_devices = [ dev for dev in hass.data[ANDROIDTV_DOMAIN].values() @@ -250,6 +275,52 @@ def setup_platform(hass, config, add_entities, discovery_info=None): schema=SERVICE_ADB_COMMAND_SCHEMA, ) + def service_download(service): + """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" + local_path = service.data[ATTR_LOCAL_PATH] + if not hass.config.is_allowed_path(local_path): + _LOGGER.warning("'%s' is not secure to load data from!", local_path) + return + + device_path = service.data[ATTR_DEVICE_PATH] + entity_id = service.data[ATTR_ENTITY_ID] + target_device = [ + dev + for dev in hass.data[ANDROIDTV_DOMAIN].values() + if dev.entity_id in entity_id + ][0] + + target_device.adb_pull(local_path, device_path) + + hass.services.register( + ANDROIDTV_DOMAIN, + SERVICE_DOWNLOAD, + service_download, + schema=SERVICE_DOWNLOAD_SCHEMA, + ) + + def service_upload(service): + """Upload a file from your Home Assistant instance to an Android TV / Fire TV device.""" + local_path = service.data[ATTR_LOCAL_PATH] + if not hass.config.is_allowed_path(local_path): + _LOGGER.warning("'%s' is not secure to load data from!", local_path) + return + + device_path = service.data[ATTR_DEVICE_PATH] + entity_id = service.data[ATTR_ENTITY_ID] + target_devices = [ + dev + for dev in hass.data[ANDROIDTV_DOMAIN].values() + if dev.entity_id in entity_id + ] + + for target_device in target_devices: + target_device.adb_push(local_path, device_path) + + hass.services.register( + ANDROIDTV_DOMAIN, SERVICE_UPLOAD, service_upload, schema=SERVICE_UPLOAD_SCHEMA, + ) + def adb_decorator(override_available=False): """Wrap ADB methods and catch exceptions. @@ -269,6 +340,12 @@ def adb_decorator(override_available=False): try: return func(self, *args, **kwargs) + except LockNotAcquiredException: + # If the ADB lock could not be acquired, skip this command + _LOGGER.info( + "ADB command not executed because the connection is currently in use" + ) + return except self.exceptions as err: _LOGGER.error( "Failed to execute an ADB command. ADB connection re-" @@ -287,7 +364,9 @@ def adb_decorator(override_available=False): class ADBDevice(MediaPlayerDevice): """Representation of an Android TV or Fire TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Android TV / Fire TV device.""" self.aftv = aftv self._name = name @@ -296,6 +375,7 @@ class ADBDevice(MediaPlayerDevice): self._app_name_to_id = { value: key for key, value in self._app_id_to_name.items() } + self._get_sources = get_sources self._keys = KEYS self._device_properties = self.aftv.device_properties @@ -325,6 +405,7 @@ class ADBDevice(MediaPlayerDevice): self._adb_response = None self._available = True self._current_app = None + self._sources = None self._state = None @property @@ -357,6 +438,16 @@ class ADBDevice(MediaPlayerDevice): """Device should be polled.""" return True + @property + def source(self): + """Return the current app.""" + return self._app_id_to_name.get(self._current_app, self._current_app) + + @property + def source_list(self): + """Return a list of running apps.""" + return self._sources + @property def state(self): """Return the state of the player.""" @@ -408,6 +499,20 @@ class ADBDevice(MediaPlayerDevice): """Send next track command (results in fast-forward).""" self.aftv.media_next_track() + @adb_decorator() + def select_source(self, source): + """Select input source. + + If the source starts with a '!', then it will close the app instead of + opening it. + """ + if isinstance(source, str): + if not source.startswith("!"): + self.aftv.launch_app(self._app_name_to_id.get(source, source)) + else: + source_ = source[1:].lstrip() + self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) + @adb_decorator() def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" @@ -423,7 +528,13 @@ class ADBDevice(MediaPlayerDevice): self.schedule_update_ha_state() return self._adb_response - response = self.aftv.adb_shell(cmd) + try: + response = self.aftv.adb_shell(cmd) + except UnicodeDecodeError: + self._adb_response = None + self.schedule_update_ha_state() + return + if isinstance(response, str) and response.strip(): self._adb_response = response.strip() else: @@ -432,15 +543,28 @@ class ADBDevice(MediaPlayerDevice): self.schedule_update_ha_state() return self._adb_response + @adb_decorator() + def adb_pull(self, local_path, device_path): + """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" + self.aftv.adb_pull(local_path, device_path) + + @adb_decorator() + def adb_push(self, local_path, device_path): + """Upload a file from your Home Assistant instance to an Android TV / Fire TV device.""" + self.aftv.adb_push(local_path, device_path) + class AndroidTVDevice(ADBDevice): """Representation of an Android TV device.""" - def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): + def __init__( + self, aftv, name, apps, get_sources, turn_on_command, turn_off_command + ): """Initialize the Android TV device.""" - super().__init__(aftv, name, apps, turn_on_command, turn_off_command) + super().__init__( + aftv, name, apps, get_sources, turn_on_command, turn_off_command + ) - self._device = None self._is_volume_muted = None self._volume_level = None @@ -465,25 +589,28 @@ class AndroidTVDevice(ADBDevice): ( state, self._current_app, - self._device, + running_apps, + _, self._is_volume_muted, self._volume_level, - ) = self.aftv.update() + ) = self.aftv.update(self._get_sources) self._state = ANDROIDTV_STATES.get(state) if self._state is None: self._available = False + if running_apps: + self._sources = [ + self._app_id_to_name.get(app_id, app_id) for app_id in running_apps + ] + else: + self._sources = None + @property def is_volume_muted(self): """Boolean if volume is currently muted.""" return self._is_volume_muted - @property - def source(self): - """Return the current playback device.""" - return self._device - @property def supported_features(self): """Flag media player features that are supported.""" @@ -518,15 +645,6 @@ class AndroidTVDevice(ADBDevice): class FireTVDevice(ADBDevice): """Representation of a Fire TV device.""" - def __init__( - self, aftv, name, apps, get_sources, turn_on_command, turn_off_command - ): - """Initialize the Fire TV device.""" - super().__init__(aftv, name, apps, turn_on_command, turn_off_command) - - self._get_sources = get_sources - self._sources = None - @adb_decorator(override_available=True) def update(self): """Update the device state and, if necessary, re-connect.""" @@ -558,16 +676,6 @@ class FireTVDevice(ADBDevice): else: self._sources = None - @property - def source(self): - """Return the current app.""" - return self._app_id_to_name.get(self._current_app, self._current_app) - - @property - def source_list(self): - """Return a list of running apps.""" - return self._sources - @property def supported_features(self): """Flag media player features that are supported.""" @@ -577,17 +685,3 @@ class FireTVDevice(ADBDevice): def media_stop(self): """Send stop (back) command.""" self.aftv.back() - - @adb_decorator() - def select_source(self, source): - """Select input source. - - If the source starts with a '!', then it will close the app instead of - opening it. - """ - if isinstance(source, str): - if not source.startswith("!"): - self.aftv.launch_app(self._app_name_to_id.get(source, source)) - else: - source_ = source[1:].lstrip() - self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) diff --git a/homeassistant/components/androidtv/services.yaml b/homeassistant/components/androidtv/services.yaml index 78ff0a828f6..96d70ef4998 100644 --- a/homeassistant/components/androidtv/services.yaml +++ b/homeassistant/components/androidtv/services.yaml @@ -9,3 +9,27 @@ adb_command: command: description: Either a key command or an ADB shell command. example: 'HOME' +download: + description: Download a file from your Android TV / Fire TV device to your Home Assistant instance. + fields: + entity_id: + description: Name of Android TV / Fire TV entity. + example: 'media_player.android_tv_living_room' + device_path: + description: The filepath on the Android TV / Fire TV device. + example: '/storage/emulated/0/Download/example.txt' + local_path: + description: The filepath on your Home Assistant instance. + example: '/config/example.txt' +upload: + description: Upload a file from your Home Assistant instance to an Android TV / Fire TV device. + fields: + entity_id: + description: Name(s) of Android TV / Fire TV entities. + example: 'media_player.android_tv_living_room' + device_path: + description: The filepath on the Android TV / Fire TV device. + example: '/storage/emulated/0/Download/example.txt' + local_path: + description: The filepath on your Home Assistant instance. + example: '/config/example.txt' diff --git a/homeassistant/components/anel_pwrctrl/manifest.json b/homeassistant/components/anel_pwrctrl/manifest.json index d4055a4068f..d076d71b24a 100644 --- a/homeassistant/components/anel_pwrctrl/manifest.json +++ b/homeassistant/components/anel_pwrctrl/manifest.json @@ -1,10 +1,8 @@ { "domain": "anel_pwrctrl", - "name": "Anel pwrctrl", + "name": "Anel NET-PwrCtrl", "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", - "requirements": [ - "anel_pwrctrl-homeassistant==0.0.1.dev2" - ], + "requirements": ["anel_pwrctrl-homeassistant==0.0.1.dev2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/anthemav/manifest.json b/homeassistant/components/anthemav/manifest.json index 7c648d37b10..df0de2079de 100644 --- a/homeassistant/components/anthemav/manifest.json +++ b/homeassistant/components/anthemav/manifest.json @@ -1,10 +1,8 @@ { "domain": "anthemav", - "name": "Anthemav", + "name": "Anthem A/V Receivers", "documentation": "https://www.home-assistant.io/integrations/anthemav", - "requirements": [ - "anthemav==1.1.10" - ], + "requirements": ["anthemav==1.1.10"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index d472af6104e..f7b385d80a2 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -2,10 +2,9 @@ import logging import anthemav - import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index fb2bd643525..0061aecade9 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -2,11 +2,7 @@ "domain": "apache_kafka", "name": "Apache Kafka", "documentation": "https://www.home-assistant.io/integrations/apache_kafka", - "requirements": [ - "aiokafka==0.5.1" - ], + "requirements": ["aiokafka==0.5.1"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 71f25f04387..01f74165190 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -18,11 +18,11 @@ DEFAULT_HOST = "localhost" DEFAULT_PORT = 3551 DOMAIN = "apcupsd" -KEY_STATUS = "STATUS" +KEY_STATUS = "STATFLAG" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) -VALUE_ONLINE = "ONLINE" +VALUE_ONLINE = 8 CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/apcupsd/binary_sensor.py b/homeassistant/components/apcupsd/binary_sensor.py index 62f0c90a447..de4e1f17200 100644 --- a/homeassistant/components/apcupsd/binary_sensor.py +++ b/homeassistant/components/apcupsd/binary_sensor.py @@ -1,10 +1,10 @@ """Support for tracking the online status of a UPS.""" import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components import apcupsd +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components import apcupsd DEFAULT_NAME = "UPS Online Status" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -34,8 +34,8 @@ class OnlineStatus(BinarySensorDevice): @property def is_on(self): """Return true if the UPS is online, else false.""" - return self._state == apcupsd.VALUE_ONLINE + return self._state & apcupsd.VALUE_ONLINE > 0 def update(self): """Get the status report from APCUPSd and set this entity's state.""" - self._state = self._data.status[apcupsd.KEY_STATUS] + self._state = int(self._data.status[apcupsd.KEY_STATUS], 16) diff --git a/homeassistant/components/apcupsd/manifest.json b/homeassistant/components/apcupsd/manifest.json index 08cac545249..a8a5506fc0a 100644 --- a/homeassistant/components/apcupsd/manifest.json +++ b/homeassistant/components/apcupsd/manifest.json @@ -1,10 +1,8 @@ { "domain": "apcupsd", - "name": "Apcupsd", + "name": "APCUPSd", "documentation": "https://www.home-assistant.io/integrations/apcupsd", - "requirements": [ - "apcaccess==0.0.13" - ], + "requirements": ["apcaccess==0.0.13"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/api/__init__.py b/homeassistant/components/api/__init__.py index d4faa55ed8c..fc2f01d418d 100644 --- a/homeassistant/components/api/__init__.py +++ b/homeassistant/components/api/__init__.py @@ -8,6 +8,7 @@ from aiohttp.web_exceptions import HTTPBadRequest import async_timeout import voluptuous as vol +from homeassistant.auth.permissions.const import POLICY_READ from homeassistant.bootstrap import DATA_LOGGING from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( @@ -31,12 +32,11 @@ from homeassistant.const import ( __version__, ) import homeassistant.core as ha -from homeassistant.auth.permissions.const import POLICY_READ -from homeassistant.exceptions import TemplateError, Unauthorized, ServiceNotFound +from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized from homeassistant.helpers import template +from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.state import AsyncTrackStates -from homeassistant.helpers.json import JSONEncoder _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/api/manifest.json b/homeassistant/components/api/manifest.json index 830fc04445a..f5795a55f04 100644 --- a/homeassistant/components/api/manifest.json +++ b/homeassistant/components/api/manifest.json @@ -3,10 +3,7 @@ "name": "Home Assistant API", "documentation": "https://www.home-assistant.io/integrations/api", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/apns/manifest.json b/homeassistant/components/apns/manifest.json index 4845c45a963..b498e4476ec 100644 --- a/homeassistant/components/apns/manifest.json +++ b/homeassistant/components/apns/manifest.json @@ -1,10 +1,9 @@ { "domain": "apns", - "name": "Apns", + "name": "Apple Push Notification Service (APNS)", "documentation": "https://www.home-assistant.io/integrations/apns", - "requirements": [ - "apns2==0.3.0" - ], + "requirements": ["apns2==0.3.0"], "dependencies": [], + "after_dependencies": ["device_tracker"], "codeowners": [] } diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index ce761b502ac..febe344a9c4 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -6,6 +6,7 @@ from apns2.errors import Unregistered from apns2.payload import Payload import voluptuous as vol +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -17,7 +18,6 @@ from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import template as template_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_state_change -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from .const import DOMAIN @@ -150,7 +150,7 @@ class ApnsNotificationService(BaseNotificationService): self.app_name = app_name self.sandbox = sandbox self.certificate = cert_file - self.yaml_path = hass.config.path(app_name + "_" + APNS_DEVICES) + self.yaml_path = hass.config.path(f"{app_name}_{APNS_DEVICES}") self.devices = {} self.device_states = {} self.topic = topic diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 38d520f73da..e11b246fd5e 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -7,11 +7,11 @@ from pyatv import AppleTVDevice, connect_to_apple_tv, scan_for_apple_tvs from pyatv.exceptions import DeviceAuthenticationError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.discovery import SERVICE_APPLE_TV from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index f8fd2e0efa2..2f37d941ac8 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -1,10 +1,8 @@ { "domain": "apple_tv", - "name": "Apple tv", + "name": "Apple TV", "documentation": "https://www.home-assistant.io/integrations/apple_tv", - "requirements": [ - "pyatv==0.3.13" - ], + "requirements": ["pyatv==0.3.13"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json index 09d840e796a..6e8a0567d06 100644 --- a/homeassistant/components/apprise/manifest.json +++ b/homeassistant/components/apprise/manifest.json @@ -2,11 +2,7 @@ "domain": "apprise", "name": "Apprise", "documentation": "https://www.home-assistant.io/components/apprise", - "requirements": [ - "apprise==0.8.2" - ], + "requirements": ["apprise==0.8.2"], "dependencies": [], - "codeowners": [ - "@caronc" - ] + "codeowners": ["@caronc"] } diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py index 662cc9c1ab6..0c8c5b26eec 100644 --- a/homeassistant/components/apprise/notify.py +++ b/homeassistant/components/apprise/notify.py @@ -1,11 +1,8 @@ """Apprise platform for notify component.""" import logging -import voluptuous as vol - import apprise - -import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.components.notify import ( ATTR_TARGET, @@ -14,6 +11,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 0d23cedb4ee..6258b470ebb 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -3,11 +3,9 @@ import logging import threading -import geopy.distance import aprslib -from aprslib import ConnectionError as AprsConnectionError -from aprslib import LoginError - +from aprslib import ConnectionError as AprsConnectionError, LoginError +import geopy.distance import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA diff --git a/homeassistant/components/aprs/manifest.json b/homeassistant/components/aprs/manifest.json index c7615817b50..bc887505cd7 100644 --- a/homeassistant/components/aprs/manifest.json +++ b/homeassistant/components/aprs/manifest.json @@ -4,8 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/aprs", "dependencies": [], "codeowners": ["@PhilRW"], - "requirements": [ - "aprslib==0.6.46", - "geopy==1.19.0" - ] + "requirements": ["aprslib==0.6.46", "geopy==1.19.0"] } diff --git a/homeassistant/components/aqualogic/manifest.json b/homeassistant/components/aqualogic/manifest.json index 2e5dded4af6..f7f704e998b 100644 --- a/homeassistant/components/aqualogic/manifest.json +++ b/homeassistant/components/aqualogic/manifest.json @@ -1,10 +1,8 @@ { "domain": "aqualogic", - "name": "Aqualogic", + "name": "AquaLogic", "documentation": "https://www.home-assistant.io/integrations/aqualogic", - "requirements": [ - "aqualogic==1.0" - ], + "requirements": ["aqualogic==1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/aquostv/manifest.json b/homeassistant/components/aquostv/manifest.json index d4c491cb70d..8922249e3fa 100644 --- a/homeassistant/components/aquostv/manifest.json +++ b/homeassistant/components/aquostv/manifest.json @@ -1,10 +1,8 @@ { "domain": "aquostv", - "name": "Aquostv", + "name": "Sharp Aquos TV", "documentation": "https://www.home-assistant.io/integrations/aquostv", - "requirements": [ - "sharp_aquos_rc==0.3.2" - ], + "requirements": ["sharp_aquos_rc==0.3.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index d8770592c9f..f71f41dc293 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -2,10 +2,9 @@ import logging import sharp_aquos_rc - import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index bdb3bf67bbe..d818414753f 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -1,31 +1,32 @@ """Arcam component.""" -import logging import asyncio +import logging -import voluptuous as vol -import async_timeout -from arcam.fmj.client import Client from arcam.fmj import ConnectionFailed +from arcam.fmj.client import Client +import async_timeout +import voluptuous as vol from homeassistant import config_entries -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_NAME, CONF_PORT, CONF_SCAN_INTERVAL, CONF_ZONE, + EVENT_HOMEASSISTANT_STOP, SERVICE_TURN_ON, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + from .const import ( - DOMAIN, - DOMAIN_DATA_ENTRIES, - DOMAIN_DATA_CONFIG, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, + DOMAIN, + DOMAIN_DATA_CONFIG, + DOMAIN_DATA_ENTRIES, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, diff --git a/homeassistant/components/arcam_fmj/manifest.json b/homeassistant/components/arcam_fmj/manifest.json index 288b8fb3890..cb063c4c047 100644 --- a/homeassistant/components/arcam_fmj/manifest.json +++ b/homeassistant/components/arcam_fmj/manifest.json @@ -1,13 +1,9 @@ { "domain": "arcam_fmj", - "name": "Arcam FMJ Receiver control", + "name": "Arcam FMJ Receivers", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/arcam_fmj", - "requirements": [ - "arcam-fmj==0.4.3" - ], + "requirements": ["arcam-fmj==0.4.3"], "dependencies": [], - "codeowners": [ - "@elupus" - ] + "codeowners": ["@elupus"] } diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 231e9821dc6..8a54c745695 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -6,14 +6,13 @@ from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceC from arcam.fmj.state import State from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, - SUPPORT_TURN_ON, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, @@ -25,15 +24,16 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.core import callback from homeassistant.helpers.service import async_call_from_config +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( + DOMAIN, + DOMAIN_DATA_ENTRIES, SIGNAL_CLIENT_DATA, SIGNAL_CLIENT_STARTED, SIGNAL_CLIENT_STOPPED, - DOMAIN_DATA_ENTRIES, - DOMAIN, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py index f973ec136e3..61b03a3160d 100644 --- a/homeassistant/components/arduino/__init__.py +++ b/homeassistant/components/arduino/__init__.py @@ -1,13 +1,15 @@ """Support for Arduino boards running with the Firmata firmware.""" import logging +from PyMata.pymata import PyMata import serial import voluptuous as vol -from PyMata.pymata import PyMata - -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.const import CONF_PORT +from homeassistant.const import ( + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index a29f65700ff..aded8c1c9ac 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -2,11 +2,7 @@ "domain": "arduino", "name": "Arduino", "documentation": "https://www.home-assistant.io/integrations/arduino", - "requirements": [ - "PyMata==2.20" - ], + "requirements": ["PyMata==2.20"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/arduino/sensor.py b/homeassistant/components/arduino/sensor.py index a92432537ca..c5863475512 100644 --- a/homeassistant/components/arduino/sensor.py +++ b/homeassistant/components/arduino/sensor.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components import arduino +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/arduino/switch.py b/homeassistant/components/arduino/switch.py index 63d83c8575e..5b5b161a24a 100644 --- a/homeassistant/components/arduino/switch.py +++ b/homeassistant/components/arduino/switch.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import arduino -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/arest/binary_sensor.py b/homeassistant/components/arest/binary_sensor.py index 669a28b7078..3bd0a85c6f0 100644 --- a/homeassistant/components/arest/binary_sensor.py +++ b/homeassistant/components/arest/binary_sensor.py @@ -1,18 +1,18 @@ """Support for an exposed aREST RESTful API of a device.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, ) -from homeassistant.const import CONF_RESOURCE, CONF_PIN, CONF_NAME, CONF_DEVICE_CLASS -from homeassistant.util import Throttle +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_PIN, CONF_RESOURCE import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10).json() except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/arest/manifest.json b/homeassistant/components/arest/manifest.json index ee6b915e658..58eaad4648b 100644 --- a/homeassistant/components/arest/manifest.json +++ b/homeassistant/components/arest/manifest.json @@ -1,10 +1,8 @@ { "domain": "arest", - "name": "Arest", + "name": "aREST", "documentation": "https://www.home-assistant.io/integrations/arest", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/arest/sensor.py b/homeassistant/components/arest/sensor.py index 2416eeb0ebb..2533ce3619e 100644 --- a/homeassistant/components/arest/sensor.py +++ b/homeassistant/components/arest/sensor.py @@ -1,22 +1,22 @@ """Support for an exposed aREST RESTful API of a device.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, - CONF_RESOURCE, CONF_MONITORED_VARIABLES, CONF_NAME, + CONF_RESOURCE, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, ) from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -59,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10).json() except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/arest/switch.py b/homeassistant/components/arest/switch.py index e1a7edacb7e..ccc2c5d8bf5 100644 --- a/homeassistant/components/arest/switch.py +++ b/homeassistant/components/arest/switch.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_NAME, CONF_RESOURCE import homeassistant.helpers.config_validation as cv @@ -46,7 +46,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): response = requests.get(resource, timeout=10) except requests.exceptions.MissingSchema: _LOGGER.error( - "Missing resource or schema in configuration. " "Add http:// to your URL" + "Missing resource or schema in configuration. Add http:// to your URL" ) return False except requests.exceptions.ConnectionError: diff --git a/homeassistant/components/arlo/manifest.json b/homeassistant/components/arlo/manifest.json index 8779e051dc0..41d4fc40e5f 100644 --- a/homeassistant/components/arlo/manifest.json +++ b/homeassistant/components/arlo/manifest.json @@ -2,11 +2,7 @@ "domain": "arlo", "name": "Arlo", "documentation": "https://www.home-assistant.io/integrations/arlo", - "requirements": [ - "pyarlo==0.2.3" - ], - "dependencies": [ - "ffmpeg" - ], + "requirements": ["pyarlo==0.2.3"], + "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/homeassistant/components/aruba/manifest.json b/homeassistant/components/aruba/manifest.json index ccc4f119092..b871fa029cf 100644 --- a/homeassistant/components/aruba/manifest.json +++ b/homeassistant/components/aruba/manifest.json @@ -2,9 +2,7 @@ "domain": "aruba", "name": "Aruba", "documentation": "https://www.home-assistant.io/integrations/aruba", - "requirements": [ - "pexpect==4.6.0" - ], + "requirements": ["pexpect==4.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/arwn/manifest.json b/homeassistant/components/arwn/manifest.json index d84202cbac8..d756fa457d3 100644 --- a/homeassistant/components/arwn/manifest.json +++ b/homeassistant/components/arwn/manifest.json @@ -1,10 +1,8 @@ { "domain": "arwn", - "name": "Arwn", + "name": "Ambient Radio Weather Network", "documentation": "https://www.home-assistant.io/integrations/arwn", "requirements": [], - "dependencies": [ - "mqtt" - ], + "dependencies": ["mqtt"], "codeowners": [] } diff --git a/homeassistant/components/arwn/sensor.py b/homeassistant/components/arwn/sensor.py index 23cd811a3e0..685e5d90f53 100644 --- a/homeassistant/components/arwn/sensor.py +++ b/homeassistant/components/arwn/sensor.py @@ -3,8 +3,8 @@ import json import logging from homeassistant.components import mqtt +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.util import slugify diff --git a/homeassistant/components/asterisk_cdr/mailbox.py b/homeassistant/components/asterisk_cdr/mailbox.py index 4146ca9ddf9..0bae6ebf3ad 100644 --- a/homeassistant/components/asterisk_cdr/mailbox.py +++ b/homeassistant/components/asterisk_cdr/mailbox.py @@ -1,12 +1,14 @@ """Support for the Asterisk CDR interface.""" -import logging -import hashlib import datetime +import hashlib +import logging -from homeassistant.core import callback -from homeassistant.components.asterisk_mbox import SIGNAL_CDR_UPDATE -from homeassistant.components.asterisk_mbox import DOMAIN as ASTERISK_DOMAIN +from homeassistant.components.asterisk_mbox import ( + DOMAIN as ASTERISK_DOMAIN, + SIGNAL_CDR_UPDATE, +) from homeassistant.components.mailbox import Mailbox +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/asterisk_cdr/manifest.json b/homeassistant/components/asterisk_cdr/manifest.json index 56018ba777b..ac18b14682e 100644 --- a/homeassistant/components/asterisk_cdr/manifest.json +++ b/homeassistant/components/asterisk_cdr/manifest.json @@ -1,10 +1,8 @@ { "domain": "asterisk_cdr", - "name": "Asterisk cdr", + "name": "Asterisk Call Detail Records", "documentation": "https://www.home-assistant.io/integrations/asterisk_cdr", "requirements": [], - "dependencies": [ - "asterisk_mbox" - ], + "dependencies": ["asterisk_mbox"], "codeowners": [] } diff --git a/homeassistant/components/asterisk_mbox/manifest.json b/homeassistant/components/asterisk_mbox/manifest.json index cf793328d93..6a3591b001b 100644 --- a/homeassistant/components/asterisk_mbox/manifest.json +++ b/homeassistant/components/asterisk_mbox/manifest.json @@ -1,10 +1,8 @@ { "domain": "asterisk_mbox", - "name": "Asterisk mbox", + "name": "Asterisk Voicemail", "documentation": "https://www.home-assistant.io/integrations/asterisk_mbox", - "requirements": [ - "asterisk_mbox==0.5.0" - ], + "requirements": ["asterisk_mbox==0.5.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index 3d8cebce096..02999ada68b 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -2,11 +2,7 @@ "domain": "asuswrt", "name": "Asuswrt", "documentation": "https://www.home-assistant.io/integrations/asuswrt", - "requirements": [ - "aioasuswrt==1.1.22" - ], + "requirements": ["aioasuswrt==1.1.22"], "dependencies": [], - "codeowners": [ - "@kennedyshead" - ] + "codeowners": ["@kennedyshead"] } diff --git a/homeassistant/components/aten_pe/manifest.json b/homeassistant/components/aten_pe/manifest.json index 4f6416dd76c..c7910a1254b 100644 --- a/homeassistant/components/aten_pe/manifest.json +++ b/homeassistant/components/aten_pe/manifest.json @@ -1,12 +1,8 @@ { "domain": "aten_pe", - "name": "ATEN eco PDUs", + "name": "ATEN Rack PDU", "documentation": "https://www.home-assistant.io/integrations/aten_pe", - "requirements": [ - "atenpdu==0.3.0" - ], + "requirements": ["atenpdu==0.3.0"], "dependencies": [], - "codeowners": [ - "@mtdcr" - ] + "codeowners": ["@mtdcr"] } diff --git a/homeassistant/components/atome/manifest.json b/homeassistant/components/atome/manifest.json index 55739cad8cc..493940329f8 100644 --- a/homeassistant/components/atome/manifest.json +++ b/homeassistant/components/atome/manifest.json @@ -1,8 +1,8 @@ { "domain": "atome", - "name": "Atome", + "name": "Atome Linky", "documentation": "https://www.home-assistant.io/integrations/atome", "dependencies": [], "codeowners": ["@baqs"], "requirements": ["pyatome==0.1.1"] -} +} diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index c98b634bb21..f9dd6b2dd61 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -1,23 +1,22 @@ """Linky Atome.""" -import logging from datetime import timedelta +import logging +from pyatome.client import AtomeClient, PyAtomeError import voluptuous as vol -from pyatome.client import AtomeClient -from pyatome.client import PyAtomeError +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_USERNAME, - CONF_NAME, DEVICE_CLASS_POWER, - POWER_WATT, ENERGY_KILO_WATT_HOUR, + POWER_WATT, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 468e6e429a7..0bb0d639896 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -165,7 +165,7 @@ def setup(hass, config): _LOGGER.debug("August HTTP session closed.") hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, close_http_session) - _LOGGER.debug("Registered for HASS stop event") + _LOGGER.debug("Registered for Home Assistant stop event") return setup_august(hass, config, api, authenticator) @@ -254,7 +254,7 @@ class AugustData: ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve doorbell" " status for %s. %s", + "Request error trying to retrieve doorbell status for %s. %s", doorbell.device_name, ex, ) @@ -301,7 +301,7 @@ class AugustData: ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " status for %s. %s", + "Request error trying to retrieve door status for %s. %s", lock.device_name, ex, ) @@ -327,7 +327,7 @@ class AugustData: ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " status for %s. %s", + "Request error trying to retrieve door status for %s. %s", lock.device_name, ex, ) @@ -342,7 +342,7 @@ class AugustData: ) except RequestException as ex: _LOGGER.error( - "Request error trying to retrieve door" " details for %s. %s", + "Request error trying to retrieve door details for %s. %s", lock.device_name, ex, ) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index ebaa564736f..e3e417d20e0 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -2,9 +2,7 @@ "domain": "august", "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", - "requirements": [ - "py-august==0.7.0" - ], + "requirements": ["py-august==0.7.0"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/homeassistant/components/aurora/binary_sensor.py b/homeassistant/components/aurora/binary_sensor.py index a69433c4186..454c3ad2405 100644 --- a/homeassistant/components/aurora/binary_sensor.py +++ b/homeassistant/components/aurora/binary_sensor.py @@ -7,13 +7,13 @@ import requests import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric " "Administration" +ATTRIBUTION = "Data provided by the National Oceanic and Atmospheric Administration" CONF_THRESHOLD = "forecast_threshold" DEFAULT_DEVICE_CLASS = "visible" diff --git a/homeassistant/components/aurora_abb_powerone/manifest.json b/homeassistant/components/aurora_abb_powerone/manifest.json index f49421ea954..18e5a4b5ed9 100644 --- a/homeassistant/components/aurora_abb_powerone/manifest.json +++ b/homeassistant/components/aurora_abb_powerone/manifest.json @@ -3,8 +3,6 @@ "name": "Aurora ABB Solar PV", "documentation": "https://www.home-assistant.io/integrations/aurora_abb_powerone/", "dependencies": [], - "codeowners": [ - "@davet2001" - ], + "codeowners": ["@davet2001"], "requirements": ["aurorapy==0.2.6"] } diff --git a/homeassistant/components/aurora_abb_powerone/sensor.py b/homeassistant/components/aurora_abb_powerone/sensor.py index 05ed5fa99bf..a2645e5d7cb 100644 --- a/homeassistant/components/aurora_abb_powerone/sensor.py +++ b/homeassistant/components/aurora_abb_powerone/sensor.py @@ -2,8 +2,8 @@ import logging +from aurorapy.client import AuroraError, AuroraSerialClient import voluptuous as vol -from aurorapy.client import AuroraSerialClient, AuroraError from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( diff --git a/homeassistant/components/auth/.translations/da.json b/homeassistant/components/auth/.translations/da.json index f461f376d16..7877a813218 100644 --- a/homeassistant/components/auth/.translations/da.json +++ b/homeassistant/components/auth/.translations/da.json @@ -2,7 +2,7 @@ "mfa_setup": { "notify": { "abort": { - "no_available_service": "Ingen underretningstjenester til r\u00e5dighed." + "no_available_service": "Ingen meddelelsestjenester tilg\u00e6ngelige." }, "error": { "invalid_code": "Ugyldig kode, pr\u00f8v venligst igen." @@ -10,14 +10,14 @@ "step": { "init": { "description": "V\u00e6lg venligst en af meddelelsestjenesterne:", - "title": "Ops\u00e6t engangsadgangskode, der er leveret af besked komponenten" + "title": "Ops\u00e6t engangsadgangskoder leveret af notify-komponenten" }, "setup": { "description": "En engangsadgangskode er blevet sendt via **notify.{notify_service}**. Indtast den venligst nedenunder:", "title": "Bekr\u00e6ft ops\u00e6tningen" } }, - "title": "Advis\u00e9r engangskodeord" + "title": "Notify-engangsadgangskode" }, "totp": { "error": { @@ -25,8 +25,8 @@ }, "step": { "init": { - "description": "Hvis du vil aktivere tofaktorautentificering ved hj\u00e6lp af tidsbaserede engangskoder skal du scanne QR-koden med din autentificeringsapp. Hvis du ikke har en anbefaler vi enten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har scannet koden skal du indtaste den sekscifrede kode fra din app for at bekr\u00e6fte ops\u00e6tningen. Hvis du har problemer med at scanne QR-koden skal du lave en manuel ops\u00e6tning med kode **`{code}`**.", - "title": "Konfigurer to-faktors godkendelse ved hj\u00e6lp af TOTP" + "description": "Hvis du vil aktivere tofaktorgodkendelse ved hj\u00e6lp af tidsbaserede engangskoder skal du scanne QR-koden med din autentificeringsapp. Hvis du ikke har en anbefaler vi enten [Google Authenticator] (https://support.google.com/accounts/answer/1066447) eller [Authy] (https://authy.com/). \n\n {qr_code} \n \nN\u00e5r du har scannet koden skal du indtaste den sekscifrede kode fra din app for at bekr\u00e6fte ops\u00e6tningen. Hvis du har problemer med at scanne QR-koden skal du lave en manuel ops\u00e6tning med kode **`{code}`**.", + "title": "Konfigurer tofaktorgodkendelse ved hj\u00e6lp af TOTP" } }, "title": "TOTP" diff --git a/homeassistant/components/auth/.translations/nl.json b/homeassistant/components/auth/.translations/nl.json index 9ec8006507b..d61613097dd 100644 --- a/homeassistant/components/auth/.translations/nl.json +++ b/homeassistant/components/auth/.translations/nl.json @@ -9,7 +9,7 @@ }, "step": { "init": { - "description": "Selecteer een van de meldingsdiensten:", + "description": "Selecteer een van de meldingsservices:", "title": "Stel een \u00e9\u00e9nmalig wachtwoord in dat wordt afgegeven door een meldingscomponent" }, "setup": { diff --git a/homeassistant/components/auth/__init__.py b/homeassistant/components/auth/__init__.py index d0da9d39fe8..888ef98a582 100644 --- a/homeassistant/components/auth/__init__.py +++ b/homeassistant/components/auth/__init__.py @@ -114,31 +114,29 @@ Result will be a long-lived access token: } """ +from datetime import timedelta import logging import uuid -from datetime import timedelta from aiohttp import web import voluptuous as vol from homeassistant.auth.models import ( - User, - Credentials, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN, + Credentials, + User, ) -from homeassistant.loader import bind_hass from homeassistant.components import websocket_api from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.ban import log_invalid_auth from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util -from . import indieauth -from . import login_flow -from . import mfa_setup_flow +from . import indieauth, login_flow, mfa_setup_flow DOMAIN = "auth" WS_TYPE_CURRENT_USER = "auth/current_user" diff --git a/homeassistant/components/auth/indieauth.py b/homeassistant/components/auth/indieauth.py index 6a0a516bee2..3266ae65d7a 100644 --- a/homeassistant/components/auth/indieauth.py +++ b/homeassistant/components/auth/indieauth.py @@ -1,9 +1,9 @@ """Helpers to resolve client ID/secret.""" -import logging import asyncio -from ipaddress import ip_address from html.parser import HTMLParser -from urllib.parse import urlparse, urljoin +from ipaddress import ip_address +import logging +from urllib.parse import urljoin, urlparse import aiohttp @@ -30,6 +30,14 @@ async def verify_redirect_uri(hass, client_id, redirect_uri): if is_valid: return True + # Whitelist the iOS and Android callbacks so that people can link apps + # without being connected to the internet. + if redirect_uri == "homeassistant://auth-callback" and client_id in ( + "https://home-assistant.io/android", + "https://home-assistant.io/iOS", + ): + return True + # IndieAuth 4.2.2 allows for redirect_uri to be on different domain # but needs to be specified in link tag when fetching `client_id`. redirect_uris = await fetch_redirect_uris(hass, client_id) @@ -91,7 +99,7 @@ async def fetch_redirect_uris(hass, url): pass except aiohttp.client_exceptions.ClientConnectionError: _LOGGER.error( - ("Low level connection error while looking up " "redirect_uri %s"), url + "Low level connection error while looking up redirect_uri %s", url ) pass except aiohttp.client_exceptions.ClientError: diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index d6844396ce7..6f8d2751018 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -73,12 +73,13 @@ import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components.http import KEY_REAL_IP from homeassistant.components.http.ban import ( - process_wrong_login, - process_success_login, log_invalid_auth, + process_success_login, + process_wrong_login, ) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView + from . import indieauth diff --git a/homeassistant/components/auth/manifest.json b/homeassistant/components/auth/manifest.json index 2f3e724b583..e2b49ccfec1 100644 --- a/homeassistant/components/auth/manifest.json +++ b/homeassistant/components/auth/manifest.json @@ -5,5 +5,6 @@ "requirements": [], "dependencies": ["http"], "after_dependencies": ["onboarding"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 271e9ae1634..1b199551a14 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -6,7 +6,7 @@ import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components import websocket_api -from homeassistant.core import callback, HomeAssistant +from homeassistant.core import HomeAssistant, callback WS_TYPE_SETUP_MFA = "auth/setup_mfa" SCHEMA_WS_SETUP_MFA = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( @@ -28,25 +28,27 @@ DATA_SETUP_FLOW_MGR = "auth_mfa_setup_flow_manager" _LOGGER = logging.getLogger(__name__) -async def async_setup(hass): - """Init mfa setup flow manager.""" +class MfaFlowManager(data_entry_flow.FlowManager): + """Manage multi factor authentication flows.""" - async def _async_create_setup_flow(handler, context, data): + async def async_create_flow(self, handler_key, *, context, data): """Create a setup flow. handler is a mfa module.""" - mfa_module = hass.auth.get_auth_mfa_module(handler) + mfa_module = self.hass.auth.get_auth_mfa_module(handler_key) if mfa_module is None: - raise ValueError(f"Mfa module {handler} is not found") + raise ValueError(f"Mfa module {handler_key} is not found") user_id = data.pop("user_id") return await mfa_module.async_setup_flow(user_id) - async def _async_finish_setup_flow(flow, flow_result): - _LOGGER.debug("flow_result: %s", flow_result) - return flow_result + async def async_finish_flow(self, flow, result): + """Complete an mfs setup flow.""" + _LOGGER.debug("flow_result: %s", result) + return result - hass.data[DATA_SETUP_FLOW_MGR] = data_entry_flow.FlowManager( - hass, _async_create_setup_flow, _async_finish_setup_flow - ) + +async def async_setup(hass): + """Init mfa setup flow manager.""" + hass.data[DATA_SETUP_FLOW_MGR] = MfaFlowManager(hass) hass.components.websocket_api.async_register_command( WS_TYPE_SETUP_MFA, websocket_setup_mfa, SCHEMA_WS_SETUP_MFA diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py index fbb823dd329..3c9e33cdc84 100644 --- a/homeassistant/components/automatic/device_tracker.py +++ b/homeassistant/components/automatic/device_tracker.py @@ -5,9 +5,8 @@ import json import logging import os -from aiohttp import web import aioautomatic - +from aiohttp import web import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -233,7 +232,7 @@ class AutomaticData: if event.created_at < self.vehicle_seen[event.vehicle.id]: # Skip events received out of order _LOGGER.debug( - "Skipping out of order event. Event Created %s. " "Last seen event: %s", + "Skipping out of order event. Event Created %s. Last seen event: %s", event.created_at, self.vehicle_seen[event.vehicle.id], ) diff --git a/homeassistant/components/automatic/manifest.json b/homeassistant/components/automatic/manifest.json index 63cf0da0f46..e0d06ff0f1f 100644 --- a/homeassistant/components/automatic/manifest.json +++ b/homeassistant/components/automatic/manifest.json @@ -2,14 +2,7 @@ "domain": "automatic", "name": "Automatic", "documentation": "https://www.home-assistant.io/integrations/automatic", - "requirements": [ - "aioautomatic==0.6.5" - ], - "dependencies": [ - "configurator", - "http" - ], - "codeowners": [ - "@armills" - ] + "requirements": ["aioautomatic==0.6.5"], + "dependencies": ["configurator", "http"], + "codeowners": ["@armills"] } diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 3863ab0c88d..6175646778f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -28,11 +28,11 @@ from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow - # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -50,6 +50,7 @@ CONF_ACTION = "action" CONF_TRIGGER = "trigger" CONF_CONDITION_TYPE = "condition_type" CONF_INITIAL_STATE = "initial_state" +CONF_SKIP_CONDITION = "skip_condition" CONDITION_USE_TRIGGER_VALUES = "use_trigger_values" CONDITION_TYPE_AND = "and" @@ -107,7 +108,10 @@ PLATFORM_SCHEMA = vol.Schema( ) TRIGGER_SERVICE_SCHEMA = make_entity_service_schema( - {vol.Optional(ATTR_VARIABLES, default={}): dict} + { + vol.Optional(ATTR_VARIABLES, default={}): dict, + vol.Optional(CONF_SKIP_CONDITION, default=True): bool, + } ) RELOAD_SERVICE_SCHEMA = vol.Schema({}) @@ -125,9 +129,7 @@ def is_on(hass, entity_id): async def async_setup(hass, config): """Set up the automation.""" - component = EntityComponent( - _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_AUTOMATIONS - ) + component = EntityComponent(_LOGGER, DOMAIN, hass) await _async_process_config(hass, config, component) @@ -137,8 +139,8 @@ async def async_setup(hass, config): for entity in await component.async_extract_from_service(service_call): tasks.append( entity.async_trigger( - service_call.data.get(ATTR_VARIABLES), - skip_condition=True, + service_call.data[ATTR_VARIABLES], + skip_condition=service_call.data[CONF_SKIP_CONDITION], context=service_call.context, ) ) @@ -179,8 +181,12 @@ async def async_setup(hass, config): DOMAIN, SERVICE_TRIGGER, trigger_service_handler, schema=TRIGGER_SERVICE_SCHEMA ) - hass.services.async_register( - DOMAIN, SERVICE_RELOAD, reload_service_handler, schema=RELOAD_SERVICE_SCHEMA + async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, ) hass.services.async_register( @@ -271,7 +277,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: enable_automation = DEFAULT_INITIAL_STATE _LOGGER.debug( - "Automation %s not in state storage, state %s from " "default is used.", + "Automation %s not in state storage, state %s from default is used.", self.entity_id, enable_automation, ) @@ -319,7 +325,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): await self.async_update_ha_state() async def async_will_remove_from_hass(self): - """Remove listeners when removing automation from HASS.""" + """Remove listeners when removing automation from Home Assistant.""" await super().async_will_remove_from_hass() await self.async_disable() diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 5733cd2e83e..d11472a2128 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -7,8 +7,8 @@ import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain +from homeassistant.const import CONF_PLATFORM from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition, config_per_platform, script from homeassistant.loader import IntegrationNotFound diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index ced8f65cbf5..b2892d1abaa 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -7,7 +7,6 @@ from homeassistant.components.device_automation import ( ) from homeassistant.const import CONF_DOMAIN - # mypy: allow-untyped-defs, no-check-untyped-defs TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index 26dacac974d..9fc78746a7c 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -3,11 +3,10 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv - # mypy: allow-untyped-defs CONF_EVENT_TYPE = "event_type" diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 0ef0884d329..5dc4f3c80f6 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant.components.geo_location import DOMAIN -from homeassistant.core import callback from homeassistant.const import ( CONF_EVENT, CONF_PLATFORM, @@ -10,10 +9,10 @@ from homeassistant.const import ( CONF_ZONE, EVENT_STATE_CHANGED, ) +from homeassistant.core import callback from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.config_validation import entity_domain - # mypy: allow-untyped-defs, no-check-untyped-defs EVENT_ENTER = "enter" diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index e4eb029d5aa..743b169c86c 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -3,9 +3,8 @@ import logging import voluptuous as vol -from homeassistant.core import callback, CoreState -from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP - +from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import CoreState, callback # mypy: allow-untyped-defs diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index 9512db8261d..466fc941a9a 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -3,12 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_point_in_utc_time - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 79a6877692e..34261cba5a9 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -3,12 +3,7 @@ "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", "requirements": [], - "dependencies": [ - "device_automation", - "group", - "webhook" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["device_automation", "group", "webhook"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 135a421f72e..fb0073c78d5 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -3,12 +3,11 @@ import json import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt -from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD +from homeassistant.const import CONF_PAYLOAD, CONF_PLATFORM +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv - # mypy: allow-untyped-defs CONF_ENCODING = "encoding" diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 0c8ab3d9c8b..e944b66751b 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -4,18 +4,17 @@ import logging import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.const import ( - CONF_VALUE_TEMPLATE, - CONF_PLATFORM, - CONF_ENTITY_ID, - CONF_BELOW, CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, CONF_FOR, + CONF_PLATFORM, + CONF_VALUE_TEMPLATE, ) -from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import condition, config_validation as cv, template - +from homeassistant.helpers.event import async_track_same_state, async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py index 553d6871087..4cfe519d585 100644 --- a/homeassistant/components/automation/reproduce_state.py +++ b/homeassistant/components/automation/reproduce_state.py @@ -5,10 +5,10 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/automation/services.yaml b/homeassistant/components/automation/services.yaml index 90f66036706..ce54220aff4 100644 --- a/homeassistant/components/automation/services.yaml +++ b/homeassistant/components/automation/services.yaml @@ -27,6 +27,9 @@ trigger: entity_id: description: Name of the automation to trigger. example: 'automation.notify_home' + skip_condition: + description: Whether or not the condition will be skipped (defaults to True). + example: True reload: description: Reload the automation configuration. diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 47c44587b08..fc3fff47514 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -6,11 +6,10 @@ from typing import Dict import voluptuous as vol from homeassistant import exceptions -from homeassistant.core import HomeAssistant, CALLBACK_TYPE, callback -from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR +from homeassistant.const import CONF_FOR, CONF_PLATFORM, MATCH_ALL +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.event import async_track_state_change, async_track_same_state - +from homeassistant.helpers.event import async_track_same_state, async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index 66892784a54..c416742f397 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -4,16 +4,15 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( CONF_EVENT, CONF_OFFSET, CONF_PLATFORM, SUN_EVENT_SUNRISE, ) -from homeassistant.helpers.event import async_track_sunrise, async_track_sunset +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv - +from homeassistant.helpers.event import async_track_sunrise, async_track_sunset # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 95b6b857c9d..ee4484410cd 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -3,13 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback -from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_FOR from homeassistant import exceptions -from homeassistant.helpers import condition +from homeassistant.const import CONF_FOR, CONF_PLATFORM, CONF_VALUE_TEMPLATE +from homeassistant.core import callback +from homeassistant.helpers import condition, config_validation as cv, template from homeassistant.helpers.event import async_track_same_state, async_track_template -from homeassistant.helpers import config_validation as cv, template - # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 231bc346e14..5f461952960 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -3,12 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_AT, CONF_PLATFORM +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index ee092916112..65d44f5b1ca 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -3,12 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_time_change - # mypy: allow-untyped-defs, no-check-untyped-defs CONF_HOURS = "hours" diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index bbcf9bd9ddc..5d01c6454a8 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -5,13 +5,12 @@ import logging from aiohttp import hdrs import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM, CONF_WEBHOOK_ID +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import DOMAIN as AUTOMATION_DOMAIN - # mypy: allow-untyped-defs DEPENDENCIES = ("webhook",) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 535ef298a2a..3dba1a4df35 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -1,17 +1,16 @@ """Offer zone automation rules.""" import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( - CONF_EVENT, CONF_ENTITY_ID, + CONF_EVENT, + CONF_PLATFORM, CONF_ZONE, MATCH_ALL, - CONF_PLATFORM, ) -from homeassistant.helpers.event import async_track_state_change +from homeassistant.core import callback from homeassistant.helpers import condition, config_validation as cv, location - +from homeassistant.helpers.event import async_track_state_change # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/avea/light.py b/homeassistant/components/avea/light.py index e6ceedcf96d..92d66a554da 100644 --- a/homeassistant/components/avea/light.py +++ b/homeassistant/components/avea/light.py @@ -1,5 +1,6 @@ """Support for the Elgato Avea lights.""" import logging + import avea from homeassistant.components.light import ( @@ -12,7 +13,6 @@ from homeassistant.components.light import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.util.color as color_util - _LOGGER = logging.getLogger(__name__) SUPPORT_AVEA = SUPPORT_BRIGHTNESS | SUPPORT_COLOR diff --git a/homeassistant/components/avion/manifest.json b/homeassistant/components/avion/manifest.json index 8bb8b56cb9d..cfdda5a0d84 100644 --- a/homeassistant/components/avion/manifest.json +++ b/homeassistant/components/avion/manifest.json @@ -1,10 +1,8 @@ { "domain": "avion", - "name": "Avion", + "name": "Avi-on", "documentation": "https://www.home-assistant.io/integrations/avion", - "requirements": [ - "avion==0.10" - ], + "requirements": ["avion==0.10"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json index f4e632cb7d2..2741ad358f6 100644 --- a/homeassistant/components/awair/manifest.json +++ b/homeassistant/components/awair/manifest.json @@ -2,11 +2,7 @@ "domain": "awair", "name": "Awair", "documentation": "https://www.home-assistant.io/integrations/awair", - "requirements": [ - "python_awair==0.0.4" - ], + "requirements": ["python_awair==0.0.4"], "dependencies": [], - "codeowners": [ - "@danielsjf" - ] + "codeowners": ["@danielsjf"] } diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index b553b7eafd6..600874b0d25 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -1,10 +1,9 @@ """Support for Amazon Web Services (AWS).""" import asyncio -import logging from collections import OrderedDict +import logging import aiobotocore - import voluptuous as vol from homeassistant import config_entries diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index b617eb75ee1..7b706eb1bfa 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -1,13 +1,8 @@ { "domain": "aws", - "name": "Aws", + "name": "Amazon Web Services (AWS)", "documentation": "https://www.home-assistant.io/integrations/aws", - "requirements": [ - "aiobotocore==0.10.4" - ], + "requirements": ["aiobotocore==0.10.4"], "dependencies": [], - "codeowners": [ - "@awarecan", - "@robbiet480" - ] + "codeowners": ["@awarecan", "@robbiet480"] } diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index 2afa9a3a402..13fa189a318 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -12,8 +12,9 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) -from homeassistant.const import CONF_PLATFORM, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers.json import JSONEncoder + from .const import ( CONF_CONTEXT, CONF_CREDENTIAL_NAME, diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json index 3458dcc4529..ecf7b552bba 100644 --- a/homeassistant/components/axis/.translations/ca.json +++ b/homeassistant/components/axis/.translations/ca.json @@ -4,7 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "bad_config_file": "Dades incorrectes del fitxer de configuraci\u00f3", "link_local_address": "L'enlla\u00e7 d'adreces locals no est\u00e0 disponible", - "not_axis_device": "El dispositiu descobert no \u00e9s un dispositiu Axis" + "not_axis_device": "El dispositiu descobert no \u00e9s un dispositiu Axis", + "updated_configuration": "S'ha actualitzat la configuraci\u00f3 del dispositiu amb l'adre\u00e7a nova" }, "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json index c169f85f280..355dbad83d5 100644 --- a/homeassistant/components/axis/.translations/da.json +++ b/homeassistant/components/axis/.translations/da.json @@ -4,15 +4,16 @@ "already_configured": "Enheden er allerede konfigureret", "bad_config_file": "Forkerte data fra konfigurationsfilen", "link_local_address": "Link lokale adresser underst\u00f8ttes ikke", - "not_axis_device": "Fundet enhed ikke en Axis enhed" + "not_axis_device": "Fundet enhed ikke en Axis enhed", + "updated_configuration": "Opdaterede enhedskonfiguration med ny v\u00e6rtsadresse" }, "error": { "already_configured": "Enheden er allerede konfigureret", - "already_in_progress": "Enheds konfiguration er allerede i gang.", + "already_in_progress": "Enhedskonfiguration er allerede i gang.", "device_unavailable": "Enheden er ikke tilg\u00e6ngelig", "faulty_credentials": "Ugyldige legitimationsoplysninger" }, - "flow_title": "Axis enhed: {name} ({host})", + "flow_title": "Axis-enhed: {name} ({host})", "step": { "user": { "data": { @@ -21,9 +22,9 @@ "port": "Port", "username": "Brugernavn" }, - "title": "Konfigurer Axis enhed" + "title": "Indstil Axis-enhed" } }, - "title": "Axis enhed" + "title": "Axis-enhed" } } \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/de.json b/homeassistant/components/axis/.translations/de.json index 05c1853d769..a92c948a2a7 100644 --- a/homeassistant/components/axis/.translations/de.json +++ b/homeassistant/components/axis/.translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "bad_config_file": "Fehlerhafte Daten aus der Konfigurationsdatei", "link_local_address": "Link-local Adressen werden nicht unterst\u00fctzt", - "not_axis_device": "Erkanntes Ger\u00e4t ist kein Axis-Ger\u00e4t" + "not_axis_device": "Erkanntes Ger\u00e4t ist kein Axis-Ger\u00e4t", + "updated_configuration": "Ger\u00e4tekonfiguration mit neuer Hostadresse aktualisiert" }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", @@ -12,6 +13,7 @@ "device_unavailable": "Ger\u00e4t ist nicht verf\u00fcgbar", "faulty_credentials": "Ung\u00fcltige Anmeldeinformationen" }, + "flow_title": "Achsenger\u00e4t: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json index c7d84aa8cc3..abc1e2f17ec 100644 --- a/homeassistant/components/axis/.translations/en.json +++ b/homeassistant/components/axis/.translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured", "bad_config_file": "Bad data from config file", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Discovered device not an Axis device" + "not_axis_device": "Discovered device not an Axis device", + "updated_configuration": "Updated device configuration with new host address" }, "error": { "already_configured": "Device is already configured", diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index 3f7db674fdf..885e8f68913 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -4,7 +4,8 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "bad_config_file": "Datos err\u00f3neos en el archivo de configuraci\u00f3n", "link_local_address": "Las direcciones de enlace locales no son compatibles", - "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" + "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis", + "updated_configuration": "Configuraci\u00f3n del dispositivo actualizada con la nueva direcci\u00f3n de host" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index 3f303140c68..9e2eecf5747 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -4,7 +4,8 @@ "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" + "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis", + "updated_configuration": "Configurazione del dispositivo aggiornata con nuovo indirizzo host" }, "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json index f02b7cdcefa..e471ae3ea7a 100644 --- a/homeassistant/components/axis/.translations/ko.json +++ b/homeassistant/components/axis/.translations/ko.json @@ -4,7 +4,8 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "bad_config_file": "\uad6c\uc131 \ud30c\uc77c\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "not_axis_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Axis \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" + "not_axis_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Axis \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", + "updated_configuration": "\uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ub41c \uae30\uae30 \uad6c\uc131" }, "error": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/axis/.translations/lb.json b/homeassistant/components/axis/.translations/lb.json index 24ee0e24125..589932cd68e 100644 --- a/homeassistant/components/axis/.translations/lb.json +++ b/homeassistant/components/axis/.translations/lb.json @@ -4,7 +4,8 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert", "bad_config_file": "Feelerhaft Donn\u00e9e\u00eb aus der Konfiguratioun's Datei", "link_local_address": "Lokal Link Adressen ginn net \u00ebnnerst\u00ebtzt", - "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat" + "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat", + "updated_configuration": "Konfiguratioun vum Apparat gouf mat der neier Adress aktualis\u00e9iert" }, "error": { "already_configured": "Apparat ass scho konfigur\u00e9iert", diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json index 190737e5a76..60db56146fa 100644 --- a/homeassistant/components/axis/.translations/no.json +++ b/homeassistant/components/axis/.translations/no.json @@ -4,7 +4,8 @@ "already_configured": "Enheten er allerede konfigurert", "bad_config_file": "D\u00e5rlig data fra konfigurasjonsfilen", "link_local_address": "Linking av lokale adresser st\u00f8ttes ikke", - "not_axis_device": "Oppdaget enhet ikke en Axis enhet" + "not_axis_device": "Oppdaget enhet ikke en Axis enhet", + "updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse" }, "error": { "already_configured": "Enheten er allerede konfigurert", diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json index 4ca87310f48..d5deb327a75 100644 --- a/homeassistant/components/axis/.translations/pl.json +++ b/homeassistant/components/axis/.translations/pl.json @@ -4,7 +4,8 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego", "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", - "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis" + "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis", + "updated_configuration": "Zaktualizowano konfiguracj\u0119 urz\u0105dzenia o nowy adres hosta" }, "error": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", diff --git a/homeassistant/components/axis/.translations/pt-BR.json b/homeassistant/components/axis/.translations/pt-BR.json index 453c8fa3643..ceb6325af60 100644 --- a/homeassistant/components/axis/.translations/pt-BR.json +++ b/homeassistant/components/axis/.translations/pt-BR.json @@ -4,7 +4,8 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "bad_config_file": "Dados incorretos do arquivo de configura\u00e7\u00e3o", "link_local_address": "Link de endere\u00e7os locais n\u00e3o s\u00e3o suportados", - "not_axis_device": "Dispositivo descoberto n\u00e3o \u00e9 um dispositivo Axis" + "not_axis_device": "Dispositivo descoberto n\u00e3o \u00e9 um dispositivo Axis", + "updated_configuration": "Configura\u00e7\u00e3o do dispositivo atualizada com novo endere\u00e7o de host" }, "error": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 24990bb0f1a..b0da189d20f 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", - "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis." + "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis.", + "updated_configuration": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, diff --git a/homeassistant/components/axis/.translations/sl.json b/homeassistant/components/axis/.translations/sl.json index 5ffa02e19f7..43a352c4bc0 100644 --- a/homeassistant/components/axis/.translations/sl.json +++ b/homeassistant/components/axis/.translations/sl.json @@ -4,7 +4,8 @@ "already_configured": "Naprava je \u017ee konfigurirana", "bad_config_file": "Napa\u010dni podatki iz konfiguracijske datoteke", "link_local_address": "Lokalni naslovi povezave niso podprti", - "not_axis_device": "Odkrita naprava ni naprava Axis" + "not_axis_device": "Odkrita naprava ni naprava Axis", + "updated_configuration": "Posodobljena konfiguracija naprave z novim naslovom gostitelja" }, "error": { "already_configured": "Naprava je \u017ee konfigurirana", diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index 6c78fc2166c..751a7544202 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -4,7 +4,8 @@ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", - "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099" + "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099", + "updated_configuration": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0\u88dd\u7f6e\u8a2d\u5b9a" }, "error": { "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index bdda82b2145..5c928aa9f31 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -1,43 +1,18 @@ """Support for Axis devices.""" -import voluptuous as vol - -from homeassistant import config_entries from homeassistant.const import ( CONF_DEVICE, CONF_MAC, - CONF_NAME, CONF_TRIGGER_TIME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers import config_validation as cv -from .config_flow import DEVICE_SCHEMA from .const import CONF_CAMERA, CONF_EVENTS, DEFAULT_TRIGGER_TIME, DOMAIN from .device import AxisNetworkDevice, get_device -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: cv.schema_with_slug_keys(DEVICE_SCHEMA)}, extra=vol.ALLOW_EXTRA -) - async def async_setup(hass, config): - """Set up for Axis devices.""" - if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: - - for device_name, device_config in config[DOMAIN].items(): - - if CONF_NAME not in device_config: - device_config[CONF_NAME] = device_name - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=device_config, - ) - ) - + """Old way to set up Axis devices.""" return True @@ -54,6 +29,12 @@ async def async_setup_entry(hass, config_entry): if not await device.async_setup(): return False + # 0.104 introduced config entry unique id, this makes upgrading possible + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=device.api.vapix.params.system_serialnumber + ) + hass.data[DOMAIN][device.serial] = device await device.async_update_device_registry() diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 1d12e0b8d61..b3593179ffc 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -5,7 +5,7 @@ from datetime import timedelta from axis.event_stream import CLASS_INPUT, CLASS_OUTPUT from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import CONF_MAC, CONF_TRIGGER_TIME +from homeassistant.const import CONF_TRIGGER_TIME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_point_in_utc_time @@ -17,8 +17,7 @@ from .const import DOMAIN as AXIS_DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Axis binary sensor.""" - serial_number = config_entry.data[CONF_MAC] - device = hass.data[AXIS_DOMAIN][serial_number] + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback def async_add_sensor(event_id): diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index a55e45dd374..6b82c938a99 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_AUTHENTICATION, CONF_DEVICE, CONF_HOST, - CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -32,8 +31,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Axis camera video stream.""" filter_urllib3_logging() - serial_number = config_entry.data[CONF_MAC] - device = hass.data[AXIS_DOMAIN][serial_number] + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] config = { CONF_NAME: config_entry.data[CONF_NAME], diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 5eb4f9daddd..88c1cab98c1 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -12,13 +12,10 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.util.json import load_json from .const import CONF_MODEL, DOMAIN from .device import get_device -from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect +from .errors import AuthenticationRequired, CannotConnect AXIS_OUI = {"00408C", "ACCC8E", "B8A44F"} @@ -30,31 +27,8 @@ PLATFORMS = ["camera"] AXIS_INCLUDE = EVENT_TYPES + PLATFORMS -AXIS_DEFAULT_HOST = "192.168.0.90" -AXIS_DEFAULT_USERNAME = "root" -AXIS_DEFAULT_PASSWORD = "pass" DEFAULT_PORT = 80 -DEVICE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_HOST, default=AXIS_DEFAULT_HOST): cv.string, - vol.Optional(CONF_USERNAME, default=AXIS_DEFAULT_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD, default=AXIS_DEFAULT_PASSWORD): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - }, - extra=vol.ALLOW_EXTRA, -) - - -@callback -def configured_devices(hass): - """Return a set of the configured devices.""" - return { - entry.data[CONF_MAC]: entry - for entry in hass.config_entries.async_entries(DOMAIN) - } - class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Axis config flow.""" @@ -90,33 +64,30 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): device = await get_device(self.hass, self.device_config) self.serial_number = device.vapix.params.system_serialnumber - - if self.serial_number in configured_devices(self.hass): - raise AlreadyConfigured + config_entry = await self.async_set_unique_id(self.serial_number) + if config_entry: + return self._update_entry( + config_entry, + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + ) self.model = device.vapix.params.prodnbr return await self._create_entry() - except AlreadyConfigured: - errors["base"] = "already_configured" - except AuthenticationRequired: errors["base"] = "faulty_credentials" except CannotConnect: errors["base"] = "device_unavailable" - data = ( - self.import_schema - or self.discovery_schema - or { - vol.Required(CONF_HOST): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): int, - } - ) + data = self.discovery_schema or { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + } return self.async_show_form( step_id="user", @@ -130,18 +101,17 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): Generate a name to be used as a prefix for device entities. """ - if self.name is None: - same_model = [ - entry.data[CONF_NAME] - for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.data[CONF_MODEL] == self.model - ] + same_model = [ + entry.data[CONF_NAME] + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.data[CONF_MODEL] == self.model + ] - self.name = f"{self.model}" - for idx in range(len(same_model) + 1): - self.name = f"{self.model} {idx}" - if self.name not in same_model: - break + self.name = f"{self.model}" + for idx in range(len(same_model) + 1): + self.name = f"{self.model} {idx}" + if self.name not in same_model: + break data = { CONF_DEVICE: self.device_config, @@ -153,43 +123,37 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title = f"{self.model} - {self.serial_number}" return self.async_create_entry(title=title, data=data) - async def _update_entry(self, entry, host): - """Update existing entry if it is the same device.""" + def _update_entry(self, entry, host, port): + """Update existing entry.""" + if ( + entry.data[CONF_DEVICE][CONF_HOST] == host + and entry.data[CONF_DEVICE][CONF_PORT] == port + ): + return self.async_abort(reason="already_configured") + entry.data[CONF_DEVICE][CONF_HOST] = host + entry.data[CONF_DEVICE][CONF_PORT] = port + self.hass.config_entries.async_update_entry(entry) + return self.async_abort(reason="updated_configuration") async def async_step_zeroconf(self, discovery_info): - """Prepare configuration for a discovered Axis device. + """Prepare configuration for a discovered Axis device.""" + serial_number = discovery_info["properties"]["macaddress"] - This flow is triggered by the discovery component. - """ - serialnumber = discovery_info["properties"]["macaddress"] - - if serialnumber[:6] not in AXIS_OUI: + if serial_number[:6] not in AXIS_OUI: return self.async_abort(reason="not_axis_device") if discovery_info[CONF_HOST].startswith("169.254"): return self.async_abort(reason="link_local_address") - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context["macaddress"] = serialnumber - - if any( - serialnumber == flow["context"]["macaddress"] - for flow in self._async_in_progress() - ): - return self.async_abort(reason="already_in_progress") - - device_entries = configured_devices(self.hass) - - if serialnumber in device_entries: - entry = device_entries[serialnumber] - await self._update_entry(entry, discovery_info[CONF_HOST]) - return self.async_abort(reason="already_configured") - - config_file = await self.hass.async_add_executor_job( - load_json, self.hass.config.path(CONFIG_FILE) - ) + config_entry = await self.async_set_unique_id(serial_number) + if config_entry: + return self._update_entry( + config_entry, + host=discovery_info[CONF_HOST], + port=discovery_info[CONF_PORT], + ) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { @@ -197,43 +161,11 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): "host": discovery_info[CONF_HOST], } - if serialnumber not in config_file: - self.discovery_schema = { - vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int, - } - - return await self.async_step_user() - - try: - device_config = DEVICE_SCHEMA(config_file[serialnumber]) - device_config[CONF_HOST] = discovery_info[CONF_HOST] - - if CONF_NAME not in device_config: - device_config[CONF_NAME] = discovery_info["hostname"] - - except vol.Invalid: - return self.async_abort(reason="bad_config_file") - - return await self.async_step_import(device_config) - - async def async_step_import(self, import_config): - """Import a Axis device as a config entry. - - This flow is triggered by `async_setup` for configured devices. - This flow is also triggered by `async_step_discovery`. - - This will execute for any Axis device that contains a complete - configuration. - """ - self.name = import_config[CONF_NAME] - - self.import_schema = { - vol.Required(CONF_HOST, default=import_config[CONF_HOST]): str, - vol.Required(CONF_USERNAME, default=import_config[CONF_USERNAME]): str, - vol.Required(CONF_PASSWORD, default=import_config[CONF_PASSWORD]): str, - vol.Required(CONF_PORT, default=import_config[CONF_PORT]): int, + self.discovery_schema = { + vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int, } - return await self.async_step_user(user_input=import_config) + + return await self.async_step_user() diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index e42a758f3c4..85ad59268df 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -1,8 +1,8 @@ """Axis network device abstraction.""" import asyncio -import async_timeout +import async_timeout import axis from axis.streammanager import SIGNAL_PLAYING @@ -21,7 +21,6 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import CONF_CAMERA, CONF_EVENTS, CONF_MODEL, DOMAIN, LOGGER - from .errors import AuthenticationRequired, CannotConnect @@ -57,8 +56,8 @@ class AxisNetworkDevice: @property def serial(self): - """Return the mac of this device.""" - return self.config_entry.data[CONF_MAC] + """Return the serial number of this device.""" + return self.config_entry.unique_id async def async_update_device_registry(self): """Update device registry.""" diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 2dc23f3e466..7facd7060ad 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -23,7 +23,8 @@ "already_configured": "Device is already configured", "bad_config_file": "Bad data from config file", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Discovered device not an Axis device" + "not_axis_device": "Discovered device not an Axis device", + "updated_configuration": "Updated device configuration with new host address" } } } diff --git a/homeassistant/components/axis/switch.py b/homeassistant/components/axis/switch.py index a64ffc3fa85..a83460bc529 100644 --- a/homeassistant/components/axis/switch.py +++ b/homeassistant/components/axis/switch.py @@ -3,7 +3,6 @@ from axis.event_stream import CLASS_OUTPUT from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_MAC from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -13,8 +12,7 @@ from .const import DOMAIN as AXIS_DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Axis switch.""" - serial_number = config_entry.data[CONF_MAC] - device = hass.data[AXIS_DOMAIN][serial_number] + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] @callback def async_add_switch(event_id): diff --git a/homeassistant/components/azure_event_hub/__init__.py b/homeassistant/components/azure_event_hub/__init__.py index 371b8d1ea8d..7e141cd8060 100644 --- a/homeassistant/components/azure_event_hub/__init__.py +++ b/homeassistant/components/azure_event_hub/__init__.py @@ -3,8 +3,8 @@ import json import logging from typing import Any, Dict -import voluptuous as vol from azure.eventhub import EventData, EventHubClientAsync +import voluptuous as vol from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, diff --git a/homeassistant/components/azure_event_hub/manifest.json b/homeassistant/components/azure_event_hub/manifest.json index 0b705bddc29..614fb0d98ef 100644 --- a/homeassistant/components/azure_event_hub/manifest.json +++ b/homeassistant/components/azure_event_hub/manifest.json @@ -1,8 +1,8 @@ { - "domain": "azure_event_hub", - "name": "Azure Event Hub", - "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", - "requirements": ["azure-eventhub==1.3.1"], - "dependencies": [], - "codeowners": ["@eavanvalkenburg"] - } \ No newline at end of file + "domain": "azure_event_hub", + "name": "Azure Event Hub", + "documentation": "https://www.home-assistant.io/integrations/azure_event_hub", + "requirements": ["azure-eventhub==1.3.1"], + "dependencies": [], + "codeowners": ["@eavanvalkenburg"] +} diff --git a/homeassistant/components/azure_service_bus/manifest.json b/homeassistant/components/azure_service_bus/manifest.json index fa6d1c20b7f..af1d9d889df 100644 --- a/homeassistant/components/azure_service_bus/manifest.json +++ b/homeassistant/components/azure_service_bus/manifest.json @@ -2,11 +2,7 @@ "domain": "azure_service_bus", "name": "Azure Service Bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus", - "requirements": [ - "azure-servicebus==0.50.1" - ], + "requirements": ["azure-servicebus==0.50.1"], "dependencies": [], - "codeowners": [ - "@hfurubotten" - ] -} \ No newline at end of file + "codeowners": ["@hfurubotten"] +} diff --git a/homeassistant/components/baidu/manifest.json b/homeassistant/components/baidu/manifest.json index 756a1c5adcc..2448f87778b 100644 --- a/homeassistant/components/baidu/manifest.json +++ b/homeassistant/components/baidu/manifest.json @@ -2,9 +2,7 @@ "domain": "baidu", "name": "Baidu", "documentation": "https://www.home-assistant.io/integrations/baidu", - "requirements": [ - "baidu-aip==1.6.6" - ], + "requirements": ["baidu-aip==1.6.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bayesian/manifest.json b/homeassistant/components/bayesian/manifest.json index 7060dbd396b..1b4dc73810f 100644 --- a/homeassistant/components/bayesian/manifest.json +++ b/homeassistant/components/bayesian/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/bayesian", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py index e68633c0688..30a4bacc4da 100644 --- a/homeassistant/components/bbb_gpio/__init__.py +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -19,7 +19,7 @@ def setup(hass, config): GPIO.cleanup() def prepare_gpio(event): - """Stuff to do when home assistant starts.""" + """Stuff to do when Home Assistant starts.""" hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) diff --git a/homeassistant/components/bbb_gpio/binary_sensor.py b/homeassistant/components/bbb_gpio/binary_sensor.py index 105015da720..3ef13c117a2 100644 --- a/homeassistant/components/bbb_gpio/binary_sensor.py +++ b/homeassistant/components/bbb_gpio/binary_sensor.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol from homeassistant.components import bbb_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bbb_gpio/manifest.json b/homeassistant/components/bbb_gpio/manifest.json index edd60326828..42670d510da 100644 --- a/homeassistant/components/bbb_gpio/manifest.json +++ b/homeassistant/components/bbb_gpio/manifest.json @@ -1,10 +1,8 @@ { "domain": "bbb_gpio", - "name": "Bbb gpio", + "name": "BeagleBone Black GPIO", "documentation": "https://www.home-assistant.io/integrations/bbb_gpio", - "requirements": [ - "Adafruit_BBIO==1.0.0" - ], + "requirements": ["Adafruit_BBIO==1.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bbb_gpio/switch.py b/homeassistant/components/bbb_gpio/switch.py index 45f95609758..eb75c6f374c 100644 --- a/homeassistant/components/bbb_gpio/switch.py +++ b/homeassistant/components/bbb_gpio/switch.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components import bbb_gpio -from homeassistant.const import DEVICE_DEFAULT_NAME, CONF_NAME -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 122016ecf96..8097c11eb89 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -5,7 +5,6 @@ import logging from typing import List import pybbox - import voluptuous as vol from homeassistant.components.device_tracker import ( diff --git a/homeassistant/components/bbox/manifest.json b/homeassistant/components/bbox/manifest.json index 15a648167c5..ed7f7270bd5 100644 --- a/homeassistant/components/bbox/manifest.json +++ b/homeassistant/components/bbox/manifest.json @@ -2,9 +2,7 @@ "domain": "bbox", "name": "Bbox", "documentation": "https://www.home-assistant.io/integrations/bbox", - "requirements": [ - "pybbox==0.0.5-alpha" - ], + "requirements": ["pybbox==0.0.5-alpha"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index 7b795a8788e..f5e5865f6f0 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -1,20 +1,19 @@ """Support for Bbox Bouygues Modem Router.""" -import logging from datetime import timedelta +import logging -import requests import pybbox - +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_MONITORED_VARIABLES, ATTR_ATTRIBUTION, + CONF_MONITORED_VARIABLES, + CONF_NAME, DEVICE_CLASS_TIMESTAMP, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.dt import utcnow diff --git a/homeassistant/components/beewi_smartclim/manifest.json b/homeassistant/components/beewi_smartclim/manifest.json index bc69efb6490..69adb76d3cb 100644 --- a/homeassistant/components/beewi_smartclim/manifest.json +++ b/homeassistant/components/beewi_smartclim/manifest.json @@ -2,11 +2,7 @@ "domain": "beewi_smartclim", "name": "BeeWi SmartClim BLE sensor", "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", - "requirements": [ - "beewi_smartclim==0.0.7" - ], + "requirements": ["beewi_smartclim==0.0.7"], "dependencies": [], - "codeowners": [ - "@alemuro" - ] -} \ No newline at end of file + "codeowners": ["@alemuro"] +} diff --git a/homeassistant/components/beewi_smartclim/sensor.py b/homeassistant/components/beewi_smartclim/sensor.py index 7bfa8883013..187ca411988 100644 --- a/homeassistant/components/beewi_smartclim/sensor.py +++ b/homeassistant/components/beewi_smartclim/sensor.py @@ -5,15 +5,15 @@ 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, + CONF_NAME, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -88,7 +88,7 @@ class BeewiSmartclimSensor(Entity): @property def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return f"{self._mac}_{self._device}" @property diff --git a/homeassistant/components/bh1750/manifest.json b/homeassistant/components/bh1750/manifest.json index 63816f967e7..1c9724b7dd8 100644 --- a/homeassistant/components/bh1750/manifest.json +++ b/homeassistant/components/bh1750/manifest.json @@ -1,11 +1,8 @@ { "domain": "bh1750", - "name": "Bh1750", + "name": "BH1750", "documentation": "https://www.home-assistant.io/integrations/bh1750", - "requirements": [ - "i2csense==0.0.4", - "smbus-cffi==0.5.1" - ], + "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index cc91fa48bae..924bfcd5507 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -2,14 +2,13 @@ from functools import partial import logging -import smbus # pylint: disable=import-error from i2csense.bh1750 import BH1750 # pylint: disable=import-error - +import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, DEVICE_CLASS_ILLUMINANCE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json index f7bd834561c..19229c16cb3 100644 --- a/homeassistant/components/binary_sensor/.translations/da.json +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_bat_low": "{entity_name} batteri er lavt", "is_cold": "{entity_name} er kold", "is_connected": "{entity_name} er tilsluttet", "is_gas": "{entity_name} registrerer gas", @@ -17,6 +18,7 @@ "is_no_smoke": "{entity_name} registrerer ikke r\u00f8g", "is_no_sound": "{entity_name} registrerer ikke lyd", "is_no_vibration": "{entity_name} registrerer ikke vibration", + "is_not_bat_low": "{entity_name} batteri er normalt", "is_not_cold": "{entity_name} er ikke kold", "is_not_connected": "{entity_name} er afbrudt", "is_not_hot": "{entity_name} er ikke varm", @@ -25,10 +27,17 @@ "is_not_moving": "{entity_name} bev\u00e6ger sig ikke", "is_not_occupied": "{entity_name} er ikke optaget", "is_not_open": "{entity_name} er lukket", + "is_not_plugged_in": "{entity_name} er ikke tilsluttet str\u00f8m", + "is_not_powered": "{entity_name} er ikke tilsluttet str\u00f8m", "is_not_present": "{entity_name} er ikke til stede", "is_not_unsafe": "{entity_name} er sikker", "is_occupied": "{entity_name} er optaget", + "is_off": "{entity_name} er sl\u00e5et fra", + "is_on": "{entity_name} er sl\u00e5et til", "is_open": "{entity_name} er \u00e5ben", + "is_plugged_in": "{entity_name} er tilsluttet str\u00f8m", + "is_powered": "{entity_name} er tilsluttet str\u00f8m", + "is_present": "{entity_name} er til stede", "is_problem": "{entity_name} registrerer problem", "is_smoke": "{entity_name} registrerer r\u00f8g", "is_sound": "{entity_name} registrerer lyd", @@ -36,9 +45,14 @@ "is_vibration": "{entity_name} registrerer vibration" }, "trigger_type": { + "bat_low": "{entity_name} lavt batteriniveau", "closed": "{entity_name} lukket", "cold": "{entity_name} blev kold", "connected": "{entity_name} tilsluttet", + "gas": "{entity_name} begyndte at registrere gas", + "hot": "{entity_name} blev varm", + "light": "{entity_name} begyndte at registrere lys", + "locked": "{entity_name} l\u00e5st", "moist": "{entity_name} blev fugtig", "moist\u00a7": "{entity_name} blev fugtig", "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", @@ -50,18 +64,31 @@ "no_smoke": "{entity_name} stoppede med at registrere r\u00f8g", "no_sound": "{entity_name} stoppede med at registrere lyd", "no_vibration": "{entity_name} stoppede med at registrere vibration", + "not_bat_low": "{entity_name} batteri normalt", + "not_cold": "{entity_name} blev ikke kold", "not_connected": "{entity_name} afbrudt", "not_hot": "{entity_name} blev ikke varm", "not_locked": "{entity_name} l\u00e5st op", "not_moist": "{entity_name} blev t\u00f8r", + "not_moving": "{entity_name} stoppede med at bev\u00e6ge sig", + "not_occupied": "{entity_name} blev ikke optaget", "not_opened": "{entity_name} lukket", + "not_plugged_in": "{entity_name} ikke tilsluttet str\u00f8m", + "not_powered": "{entity_name} ikke tilsluttet str\u00f8m", "not_present": "{entity_name} ikke til stede", "not_unsafe": "{entity_name} blev sikker", "occupied": "{entity_name} blev optaget", + "opened": "{entity_name} \u00e5bnet", + "plugged_in": "{entity_name} tilsluttet str\u00f8m", + "powered": "{entity_name} tilsluttet str\u00f8m", "present": "{entity_name} til stede", "problem": "{entity_name} begyndte at registrere problem", "smoke": "{entity_name} begyndte at registrere r\u00f8g", - "sound": "{entity_name} begyndte at registrere lyd" + "sound": "{entity_name} begyndte at registrere lyd", + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte", + "unsafe": "{entity_name} blev usikker", + "vibration": "{entity_name} begyndte at registrere vibration" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json index 756a370ca3c..9720fb974f6 100644 --- a/homeassistant/components/binary_sensor/.translations/es.json +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -72,14 +72,14 @@ "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", "not_occupied": "{entity_name} no est\u00e1 ocupado", - "not_opened": "{nombre_de_la_entidad} cerrado", + "not_opened": "{entity_name} cerrado", "not_plugged_in": "{entity_name} desconectado", "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", "occupied": "{entity_name} se convirti\u00f3 en ocupado", "opened": "{entity_name} abierto", - "plugged_in": "{nombre_de_la_entidad} conectado", + "plugged_in": "{entity_name} conectado", "powered": "{entity_name} alimentado", "present": "{entity_name} presente", "problem": "{entity_name} empez\u00f3 a detectar problemas", diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 167708c2cf1..4c1cba2bec5 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -1,94 +1,94 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", - "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", - "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc2b5\ub2c8\ub2e4", - "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", - "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud569\ub2c8\ub2e4", - "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc600\uc2b5\ub2c8\ub2e4", - "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", - "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", - "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4", - "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", - "is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4", - "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4", - "is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4", - "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", - "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", - "is_plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud614\uc2b5\ub2c8\ub2e4", - "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_present": "{entity_name} \uc774(\uac00) \uc788\uc2b5\ub2c8\ub2e4", - "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", - "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" + "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud558\uba74", + "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6b0\uba74", + "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5b4 \uc788\uc73c\uba74", + "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uba74", + "is_hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6b0\uba74", + "is_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uba74", + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", + "is_moist": "{entity_name} \uc774(\uac00) \uc2b5\ud558\uba74", + "is_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uba74", + "is_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uba74", + "is_no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774\uba74", + "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\ub2e4\uba74", + "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc838 \uc788\ub2e4\uba74", + "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\ub2e4\uba74", + "is_not_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74", + "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud558\uba74", + "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc73c\uba74", + "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uc9c0 \uc54a\uc73c\uba74", + "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", + "is_not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud600 \uc788\uc73c\uba74", + "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc73c\uba74", + "is_not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74", + "is_not_unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uba74", + "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uba74", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", + "is_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud600 \uc788\uc73c\uba74", + "is_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uace0 \uc788\uc73c\uba74", + "is_present": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc911\uc774\uba74", + "is_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uba74", + "is_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uba74", + "is_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uba74", + "is_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc73c\uba74", + "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uba74" }, "trigger_type": { - "bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", - "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9d0", - "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub428", - "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud568", - "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9d0", - "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud568", - "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae40", - "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", - "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", - "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud568", - "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784", - "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0 \ubabb\ud568", - "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0 \ubabb\ud568", - "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0 \ubabb\ud568", - "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0 \ubabb\ud568", - "not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc815\uc0c1", - "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc74c", - "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9d0", - "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc74c", - "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub428", - "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9d0", - "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c", - "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc74c", - "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud798", - "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc74c", - "not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc74c", - "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9d0", - "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911", - "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", - "plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud798", - "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub428", - "present": "{entity_name} \uc774(\uac00) \uc788\uc74c", - "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud568", - "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud568", - "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud568", - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9d0", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9d0", - "unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc74c", - "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud568" + "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc9c8 \ub54c", + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", + "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9c8 \ub54c", + "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub420 \ub54c", + "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud560 \ub54c", + "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9c8 \ub54c", + "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud560 \ub54c", + "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c", + "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c", + "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c", + "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud560 \ub54c", + "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc77c \ub54c", + "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc774 \ub420 \ub54c", + "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9c8 \ub54c", + "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c", + "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9c8 \ub54c", + "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc744 \ub54c", + "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", + "not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud790 \ub54c", + "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc744 \ub54c", + "not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc0c1\ud0dc\uac00 \ub420 \ub54c", + "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9c8 \ub54c", + "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774 \ub420 \ub54c", + "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c", + "plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud790 \ub54c", + "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub420 \ub54c", + "present": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub420 \ub54c", + "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud560 \ub54c", + "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud560 \ub54c", + "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud560 \ub54c", + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c", + "unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uc9c0 \uc54a\uc744 \ub54c", + "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud560 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index e5f5dc94ff1..73d5e0be458 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -5,14 +5,13 @@ import logging import voluptuous as vol -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) - +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 0766d82c727..aa9a9d25e72 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,10 +1,11 @@ """Implemenet device conditions for binary sensor.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE +from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, @@ -13,7 +14,6 @@ from homeassistant.helpers.entity_registry import ( from homeassistant.helpers.typing import ConfigType from . import ( - DOMAIN, DEVICE_CLASS_BATTERY, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, @@ -37,6 +37,7 @@ from . import ( DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, + DOMAIN, ) DEVICE_CLASS_NONE = "none" @@ -89,7 +90,7 @@ IS_ON = [ CONF_IS_GAS, CONF_IS_HOT, CONF_IS_LIGHT, - CONF_IS_LOCKED, + CONF_IS_NOT_LOCKED, CONF_IS_MOIST, CONF_IS_MOTION, CONF_IS_MOVING, @@ -111,7 +112,7 @@ IS_OFF = [ CONF_IS_NOT_COLD, CONF_IS_NOT_CONNECTED, CONF_IS_NOT_HOT, - CONF_IS_NOT_LOCKED, + CONF_IS_LOCKED, CONF_IS_NOT_MOIST, CONF_IS_NOT_MOVING, CONF_IS_NOT_OCCUPIED, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index c51b9749288..f4799828c68 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -8,11 +8,10 @@ from homeassistant.components.device_automation.const import ( CONF_TURNED_ON, ) from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE -from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from . import ( - DOMAIN, DEVICE_CLASS_BATTERY, DEVICE_CLASS_COLD, DEVICE_CLASS_CONNECTIVITY, @@ -36,9 +35,9 @@ from . import ( DEVICE_CLASS_SOUND, DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, + DOMAIN, ) - # mypy: allow-untyped-defs, no-check-untyped-defs DEVICE_CLASS_NONE = "none" @@ -92,7 +91,7 @@ TURNED_ON = [ CONF_GAS, CONF_HOT, CONF_LIGHT, - CONF_LOCKED, + CONF_NOT_LOCKED, CONF_MOIST, CONF_MOTION, CONF_MOVING, @@ -114,7 +113,7 @@ TURNED_OFF = [ CONF_NOT_COLD, CONF_NOT_CONNECTED, CONF_NOT_HOT, - CONF_NOT_LOCKED, + CONF_LOCKED, CONF_NOT_MOIST, CONF_NOT_MOVING, CONF_NOT_OCCUPIED, diff --git a/homeassistant/components/binary_sensor/manifest.json b/homeassistant/components/binary_sensor/manifest.json index ea29314eb1b..cbe95684715 100644 --- a/homeassistant/components/binary_sensor/manifest.json +++ b/homeassistant/components/binary_sensor/manifest.json @@ -1,8 +1,9 @@ { "domain": "binary_sensor", - "name": "Binary sensor", + "name": "Binary Sensor", "documentation": "https://www.home-assistant.io/integrations/binary_sensor", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/bitcoin/manifest.json b/homeassistant/components/bitcoin/manifest.json index 1ffc34fcd9d..caf1aafcacf 100644 --- a/homeassistant/components/bitcoin/manifest.json +++ b/homeassistant/components/bitcoin/manifest.json @@ -2,11 +2,7 @@ "domain": "bitcoin", "name": "Bitcoin", "documentation": "https://www.home-assistant.io/integrations/bitcoin", - "requirements": [ - "blockchain==1.4.4" - ], + "requirements": ["blockchain==1.4.4"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index b62bb434e85..bc8394d51a5 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -148,7 +148,7 @@ class BitcoinSensor(Entity): elif self.type == "total_btc": self._state = "{0:.2f}".format(stats.total_btc * 0.00000001) elif self.type == "total_blocks": - self._state = "{0:.2f}".format(stats.total_blocks) + self._state = "{0:.0f}".format(stats.total_blocks) elif self.type == "next_retarget": self._state = "{0:.2f}".format(stats.next_retarget) elif self.type == "estimated_transaction_volume_usd": diff --git a/homeassistant/components/bizkaibus/sensor.py b/homeassistant/components/bizkaibus/sensor.py index c54a61c66b1..931fbbb834d 100644 --- a/homeassistant/components/bizkaibus/sensor.py +++ b/homeassistant/components/bizkaibus/sensor.py @@ -2,15 +2,14 @@ import logging -import voluptuous as vol from bizkaibus.bizkaibus import BizkaibusData -import homeassistant.helpers.config_validation as cv +import voluptuous as vol -from homeassistant.const import CONF_NAME from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity - _LOGGER = logging.getLogger(__name__) ATTR_DUE_IN = "Due in" diff --git a/homeassistant/components/blackbird/manifest.json b/homeassistant/components/blackbird/manifest.json index c5b3a632c16..d68703eee84 100644 --- a/homeassistant/components/blackbird/manifest.json +++ b/homeassistant/components/blackbird/manifest.json @@ -1,10 +1,8 @@ { "domain": "blackbird", - "name": "Blackbird", + "name": "Monoprice Blackbird Matrix Switch", "documentation": "https://www.home-assistant.io/integrations/blackbird", - "requirements": [ - "pyblackbird==0.5" - ], + "requirements": ["pyblackbird==0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index 08efc1e6647..a0ea369bb9b 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -22,6 +22,7 @@ from homeassistant.const import ( STATE_ON, ) import homeassistant.helpers.config_validation as cv + from .const import DOMAIN, SERVICE_SETALLZONES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index 47cded00cc0..4912f72b906 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -2,11 +2,7 @@ "domain": "blink", "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", - "requirements": [ - "blinkpy==0.14.2" - ], + "requirements": ["blinkpy==0.14.2"], "dependencies": [], - "codeowners": [ - "@fronzbot" - ] + "codeowners": ["@fronzbot"] } diff --git a/homeassistant/components/blinksticklight/manifest.json b/homeassistant/components/blinksticklight/manifest.json index 0a6e2540790..75ee7c8ad36 100644 --- a/homeassistant/components/blinksticklight/manifest.json +++ b/homeassistant/components/blinksticklight/manifest.json @@ -1,10 +1,8 @@ { "domain": "blinksticklight", - "name": "Blinksticklight", + "name": "BlinkStick", "documentation": "https://www.home-assistant.io/integrations/blinksticklight", - "requirements": [ - "blinkstick==1.1.8" - ], + "requirements": ["blinkstick==1.1.8"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/blinkt/light.py b/homeassistant/components/blinkt/light.py index e626a73d287..0fedc2b794b 100644 --- a/homeassistant/components/blinkt/light.py +++ b/homeassistant/components/blinkt/light.py @@ -4,16 +4,16 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/blinkt/manifest.json b/homeassistant/components/blinkt/manifest.json index 629bdebf27e..f61b674aa3a 100644 --- a/homeassistant/components/blinkt/manifest.json +++ b/homeassistant/components/blinkt/manifest.json @@ -1,10 +1,8 @@ { "domain": "blinkt", - "name": "Blinkt", + "name": "Blinkt!", "documentation": "https://www.home-assistant.io/integrations/blinkt", - "requirements": [ - "blinkt==0.1.0" - ], + "requirements": ["blinkt==0.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/blockchain/manifest.json b/homeassistant/components/blockchain/manifest.json index 773b52e724b..f57e91a9262 100644 --- a/homeassistant/components/blockchain/manifest.json +++ b/homeassistant/components/blockchain/manifest.json @@ -1,10 +1,8 @@ { "domain": "blockchain", - "name": "Blockchain", + "name": "Blockchain.info", "documentation": "https://www.home-assistant.io/integrations/blockchain", - "requirements": [ - "python-blockchain-api==0.0.2" - ], + "requirements": ["python-blockchain-api==0.0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bloomsky/binary_sensor.py b/homeassistant/components/bloomsky/binary_sensor.py index 99951fcf5c5..cc6562a0bc1 100644 --- a/homeassistant/components/bloomsky/binary_sensor.py +++ b/homeassistant/components/bloomsky/binary_sensor.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/bloomsky/manifest.json b/homeassistant/components/bloomsky/manifest.json index 49da6534bae..fdaa649b344 100644 --- a/homeassistant/components/bloomsky/manifest.json +++ b/homeassistant/components/bloomsky/manifest.json @@ -1,6 +1,6 @@ { "domain": "bloomsky", - "name": "Bloomsky", + "name": "BloomSky", "documentation": "https://www.home-assistant.io/integrations/bloomsky", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/bloomsky/sensor.py b/homeassistant/components/bloomsky/sensor.py index 18f60036397..84871b7b30e 100644 --- a/homeassistant/components/bloomsky/sensor.py +++ b/homeassistant/components/bloomsky/sensor.py @@ -4,9 +4,9 @@ import logging import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_FAHRENHEIT, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from . import BLOOMSKY diff --git a/homeassistant/components/bluesound/manifest.json b/homeassistant/components/bluesound/manifest.json index e64e3e61f16..df6aa5b03de 100644 --- a/homeassistant/components/bluesound/manifest.json +++ b/homeassistant/components/bluesound/manifest.json @@ -2,9 +2,7 @@ "domain": "bluesound", "name": "Bluesound", "documentation": "https://www.home-assistant.io/integrations/bluesound", - "requirements": [ - "xmltodict==0.12.0" - ], + "requirements": ["xmltodict==0.12.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 5a9f3561dc9..04ba21555d4 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -49,6 +49,7 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import Throttle import homeassistant.util.dt as dt_util + from .const import ( DOMAIN, SERVICE_CLEAR_TIMER, diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 18edd750639..4957356d26a 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -4,18 +4,18 @@ import logging import pygatt # pylint: disable=import-error -from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.components.device_tracker.const import ( + CONF_SCAN_INTERVAL, + CONF_TRACK_NEW, + SCAN_INTERVAL, + SOURCE_TYPE_BLUETOOTH_LE, +) from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, async_load_config, ) -from homeassistant.components.device_tracker.const import ( - CONF_TRACK_NEW, - CONF_SCAN_INTERVAL, - SCAN_INTERVAL, - SOURCE_TYPE_BLUETOOTH_LE, -) from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers.event import track_point_in_utc_time import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -44,23 +44,26 @@ def setup_scanner(hass, config, see, discovery_info=None): def see_device(address, name, new_device=False): """Mark a device as seen.""" - if new_device: - if address in new_devices: - _LOGGER.debug("Seen %s %s times", address, new_devices[address]) - new_devices[address] += 1 - if new_devices[address] >= MIN_SEEN_NEW: - _LOGGER.debug("Adding %s to tracked devices", address) - devs_to_track.append(address) - else: - return - else: - _LOGGER.debug("Seen %s for the first time", address) - new_devices[address] = 1 - return - if name is not None: name = name.strip("\x00") + if new_device: + if address in new_devices: + new_devices[address]["seen"] += 1 + if name: + new_devices[address]["name"] = name + else: + name = new_devices[address]["name"] + _LOGGER.debug("Seen %s %s times", address, new_devices[address]["seen"]) + if new_devices[address]["seen"] < MIN_SEEN_NEW: + return + _LOGGER.debug("Adding %s to tracked devices", address) + devs_to_track.append(address) + else: + _LOGGER.debug("Seen %s for the first time", address) + new_devices[address] = {"seen": 1, "name": name} + return + see( mac=BLE_PREFIX + address, host_name=name, @@ -77,7 +80,7 @@ def setup_scanner(hass, config, see, discovery_info=None): devices = {x["address"]: x["name"] for x in devs} _LOGGER.debug("Bluetooth LE devices discovered = %s", devices) - except RuntimeError as error: + except (RuntimeError, pygatt.exceptions.BLEError) as error: _LOGGER.error("Error during Bluetooth LE scan: %s", error) return {} return devices diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index d9f4cb0a2b5..52d2d40a99b 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -1,10 +1,8 @@ { "domain": "bluetooth_le_tracker", - "name": "Bluetooth le tracker", + "name": "Bluetooth LE Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", - "requirements": [ - "pygatt[GATTTOOL]==4.0.5" - ], + "requirements": ["pygatt[GATTTOOL]==4.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bluetooth_tracker/device_tracker.py b/homeassistant/components/bluetooth_tracker/device_tracker.py index 102c8e494aa..d833f60c84f 100644 --- a/homeassistant/components/bluetooth_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_tracker/device_tracker.py @@ -1,7 +1,7 @@ """Tracking for bluetooth devices.""" import asyncio import logging -from typing import List, Set, Tuple, Optional +from typing import List, Optional, Set, Tuple # pylint: disable=import-error import bluetooth diff --git a/homeassistant/components/bluetooth_tracker/manifest.json b/homeassistant/components/bluetooth_tracker/manifest.json index 20fe51c561b..b6ae27346f2 100644 --- a/homeassistant/components/bluetooth_tracker/manifest.json +++ b/homeassistant/components/bluetooth_tracker/manifest.json @@ -1,11 +1,8 @@ { "domain": "bluetooth_tracker", - "name": "Bluetooth tracker", + "name": "Bluetooth Tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_tracker", - "requirements": [ - "bt_proximity==0.2", - "pybluez==0.22" - ], + "requirements": ["bt_proximity==0.2", "pybluez==0.22"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bme280/manifest.json b/homeassistant/components/bme280/manifest.json index 9d01b301d43..393d3f45104 100644 --- a/homeassistant/components/bme280/manifest.json +++ b/homeassistant/components/bme280/manifest.json @@ -1,11 +1,8 @@ { "domain": "bme280", - "name": "Bme280", + "name": "Bosch BME280 Environmental Sensor", "documentation": "https://www.home-assistant.io/integrations/bme280", - "requirements": [ - "i2csense==0.0.4", - "smbus-cffi==0.5.1" - ], + "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index b9bc18e6abf..e1e33210c9b 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -3,14 +3,13 @@ from datetime import timedelta from functools import partial import logging -import smbus # pylint: disable=import-error from i2csense.bme280 import BME280 # pylint: disable=import-error - +import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit diff --git a/homeassistant/components/bme680/manifest.json b/homeassistant/components/bme680/manifest.json index c062d14f8c9..058bbf341e8 100644 --- a/homeassistant/components/bme680/manifest.json +++ b/homeassistant/components/bme680/manifest.json @@ -1,11 +1,8 @@ { "domain": "bme680", - "name": "Bme680", + "name": "Bosch BME680 Environmental Sensor", "documentation": "https://www.home-assistant.io/integrations/bme680", - "requirements": [ - "bme680==1.0.5", - "smbus-cffi==0.5.1" - ], + "requirements": ["bme680==1.0.5", "smbus-cffi==0.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index 5a1e9fd120f..65c87890242 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -3,8 +3,8 @@ import logging import threading from time import sleep, time -from smbus import SMBus # pylint: disable=import-error import bme680 # pylint: disable=import-error +from smbus import SMBus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 455d821e669..6e7723b16ec 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -119,7 +119,7 @@ class BMWConnectedDriveAccount: def __init__( self, username: str, password: str, region_str: str, name: str, read_only ) -> None: - """Constructor.""" + """Initialize account.""" region = get_region_from_name(region_str) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index 8163ae4eae3..591cdadda35 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { "lids": ["Doors", "opening", "mdi:car-door-lock"], "windows": ["Windows", "opening", "mdi:car-door"], - "door_lock_state": ["Door lock state", "safety", "mdi:car-key"], + "door_lock_state": ["Door lock state", "lock", "mdi:car-key"], "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], "condition_based_services": ["Condition based services", "problem", "mdi:wrench"], "check_control_messages": ["Control messages", "problem", "mdi:car-tire-alert"], @@ -59,7 +59,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def __init__( self, account, vehicle, attribute: str, sensor_name, device_class, icon ): - """Constructor.""" + """Initialize sensor.""" self._account = account self._vehicle = vehicle self._attribute = attribute diff --git a/homeassistant/components/bmw_connected_drive/device_tracker.py b/homeassistant/components/bmw_connected_drive/device_tracker.py index c4e835af0eb..fa732b64e77 100644 --- a/homeassistant/components/bmw_connected_drive/device_tracker.py +++ b/homeassistant/components/bmw_connected_drive/device_tracker.py @@ -31,7 +31,7 @@ class BMWDeviceTracker: def update(self) -> None: """Update the device info. - Only update the state in home assistant if tracking in + Only update the state in Home Assistant if tracking in the car is enabled. """ dev_id = slugify(self.vehicle.name) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index a88675ef80f..c6e6ab9d89a 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -2,11 +2,7 @@ "domain": "bmw_connected_drive", "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", - "requirements": [ - "bimmer_connected==0.6.2" - ], + "requirements": ["bimmer_connected==0.6.2"], "dependencies": [], - "codeowners": [ - "@gerard33" - ] -} \ No newline at end of file + "codeowners": ["@gerard33"] +} diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index f919bba6b95..3c40900bed8 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -69,7 +69,7 @@ class BMWConnectedDriveSensor(Entity): """Representation of a BMW vehicle sensor.""" def __init__(self, account, vehicle, attribute: str, attribute_info): - """Constructor.""" + """Initialize BMW vehicle sensor.""" self._vehicle = vehicle self._account = account self._attribute = attribute diff --git a/homeassistant/components/bom/manifest.json b/homeassistant/components/bom/manifest.json index b2e7eb08ef6..211d1dafc7b 100644 --- a/homeassistant/components/bom/manifest.json +++ b/homeassistant/components/bom/manifest.json @@ -1,10 +1,8 @@ { "domain": "bom", - "name": "Bom", + "name": "Australian Bureau of Meteorology (BOM)", "documentation": "https://www.home-assistant.io/integrations/bom", - "requirements": [ - "bomradarloop==0.1.3" - ], + "requirements": ["bomradarloop==0.1.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bom/sensor.py b/homeassistant/components/bom/sensor.py index ed22be003ad..7d951968cb2 100644 --- a/homeassistant/components/bom/sensor.py +++ b/homeassistant/components/bom/sensor.py @@ -12,19 +12,19 @@ import zipfile 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_MONITORED_CONDITIONS, - TEMP_CELSIUS, - CONF_NAME, ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _RESOURCE = "http://www.bom.gov.au/fwo/{}/{}.{}.json" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index e49e45865c4..1cb3efdd2cc 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -1,15 +1,8 @@ { "domain": "braviatv", - "name": "Braviatv", + "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": [ - "braviarc-homeassistant==0.3.7.dev0", - "getmac==0.8.1" - ], - "dependencies": [ - "configurator" - ], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["braviarc-homeassistant==0.3.7.dev0", "getmac==0.8.1"], + "dependencies": ["configurator"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index d0458541f7b..ef0640c8e87 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -291,7 +291,7 @@ class BraviaTVDevice(MediaPlayerDevice): if self._channel_name is not None: return_value = self._channel_name if self._program_name is not None: - return_value = return_value + ": " + self._program_name + return_value = f"{return_value}: {self._program_name}" return return_value @property diff --git a/homeassistant/components/broadlink/__init__.py b/homeassistant/components/broadlink/__init__.py index 521cd68780c..3f9b5cd4597 100644 --- a/homeassistant/components/broadlink/__init__.py +++ b/homeassistant/components/broadlink/__init__.py @@ -2,11 +2,11 @@ import asyncio from base64 import b64decode, b64encode from binascii import unhexlify +from datetime import timedelta import logging import re import socket -from datetime import timedelta import voluptuous as vol from homeassistant.const import CONF_HOST diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 38adb0ddbdf..3af11f47aad 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,12 +2,7 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": [ - "broadlink==0.12.0" - ], + "requirements": ["broadlink==0.12.0"], "dependencies": [], - "codeowners": [ - "@danielhiversen", - "@felipediel" - ] + "codeowners": ["@danielhiversen", "@felipediel"] } diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 6374f35c503..9f3087335c8 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -1,23 +1,22 @@ """Support for the Broadlink RM2 Pro (only temperature) and A1 devices.""" import binascii -import logging from datetime import timedelta +import logging import broadlink - import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, - TEMP_CELSIUS, - CONF_TIMEOUT, CONF_SCAN_INTERVAL, + CONF_TIMEOUT, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index bfb6dc4f42e..78738870aaa 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -5,7 +5,6 @@ import logging import socket import broadlink - import voluptuous as vol from homeassistant.components.switch import ( @@ -25,8 +24,8 @@ from homeassistant.const import ( STATE_ON, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle, slugify from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.util import Throttle, slugify from . import async_setup_service, data_packet diff --git a/homeassistant/components/brother/.translations/ca.json b/homeassistant/components/brother/.translations/ca.json new file mode 100644 index 00000000000..62dd1807676 --- /dev/null +++ b/homeassistant/components/brother/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unsupported_model": "Aquest model d'impressora no \u00e9s compatible." + }, + "error": { + "connection_error": "Error de connexi\u00f3.", + "snmp_error": "El servidor SNMP s'ha tancat o la impressora no \u00e9s compatible.", + "wrong_host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP inv\u00e0lids." + }, + "step": { + "user": { + "data": { + "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP de la impressora", + "type": "Tipus d'impressora" + }, + "description": "Configura la integraci\u00f3 d'impressora Brother. Si tens problemes amb la configuraci\u00f3, visita: https://www.home-assistant.io/integrations/brother", + "title": "Impressora Brother" + } + }, + "title": "Impressora Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/da.json b/homeassistant/components/brother/.translations/da.json new file mode 100644 index 00000000000..2ec79228194 --- /dev/null +++ b/homeassistant/components/brother/.translations/da.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Denne printer er allerede konfigureret.", + "unsupported_model": "Denne printermodel underst\u00f8ttes ikke." + }, + "error": { + "connection_error": "Forbindelsesfejl.", + "snmp_error": "SNMP-server er sl\u00e5et fra, eller printeren underst\u00f8ttes ikke.", + "wrong_host": "Ugyldigt v\u00e6rtsnavn eller IP-adresse." + }, + "step": { + "user": { + "data": { + "host": "Printerens v\u00e6rtsnavn eller IP-adresse", + "type": "Type af printer" + }, + "description": "Konfigurer Brother-printerintegration. Hvis du har problemer med konfiguration, kan du g\u00e5 til: https://www.home-assistant.io/integrations/brother", + "title": "Brother-printer" + } + }, + "title": "Brother-printer" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/en.json b/homeassistant/components/brother/.translations/en.json new file mode 100644 index 00000000000..d586bcea1f8 --- /dev/null +++ b/homeassistant/components/brother/.translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "This printer is already configured.", + "unsupported_model": "This printer model is not supported." + }, + "error": { + "connection_error": "Connection error.", + "snmp_error": "SNMP server turned off or printer not supported.", + "wrong_host": "Invalid hostname or IP address." + }, + "step": { + "user": { + "data": { + "host": "Printer hostname or IP address", + "type": "Type of the printer" + }, + "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother", + "title": "Brother Printer" + } + }, + "title": "Brother Printer" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/es.json b/homeassistant/components/brother/.translations/es.json new file mode 100644 index 00000000000..f4e53e20793 --- /dev/null +++ b/homeassistant/components/brother/.translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Esta impresora ya est\u00e1 configurada.", + "unsupported_model": "Este modelo de impresora no es compatible." + }, + "error": { + "connection_error": "Error de conexi\u00f3n.", + "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible.", + "wrong_host": "Nombre del host o direcci\u00f3n IP no v\u00e1lidos." + }, + "step": { + "user": { + "data": { + "host": "Nombre del host o direcci\u00f3n IP de la impresora", + "type": "Tipo de impresora" + }, + "description": "Configure la integraci\u00f3n de impresoras Brother. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/brother", + "title": "Impresora Brother" + } + }, + "title": "Impresora Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/it.json b/homeassistant/components/brother/.translations/it.json new file mode 100644 index 00000000000..43bdb7aec7b --- /dev/null +++ b/homeassistant/components/brother/.translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Questa stampante \u00e8 gi\u00e0 configurata.", + "unsupported_model": "Questo modello di stampante non \u00e8 supportato." + }, + "error": { + "connection_error": "Errore di connessione.", + "snmp_error": "Server SNMP spento o stampante non supportata.", + "wrong_host": "Nome host o indirizzo IP non valido." + }, + "step": { + "user": { + "data": { + "host": "Nome host o indirizzo IP della stampante", + "type": "Tipo di stampante" + }, + "description": "Configurare l'integrazione della stampante Brother. In caso di problemi con la configurazione, visitare: https://www.home-assistant.io/integrations/brother", + "title": "Stampante Brother" + } + }, + "title": "Stampante Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/ko.json b/homeassistant/components/brother/.translations/ko.json new file mode 100644 index 00000000000..4d2e213cbee --- /dev/null +++ b/homeassistant/components/brother/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unsupported_model": "\uc774 \ud504\ub9b0\ud130 \ubaa8\ub378\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "\uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "snmp_error": "SNMP \uc11c\ubc84\uac00 \uaebc\uc838 \uc788\uac70\ub098 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \ud504\ub9b0\ud130\uc785\ub2c8\ub2e4.", + "wrong_host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "host": "\ud504\ub9b0\ud130 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c", + "type": "\ud504\ub9b0\ud130\uc758 \uc885\ub958" + }, + "description": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ubb38\uc81c\uac00\uc788\ub294 \uacbd\uc6b0 https://www.home-assistant.io/integrations/brother \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "title": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130" + } + }, + "title": "\ube0c\ub77c\ub354 \ud504\ub9b0\ud130" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/lb.json b/homeassistant/components/brother/.translations/lb.json new file mode 100644 index 00000000000..dd051b1bb0c --- /dev/null +++ b/homeassistant/components/brother/.translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse Printer ass scho konfigur\u00e9iert.", + "unsupported_model": "D\u00ebse Printer Modell g\u00ebtt net \u00ebnnerst\u00ebtzt." + }, + "error": { + "connection_error": "Feeler bei der Verbindung.", + "snmp_error": "SNMP Server ausgeschalt oder Printer net \u00ebnnerst\u00ebtzt.", + "wrong_host": "Ong\u00ebltege Numm oder IP Adresse" + }, + "step": { + "user": { + "data": { + "host": "Printer Numm oder IP Adresse", + "type": "Typ vum Printer" + }, + "description": "Brother Printer Integratioun ariichten. Am Fall vun Problemer kuckt op: https://www.home-assistant.io/integrations/brother", + "title": "Brother Printer" + } + }, + "title": "Brother Printer" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/no.json b/homeassistant/components/brother/.translations/no.json new file mode 100644 index 00000000000..d4cf935f156 --- /dev/null +++ b/homeassistant/components/brother/.translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Denne skriveren er allerede konfigurert.", + "unsupported_model": "Denne skrivermodellen er ikke st\u00f8ttet." + }, + "error": { + "connection_error": "Tilkoblingen mislyktes.", + "snmp_error": "SNMP verten er skrudd av eller printeren er ikke st\u00f8ttet.", + "wrong_host": "Ugyldig vertsnavn eller IP-adresse." + }, + "step": { + "user": { + "data": { + "host": "Vertsnavn eller IP-adresse til skriveren", + "type": "Skriver type" + }, + "description": "Konfigurer Brother skriver integrasjonen. Hvis du har problemer med konfigurasjonen, bes\u00f8k dokumentasjonen her: https://www.home-assistant.io/integrations/brother", + "title": "Brother skriver" + } + }, + "title": "Brother skriver" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/pl.json b/homeassistant/components/brother/.translations/pl.json new file mode 100644 index 00000000000..14fe4024f34 --- /dev/null +++ b/homeassistant/components/brother/.translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ta drukarka jest ju\u017c skonfigurowana.", + "unsupported_model": "Ten model drukarki nie jest obs\u0142ugiwany." + }, + "error": { + "connection_error": "B\u0142\u0105d po\u0142\u0105czenia.", + "snmp_error": "Serwer SNMP wy\u0142\u0105czony lub drukarka nie jest obs\u0142ugiwana.", + "wrong_host": "Niepoprawna nazwa hosta lub adres IP drukarki." + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP drukarki", + "type": "Typ drukarki" + }, + "description": "Konfiguracja integracji drukarek Brother. Je\u015bli masz problemy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/brother", + "title": "Drukarka Brother" + } + }, + "title": "Drukarka Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/pt-BR.json b/homeassistant/components/brother/.translations/pt-BR.json new file mode 100644 index 00000000000..454cde320d7 --- /dev/null +++ b/homeassistant/components/brother/.translations/pt-BR.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unsupported_model": "Este modelo de impressora n\u00e3o \u00e9 suportado." + }, + "error": { + "connection_error": "Erro de conex\u00e3o.", + "snmp_error": "Servidor SNMP desligado ou impressora n\u00e3o suportada.", + "wrong_host": "Nome de host ou endere\u00e7o IP inv\u00e1lido." + }, + "step": { + "user": { + "data": { + "host": "Nome do host ou endere\u00e7o IP da impressora", + "type": "Tipo de impressora" + }, + "description": "Configure a integra\u00e7\u00e3o da impressora Brother. Se voc\u00ea tiver problemas com a configura\u00e7\u00e3o, acesse: https://www.home-assistant.io/integrations/brother", + "title": "Impressora Brother" + } + }, + "title": "Impressora Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/ru.json b/homeassistant/components/brother/.translations/ru.json new file mode 100644 index 00000000000..eb12f2f1225 --- /dev/null +++ b/homeassistant/components/brother/.translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "unsupported_model": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "error": { + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d \u0438\u043b\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "wrong_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." + }, + "step": { + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/brother.", + "title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother" + } + }, + "title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/sl.json b/homeassistant/components/brother/.translations/sl.json new file mode 100644 index 00000000000..99caf69a86f --- /dev/null +++ b/homeassistant/components/brother/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "unsupported_model": "Ta model tiskalnika ni podprt." + }, + "error": { + "connection_error": "Napaka v povezavi.", + "snmp_error": "Stre\u017enik SNMP je izklopljen ali tiskalnik ni podprt.", + "wrong_host": "Neveljavno ime gostitelja ali IP naslov." + }, + "step": { + "user": { + "data": { + "host": "Gostiteljsko ime tiskalnika ali naslov IP", + "type": "Vrsta tiskalnika" + }, + "description": "Nastavite integracijo tiskalnika Brother. \u010ce imate te\u017eave s konfiguracijo, pojdite na: https://www.home-assistant.io/integrations/brother", + "title": "Brother Tiskalnik" + } + }, + "title": "Brother Tiskalnik" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/.translations/zh-Hant.json b/homeassistant/components/brother/.translations/zh-Hant.json new file mode 100644 index 00000000000..cff89ea38ca --- /dev/null +++ b/homeassistant/components/brother/.translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u5370\u8868\u6a5f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "unsupported_model": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u5370\u8868\u6a5f\u3002" + }, + "error": { + "connection_error": "\u9023\u7dda\u932f\u8aa4\u3002", + "snmp_error": "SNMP \u4f3a\u670d\u5668\u70ba\u95dc\u9589\u72c0\u614b\u6216\u5370\u8868\u6a5f\u4e0d\u652f\u63f4\u3002", + "wrong_host": "\u7121\u6548\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740" + }, + "step": { + "user": { + "data": { + "host": "\u5370\u8868\u6a5f\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740", + "type": "\u5370\u8868\u6a5f\u985e\u578b" + }, + "description": "\u8a2d\u5b9a Brother \u5370\u8868\u6a5f\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/brother", + "title": "Brother \u5370\u8868\u6a5f" + } + }, + "title": "Brother \u5370\u8868\u6a5f" + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py new file mode 100644 index 00000000000..ada740c5f10 --- /dev/null +++ b/homeassistant/components/brother/__init__.py @@ -0,0 +1,102 @@ +"""The Brother component.""" +import asyncio +from datetime import timedelta +import logging + +from brother import Brother, SnmpError, UnsupportedModel + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_TYPE +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.util import Throttle + +from .const import DOMAIN + +PLATFORMS = ["sensor"] + +DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: Config): + """Set up the Brother component.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Brother from a config entry.""" + host = entry.data[CONF_HOST] + kind = entry.data[CONF_TYPE] + + brother = BrotherPrinterData(host, kind) + + await brother.async_update() + + if not brother.available: + raise ConfigEntryNotReady() + + hass.data[DOMAIN][entry.entry_id] = brother + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class BrotherPrinterData: + """Define an object to hold sensor data.""" + + def __init__(self, host, kind): + """Initialize.""" + self._brother = Brother(host, kind=kind) + self.host = host + self.model = None + self.serial = None + self.firmware = None + self.available = False + self.data = {} + self.unavailable_logged = False + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update data via library.""" + try: + await self._brother.async_update() + except (ConnectionError, SnmpError, UnsupportedModel) as error: + if not self.unavailable_logged: + _LOGGER.error( + "Could not fetch data from %s, error: %s", self.host, error + ) + self.unavailable_logged = True + self.available = self._brother.available + return + + self.model = self._brother.model + self.serial = self._brother.serial + self.firmware = self._brother.firmware + self.available = self._brother.available + self.data = self._brother.data + if self.available and self.unavailable_logged: + _LOGGER.info("Printer %s is available again", self.host) + self.unavailable_logged = False diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py new file mode 100644 index 00000000000..b95469977a7 --- /dev/null +++ b/homeassistant/components/brother/config_flow.py @@ -0,0 +1,69 @@ +"""Adds config flow for Brother Printer.""" +import ipaddress +import re + +from brother import Brother, SnmpError, UnsupportedModel +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_HOST, CONF_TYPE + +from .const import DOMAIN, PRINTER_TYPES # pylint:disable=unused-import + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=""): str, + vol.Optional(CONF_TYPE, default="laser"): vol.In(PRINTER_TYPES), + } +) + + +def host_valid(host): + """Return True if hostname or IP address is valid.""" + try: + if ipaddress.ip_address(host).version == (4 or 6): + return True + except ValueError: + disallowed = re.compile(r"[^a-zA-Z\d\-]") + return all(x and not disallowed.search(x) for x in host.split(".")) + + +class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Brother Printer.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + try: + if not host_valid(user_input[CONF_HOST]): + raise InvalidHost() + + brother = Brother(user_input[CONF_HOST]) + await brother.async_update() + + await self.async_set_unique_id(brother.serial.lower()) + self._abort_if_unique_id_configured() + + title = f"{brother.model} {brother.serial}" + return self.async_create_entry(title=title, data=user_input) + except InvalidHost: + errors[CONF_HOST] = "wrong_host" + except ConnectionError: + errors["base"] = "connection_error" + except SnmpError: + errors["base"] = "snmp_error" + except UnsupportedModel: + return self.async_abort(reason="unsupported_model") + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class InvalidHost(exceptions.HomeAssistantError): + """Error to indicate that hostname/IP address is invalid.""" diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py new file mode 100644 index 00000000000..fdb7cd82b9c --- /dev/null +++ b/homeassistant/components/brother/const.py @@ -0,0 +1,132 @@ +"""Constants for Brother integration.""" +ATTR_BELT_UNIT_REMAINING_LIFE = "belt_unit_remaining_life" +ATTR_BLACK_INK_REMAINING = "black_ink_remaining" +ATTR_BLACK_TONER_REMAINING = "black_toner_remaining" +ATTR_BW_COUNTER = "b/w_counter" +ATTR_COLOR_COUNTER = "color_counter" +ATTR_CYAN_INK_REMAINING = "cyan_ink_remaining" +ATTR_CYAN_TONER_REMAINING = "cyan_toner_remaining" +ATTR_DRUM_COUNTER = "drum_counter" +ATTR_DRUM_REMAINING_LIFE = "drum_remaining_life" +ATTR_DRUM_REMAINING_PAGES = "drum_remaining_pages" +ATTR_FUSER_REMAINING_LIFE = "fuser_remaining_life" +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_LASER_REMAINING_LIFE = "laser_remaining_life" +ATTR_MAGENTA_INK_REMAINING = "magenta_ink_remaining" +ATTR_MAGENTA_TONER_REMAINING = "magenta_toner_remaining" +ATTR_MANUFACTURER = "Brother" +ATTR_PAGE_COUNTER = "page_counter" +ATTR_PF_KIT_1_REMAINING_LIFE = "pf_kit_1_remaining_life" +ATTR_PF_KIT_MP_REMAINING_LIFE = "pf_kit_mp_remaining_life" +ATTR_STATUS = "status" +ATTR_UNIT = "unit" +ATTR_UPTIME = "uptime" +ATTR_YELLOW_INK_REMAINING = "yellow_ink_remaining" +ATTR_YELLOW_TONER_REMAINING = "yellow_toner_remaining" + +DOMAIN = "brother" + +UNIT_PAGES = "p" +UNIT_DAYS = "days" +UNIT_PERCENT = "%" + +PRINTER_TYPES = ["laser", "ink"] + +SENSOR_TYPES = { + ATTR_STATUS: { + ATTR_ICON: "icon:mdi:printer", + ATTR_LABEL: ATTR_STATUS.title(), + ATTR_UNIT: None, + }, + ATTR_PAGE_COUNTER: { + ATTR_ICON: "mdi:file-document-outline", + ATTR_LABEL: ATTR_PAGE_COUNTER.replace("_", " ").title(), + ATTR_UNIT: UNIT_PAGES, + }, + ATTR_BW_COUNTER: { + ATTR_ICON: "mdi:file-document-outline", + ATTR_LABEL: ATTR_BW_COUNTER.replace("_", " ").title(), + ATTR_UNIT: UNIT_PAGES, + }, + ATTR_COLOR_COUNTER: { + ATTR_ICON: "mdi:file-document-outline", + ATTR_LABEL: ATTR_COLOR_COUNTER.replace("_", " ").title(), + ATTR_UNIT: UNIT_PAGES, + }, + ATTR_DRUM_REMAINING_LIFE: { + ATTR_ICON: "mdi:chart-donut", + ATTR_LABEL: ATTR_DRUM_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_BELT_UNIT_REMAINING_LIFE: { + ATTR_ICON: "mdi:current-ac", + ATTR_LABEL: ATTR_BELT_UNIT_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_FUSER_REMAINING_LIFE: { + ATTR_ICON: "mdi:water-outline", + ATTR_LABEL: ATTR_FUSER_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_LASER_REMAINING_LIFE: { + ATTR_ICON: "mdi:spotlight-beam", + ATTR_LABEL: ATTR_LASER_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_PF_KIT_1_REMAINING_LIFE: { + ATTR_ICON: "mdi:printer-3d", + ATTR_LABEL: ATTR_PF_KIT_1_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_PF_KIT_MP_REMAINING_LIFE: { + ATTR_ICON: "mdi:printer-3d", + ATTR_LABEL: ATTR_PF_KIT_MP_REMAINING_LIFE.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_BLACK_TONER_REMAINING: { + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_BLACK_TONER_REMAINING.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_CYAN_TONER_REMAINING: { + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_CYAN_TONER_REMAINING.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_MAGENTA_TONER_REMAINING: { + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_MAGENTA_TONER_REMAINING.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_YELLOW_TONER_REMAINING: { + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_YELLOW_TONER_REMAINING.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_BLACK_INK_REMAINING: { + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_BLACK_INK_REMAINING.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_CYAN_INK_REMAINING: { + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_CYAN_INK_REMAINING.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_MAGENTA_INK_REMAINING: { + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_MAGENTA_INK_REMAINING.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_YELLOW_INK_REMAINING: { + ATTR_ICON: "mdi:printer-3d-nozzle", + ATTR_LABEL: ATTR_YELLOW_INK_REMAINING.replace("_", " ").title(), + ATTR_UNIT: UNIT_PERCENT, + }, + ATTR_UPTIME: { + ATTR_ICON: "mdi:timer", + ATTR_LABEL: ATTR_UPTIME.title(), + ATTR_UNIT: UNIT_DAYS, + }, +} diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json new file mode 100644 index 00000000000..d080ee4fd6c --- /dev/null +++ b/homeassistant/components/brother/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "brother", + "name": "Brother Printer", + "documentation": "https://www.home-assistant.io/integrations/brother", + "dependencies": [], + "codeowners": ["@bieniu"], + "requirements": ["brother==0.1.4"], + "config_flow": true +} diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py new file mode 100644 index 00000000000..9ad075f81cd --- /dev/null +++ b/homeassistant/components/brother/sensor.py @@ -0,0 +1,104 @@ +"""Support for the Brother service.""" +import logging + +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_DRUM_COUNTER, + ATTR_DRUM_REMAINING_LIFE, + ATTR_DRUM_REMAINING_PAGES, + ATTR_ICON, + ATTR_LABEL, + ATTR_MANUFACTURER, + ATTR_UNIT, + DOMAIN, + SENSOR_TYPES, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add Brother entities from a config_entry.""" + brother = hass.data[DOMAIN][config_entry.entry_id] + + sensors = [] + + name = brother.model + device_info = { + "identifiers": {(DOMAIN, brother.serial)}, + "name": brother.model, + "manufacturer": ATTR_MANUFACTURER, + "model": brother.model, + "sw_version": brother.firmware, + } + + for sensor in SENSOR_TYPES: + if sensor in brother.data: + sensors.append(BrotherPrinterSensor(brother, name, sensor, device_info)) + async_add_entities(sensors, True) + + +class BrotherPrinterSensor(Entity): + """Define an Brother Printer sensor.""" + + def __init__(self, printer, name, kind, device_info): + """Initialize.""" + self.printer = printer + self._name = name + self._device_info = device_info + self._unique_id = f"{self.printer.serial.lower()}_{kind}" + self.kind = kind + self._state = None + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + + @property + def state(self): + """Return the state.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.kind == ATTR_DRUM_REMAINING_LIFE: + self._attrs["remaining_pages"] = self.printer.data.get( + ATTR_DRUM_REMAINING_PAGES + ) + self._attrs["counter"] = self.printer.data.get(ATTR_DRUM_COUNTER) + return self._attrs + + @property + def icon(self): + """Return the icon.""" + return SENSOR_TYPES[self.kind][ATTR_ICON] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self.kind][ATTR_UNIT] + + @property + def available(self): + """Return True if entity is available.""" + return self.printer.available + + @property + def device_info(self): + """Return the device info.""" + return self._device_info + + async def async_update(self): + """Update the data from printer.""" + await self.printer.async_update() + + self._state = self.printer.data.get(self.kind) diff --git a/homeassistant/components/brother/strings.json b/homeassistant/components/brother/strings.json new file mode 100644 index 00000000000..b636b7c0202 --- /dev/null +++ b/homeassistant/components/brother/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "title": "Brother Printer", + "step": { + "user": { + "title": "Brother Printer", + "description": "Set up Brother printer integration. If you have problems with configuration go to: https://www.home-assistant.io/integrations/brother", + "data": { + "host": "Printer hostname or IP address", + "type": "Type of the printer" + } + } + }, + "error": { + "wrong_host": "Invalid hostname or IP address.", + "connection_error": "Connection error.", + "snmp_error": "SNMP server turned off or printer not supported." + }, + "abort": { + "unsupported_model": "This printer model is not supported.", + "already_configured": "This printer is already configured." + } + } +} diff --git a/homeassistant/components/brottsplatskartan/manifest.json b/homeassistant/components/brottsplatskartan/manifest.json index f3dd46c96fc..dfc1a385c6b 100644 --- a/homeassistant/components/brottsplatskartan/manifest.json +++ b/homeassistant/components/brottsplatskartan/manifest.json @@ -2,9 +2,7 @@ "domain": "brottsplatskartan", "name": "Brottsplatskartan", "documentation": "https://www.home-assistant.io/integrations/brottsplatskartan", - "requirements": [ - "brottsplatskartan==0.0.1" - ], + "requirements": ["brottsplatskartan==0.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index d8592f44fff..282433aa7a4 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -5,7 +5,6 @@ import logging import uuid import brottsplatskartan - import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index b7612def701..fc0e9eccb3a 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -1,5 +1,6 @@ """Support for launching a web browser on the host machine.""" import webbrowser + import voluptuous as vol ATTR_URL = "url" diff --git a/homeassistant/components/browser/manifest.json b/homeassistant/components/browser/manifest.json index 2905bfcfe96..bb6c5e783fd 100644 --- a/homeassistant/components/browser/manifest.json +++ b/homeassistant/components/browser/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/browser", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/brunt/manifest.json b/homeassistant/components/brunt/manifest.json index 6ee4344b946..4af42fb28de 100644 --- a/homeassistant/components/brunt/manifest.json +++ b/homeassistant/components/brunt/manifest.json @@ -1,12 +1,8 @@ { "domain": "brunt", - "name": "Brunt", + "name": "Brunt Blind Engine", "documentation": "https://www.home-assistant.io/integrations/brunt", - "requirements": [ - "brunt==0.1.3" - ], + "requirements": ["brunt==0.1.3"], "dependencies": [], - "codeowners": [ - "@eavanvalkenburg" - ] + "codeowners": ["@eavanvalkenburg"] } diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 20ad909c44e..32b8e2aa050 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -2,16 +2,15 @@ import logging import bthomehub5_devicelist - import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bt_home_hub_5/manifest.json b/homeassistant/components/bt_home_hub_5/manifest.json index bee9cefce17..fde6dc6e546 100644 --- a/homeassistant/components/bt_home_hub_5/manifest.json +++ b/homeassistant/components/bt_home_hub_5/manifest.json @@ -1,10 +1,8 @@ { "domain": "bt_home_hub_5", - "name": "Bt home hub 5", + "name": "BT Home Hub 5", "documentation": "https://www.home-assistant.io/integrations/bt_home_hub_5", - "requirements": [ - "bthomehub5-devicelist==0.1.1" - ], + "requirements": ["bthomehub5-devicelist==0.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/bt_smarthub/manifest.json b/homeassistant/components/bt_smarthub/manifest.json index 985f3012422..0c474584f36 100644 --- a/homeassistant/components/bt_smarthub/manifest.json +++ b/homeassistant/components/bt_smarthub/manifest.json @@ -1,12 +1,8 @@ { "domain": "bt_smarthub", - "name": "Bt smarthub", + "name": "BT Smart Hub", "documentation": "https://www.home-assistant.io/integrations/bt_smarthub", - "requirements": [ - "btsmarthub_devicelist==0.1.3" - ], + "requirements": ["btsmarthub_devicelist==0.1.3"], "dependencies": [], - "codeowners": [ - "@jxwolstenholme" - ] + "codeowners": ["@jxwolstenholme"] } diff --git a/homeassistant/components/buienradar/camera.py b/homeassistant/components/buienradar/camera.py index cdf202bbafd..6928879d405 100644 --- a/homeassistant/components/buienradar/camera.py +++ b/homeassistant/components/buienradar/camera.py @@ -1,7 +1,7 @@ """Provide animated GIF loops of Buienradar imagery.""" import asyncio -import logging from datetime import datetime, timedelta +import logging from typing import Optional import aiohttp @@ -9,17 +9,14 @@ import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_NAME - from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession - from homeassistant.util import dt as dt_util - CONF_DIMENSION = "dimension" CONF_DELTA = "delta" -RADAR_MAP_URL_TEMPLATE = "https://api.buienradar.nl/image/1.0/" "RadarMapNL?w={w}&h={h}" +RADAR_MAP_URL_TEMPLATE = "https://api.buienradar.nl/image/1.0/RadarMapNL?w={w}&h={h}" _LOG = logging.getLogger(__name__) @@ -137,7 +134,7 @@ class BuienradarCam(Camera): Uses ayncio conditions to make sure only one task enters the critical section at the same time. Otherwise, two http requests would start - when two tabs with home assistant are open. + when two tabs with Home Assistant are open. The condition is entered in two sections because otherwise the lock would be held while doing the http request. diff --git a/homeassistant/components/buienradar/manifest.json b/homeassistant/components/buienradar/manifest.json index cc3c3b02981..23e282c34bb 100644 --- a/homeassistant/components/buienradar/manifest.json +++ b/homeassistant/components/buienradar/manifest.json @@ -2,11 +2,7 @@ "domain": "buienradar", "name": "Buienradar", "documentation": "https://www.home-assistant.io/integrations/buienradar", - "requirements": [ - "buienradar==1.0.1" - ], + "requirements": ["buienradar==1.0.1"], "dependencies": [], - "codeowners": [ - "@mjj4791", "@ties" - ] + "codeowners": ["@mjj4791", "@ties"] } diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 5fe97b6fb38..f642fc2e249 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -33,11 +33,9 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util - from .const import DEFAULT_TIMEFRAME from .util import BrData - _LOGGER = logging.getLogger(__name__) MEASURED_LABEL = "Measured" @@ -205,7 +203,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= timeframe = config.get(CONF_TIMEFRAME, DEFAULT_TIMEFRAME) if None in (latitude, longitude): - _LOGGER.error("Latitude or longitude not set in HomeAssistant config") + _LOGGER.error("Latitude or longitude not set in Home Assistant config") return False coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 579b3418271..37c518cef7a 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -5,7 +5,6 @@ import logging import aiohttp import async_timeout - from buienradar.buienradar import parse_data from buienradar.constants import ( ATTRIBUTION, @@ -31,9 +30,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util - -from .const import SCHEDULE_OK, SCHEDULE_NOK - +from .const import SCHEDULE_NOK, SCHEDULE_OK _LOGGER = logging.getLogger(__name__) @@ -118,7 +115,7 @@ class BrData: if raincontent.get(SUCCESS) is not True: # unable to get the data _LOGGER.warning( - "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", + "Unable to retrieve raindata from Buienradar. (Msg: %s, status: %s)", raincontent.get(MESSAGE), raincontent.get(STATUS_CODE), ) @@ -139,7 +136,7 @@ class BrData: if result.get(SUCCESS) is not True: if int(datetime.now().strftime("%H")) > 0: _LOGGER.warning( - "Unable to parse data from Buienradar." "(Msg: %s)", + "Unable to parse data from Buienradar. (Msg: %s)", result.get(MESSAGE), ) await self.schedule_update(SCHEDULE_NOK) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index c95e57807c4..98cbb2f5e43 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -28,8 +28,8 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_C from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from .util import BrData from .const import DEFAULT_TIMEFRAME +from .util import BrData _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 896ace7ba6a..2f48fb5fc27 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -1,10 +1,8 @@ { "domain": "caldav", - "name": "Caldav", + "name": "CalDav", "documentation": "https://www.home-assistant.io/integrations/caldav", - "requirements": [ - "caldav==0.6.1" - ], + "requirements": ["caldav==0.6.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index ca240925cf5..53edf48ae80 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -17,7 +17,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.util import dt - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -30,7 +29,7 @@ SCAN_INTERVAL = timedelta(seconds=60) async def async_setup(hass, config): """Track states and offer events for calendars.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, DOMAIN + _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) hass.http.register_view(CalendarListView(component)) diff --git a/homeassistant/components/calendar/manifest.json b/homeassistant/components/calendar/manifest.json index 3e6ee8422b4..abcff158bfb 100644 --- a/homeassistant/components/calendar/manifest.json +++ b/homeassistant/components/calendar/manifest.json @@ -3,8 +3,6 @@ "name": "Calendar", "documentation": "https://www.home-assistant.io/integrations/calendar", "requirements": [], - "dependencies": [ - "http" - ], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 58b6db139f5..4fe52a7d164 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -4,55 +4,54 @@ import base64 import collections from contextlib import suppress from datetime import timedelta -import logging import hashlib +import logging from random import SystemRandom -import attr from aiohttp import web import async_timeout +import attr import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components import websocket_api +from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, +) +from homeassistant.components.stream import request_stream +from homeassistant.components.stream.const import ( + CONF_DURATION, + CONF_LOOKBACK, + CONF_STREAM_SOURCE, + DOMAIN as DOMAIN_STREAM, + FORMAT_CONTENT_TYPE, + OUTPUT_FORMATS, + SERVICE_RECORD, +) from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_FILENAME, SERVICE_TURN_OFF, SERVICE_TURN_ON, - CONF_FILENAME, ) +from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.components.http import HomeAssistantView, KEY_AUTHENTICATED -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_CONTENT_TYPE, - SERVICE_PLAY_MEDIA, - DOMAIN as DOMAIN_MP, -) -from homeassistant.components.stream import request_stream -from homeassistant.components.stream.const import ( - OUTPUT_FORMATS, - FORMAT_CONTENT_TYPE, - CONF_STREAM_SOURCE, - CONF_LOOKBACK, - CONF_DURATION, - SERVICE_RECORD, - DOMAIN as DOMAIN_STREAM, -) -from homeassistant.components import websocket_api -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass from homeassistant.setup import async_when_setup -from .const import DOMAIN, DATA_CAMERA_PREFS +from .const import DATA_CAMERA_PREFS, DOMAIN from .prefs import CameraPreferences - # mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -169,7 +168,7 @@ async def async_get_still_stream(request, image_cb, content_type, interval): This method must be run in the event loop. """ response = web.StreamResponse() - response.content_type = "multipart/x-mixed-replace; " "boundary=--frameboundary" + response.content_type = "multipart/x-mixed-replace; boundary=--frameboundary" await response.prepare(request) async def write_to_mjpeg_stream(img_bytes): diff --git a/homeassistant/components/camera/manifest.json b/homeassistant/components/camera/manifest.json index 1bd4bb7caeb..e3a2400ac8b 100644 --- a/homeassistant/components/camera/manifest.json +++ b/homeassistant/components/camera/manifest.json @@ -4,5 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/camera", "requirements": [], "dependencies": ["http"], - "codeowners": [] + "after_dependencies": ["media_player"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index d83e0b55c96..ae182c62dc6 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,7 +1,6 @@ """Preference management for camera component.""" from .const import DOMAIN, PREF_PRELOAD_STREAM - # mypy: allow-untyped-defs, no-check-untyped-defs STORAGE_KEY = DOMAIN diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index f76d521853d..e383cb7514b 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -2,11 +2,7 @@ "domain": "canary", "name": "Canary", "documentation": "https://www.home-assistant.io/integrations/canary", - "requirements": [ - "py-canary==0.5.0" - ], - "dependencies": [ - "ffmpeg" - ], + "requirements": ["py-canary==0.5.0"], + "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 1374372aa24..f0eebf4b7b9 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Google \uce90\uc2a4\ud2b8" } }, diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index d3097b3cc29..54f165889af 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + INTERNAL_DISCOVERY_RUNNING_KEY, KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, - INTERNAL_DISCOVERY_RUNNING_KEY, SIGNAL_CAST_REMOVED, ) from .helpers import ChromecastInfo, ChromeCastZeroconf diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index d5d35ba7c9f..0b8633e1916 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -1,9 +1,8 @@ """Home Assistant Cast integration for Cast.""" from typing import Optional -import voluptuous as vol - from pychromecast.controllers.homeassistant import HomeAssistantController +import voluptuous as vol from homeassistant import auth, config_entries, core from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 8ad6f8fdb8d..c6db2d897e4 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -1,6 +1,6 @@ { "domain": "cast", - "name": "Cast", + "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", "requirements": ["pychromecast==4.0.1"], diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index c2d847fd09b..03174134502 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -4,12 +4,12 @@ import logging from typing import Optional import pychromecast +from pychromecast.controllers.homeassistant import HomeAssistantController +from pychromecast.controllers.multizone import MultizoneManager 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 @@ -46,22 +46,22 @@ import homeassistant.util.dt as dt_util from homeassistant.util.logging import async_create_catching_coro from .const import ( - DOMAIN as CAST_DOMAIN, ADDED_CAST_DEVICES_KEY, - SIGNAL_CAST_DISCOVERED, - KNOWN_CHROMECAST_INFO_KEY, CAST_MULTIZONE_MANAGER_KEY, DEFAULT_PORT, + DOMAIN as CAST_DOMAIN, + KNOWN_CHROMECAST_INFO_KEY, + SIGNAL_CAST_DISCOVERED, SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, ) +from .discovery import discover_chromecast, setup_internal_discovery from .helpers import ( - ChromecastInfo, CastStatusListener, - DynamicGroupCastStatusListener, + ChromecastInfo, ChromeCastZeroconf, + DynamicGroupCastStatusListener, ) -from .discovery import setup_internal_discovery, discover_chromecast _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json index c95a56320c9..26ee436860a 100644 --- a/homeassistant/components/cert_expiry/.translations/da.json +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -21,6 +21,6 @@ "title": "Definer certifikatet, der skal testes" } }, - "title": "Certifikat udl\u00f8b" + "title": "Certifikatets udl\u00f8bsdato" } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/.translations/de.json b/homeassistant/components/cert_expiry/.translations/de.json index 344abe13067..4df2ebe4fd9 100644 --- a/homeassistant/components/cert_expiry/.translations/de.json +++ b/homeassistant/components/cert_expiry/.translations/de.json @@ -4,10 +4,12 @@ "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert." }, "error": { + "certificate_error": "Zertifikat konnte nicht validiert werden", "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" + "resolve_failed": "Dieser Host kann nicht aufgel\u00f6st werden", + "wrong_host": "Zertifikat stimmt nicht mit Hostname \u00fcberein" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hant.json b/homeassistant/components/cert_expiry/.translations/zh-Hant.json index c710deae5c1..a14361376df 100644 --- a/homeassistant/components/cert_expiry/.translations/zh-Hant.json +++ b/homeassistant/components/cert_expiry/.translations/zh-Hant.json @@ -21,6 +21,6 @@ "title": "\u5b9a\u7fa9\u8a8d\u8b49\u9032\u884c\u6e2c\u8a66" } }, - "title": "\u8a8d\u8b49\u5df2\u904e\u671f" + "title": "\u6191\u8b49\u671f\u9650" } } \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 78450d247b9..14532aea65f 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -2,13 +2,14 @@ import logging import socket import ssl + import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME +from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN from .helper import get_cert _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 48816809bbd..dc26006d711 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -1,12 +1,9 @@ { - "domain": "cert_expiry", - "name": "Cert expiry", - "documentation": "https://www.home-assistant.io/integrations/cert_expiry", - "requirements": [], - "config_flow": true, - "dependencies": [], - "codeowners": [ - "@Cereal2nd", - "@jjlawren" - ] + "domain": "cert_expiry", + "name": "Certificate Expiry", + "documentation": "https://www.home-assistant.io/integrations/cert_expiry", + "requirements": [], + "config_flow": true, + "dependencies": [], + "codeowners": ["@Cereal2nd", "@jjlawren"] } diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 3022c7bd42b..3a76575dfdd 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -1,24 +1,24 @@ """Counter for the days until an HTTPS (TLS) certificate will expire.""" +from datetime import datetime, timedelta import logging import socket import ssl -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.config_entries import SOURCE_IMPORT from homeassistant.const import ( - CONF_NAME, CONF_HOST, + CONF_NAME, CONF_PORT, EVENT_HOMEASSISTANT_START, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT +from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN from .helper import get_cert _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/channels/manifest.json b/homeassistant/components/channels/manifest.json index c6ef7f8f127..3a61d0636bc 100644 --- a/homeassistant/components/channels/manifest.json +++ b/homeassistant/components/channels/manifest.json @@ -2,9 +2,7 @@ "domain": "channels", "name": "Channels", "documentation": "https://www.home-assistant.io/integrations/channels", - "requirements": [ - "pychannels==1.0.0" - ], + "requirements": ["pychannels==1.0.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/cisco_ios/manifest.json b/homeassistant/components/cisco_ios/manifest.json index 4a04ffa32e6..0cdcddb56df 100644 --- a/homeassistant/components/cisco_ios/manifest.json +++ b/homeassistant/components/cisco_ios/manifest.json @@ -1,10 +1,8 @@ { "domain": "cisco_ios", - "name": "Cisco ios", + "name": "Cisco IOS", "documentation": "https://www.home-assistant.io/integrations/cisco_ios", - "requirements": [ - "pexpect==4.6.0" - ], + "requirements": ["pexpect==4.6.0"], "dependencies": [], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index 702ebdfa611..db504e3d19b 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -89,5 +89,5 @@ class CiscoMEDeviceScanner(DeviceScanner): """Check the Cisco ME controller for devices.""" self.last_results = self.controller.get_associated_devices() _LOGGER.debug( - "Cisco Mobility Express controller returned:" " %s", self.last_results + "Cisco Mobility Express controller returned: %s", self.last_results ) diff --git a/homeassistant/components/cisco_mobility_express/manifest.json b/homeassistant/components/cisco_mobility_express/manifest.json index b4bc2ff86d9..4c83116747b 100644 --- a/homeassistant/components/cisco_mobility_express/manifest.json +++ b/homeassistant/components/cisco_mobility_express/manifest.json @@ -1,10 +1,8 @@ { "domain": "cisco_mobility_express", - "name": "Cisco mobility express", + "name": "Cisco Mobility Express", "documentation": "https://www.home-assistant.io/integrations/cisco_mobility_express", - "requirements": [ - "ciscomobilityexpress==0.3.3" - ], + "requirements": ["ciscomobilityexpress==0.3.3"], "dependencies": [], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/cisco_webex_teams/manifest.json b/homeassistant/components/cisco_webex_teams/manifest.json index 3560b1e7fcd..c9d6d14c109 100644 --- a/homeassistant/components/cisco_webex_teams/manifest.json +++ b/homeassistant/components/cisco_webex_teams/manifest.json @@ -1,10 +1,8 @@ { "domain": "cisco_webex_teams", - "name": "Cisco webex teams", + "name": "Cisco Webex Teams", "documentation": "https://www.home-assistant.io/integrations/cisco_webex_teams", - "requirements": [ - "webexteamssdk==1.1.1" - ], + "requirements": ["webexteamssdk==1.1.1"], "dependencies": [], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index 6f80fa138d4..7be53d1fb6c 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -54,5 +54,5 @@ class CiscoWebexTeamsNotificationService(BaseNotificationService): 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 + "Could not send CiscoWebexTeams notification. Error: %s", api_error ) diff --git a/homeassistant/components/ciscospark/manifest.json b/homeassistant/components/ciscospark/manifest.json index 8058088bf8a..4fd87a8a5e4 100644 --- a/homeassistant/components/ciscospark/manifest.json +++ b/homeassistant/components/ciscospark/manifest.json @@ -1,10 +1,8 @@ { "domain": "ciscospark", - "name": "Ciscospark", + "name": "Cisco Spark", "documentation": "https://www.home-assistant.io/integrations/ciscospark", - "requirements": [ - "ciscosparkapi==0.4.2" - ], + "requirements": ["ciscosparkapi==0.4.2"], "dependencies": [], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/citybikes/manifest.json b/homeassistant/components/citybikes/manifest.json index f46c7ba708f..488997378ef 100644 --- a/homeassistant/components/citybikes/manifest.json +++ b/homeassistant/components/citybikes/manifest.json @@ -1,6 +1,6 @@ { "domain": "citybikes", - "name": "Citybikes", + "name": "CityBikes", "documentation": "https://www.home-assistant.io/integrations/citybikes", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/citybikes/sensor.py b/homeassistant/components/citybikes/sensor.py index cb2647487ea..8e0b883b726 100644 --- a/homeassistant/components/citybikes/sensor.py +++ b/homeassistant/components/citybikes/sensor.py @@ -57,7 +57,7 @@ SCAN_INTERVAL = timedelta(minutes=5) # Timely, and doesn't suffocate the API STATIONS_URI = "v2/networks/{uid}?fields=network.stations" CITYBIKES_ATTRIBUTION = ( - "Information provided by the CityBikes Project " "(https://citybik.es/#about)" + "Information provided by the CityBikes Project (https://citybik.es/#about)" ) CITYBIKES_NETWORKS = "citybikes_networks" @@ -143,9 +143,7 @@ async def async_citybikes_request(hass, uri, schema): except ValueError: _LOGGER.error("Received non-JSON data from CityBikes API endpoint") except vol.Invalid as err: - _LOGGER.error( - "Received unexpected JSON from CityBikes" " API endpoint: %s", err - ) + _LOGGER.error("Received unexpected JSON from CityBikes API endpoint: %s", err) raise CityBikesRequestError diff --git a/homeassistant/components/clementine/manifest.json b/homeassistant/components/clementine/manifest.json index 35368dd6cdf..dadb28c0392 100644 --- a/homeassistant/components/clementine/manifest.json +++ b/homeassistant/components/clementine/manifest.json @@ -1,10 +1,8 @@ { "domain": "clementine", - "name": "Clementine", + "name": "Clementine Music Player", "documentation": "https://www.home-assistant.io/integrations/clementine", - "requirements": [ - "python-clementine-remote==1.0.1" - ], + "requirements": ["python-clementine-remote==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/clickatell/notify.py b/homeassistant/components/clickatell/notify.py index 26f2f30aeff..d59a553a4f6 100644 --- a/homeassistant/components/clickatell/notify.py +++ b/homeassistant/components/clickatell/notify.py @@ -4,11 +4,10 @@ import logging import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_API_KEY, CONF_RECIPIENT import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "clickatell" diff --git a/homeassistant/components/clicksend/manifest.json b/homeassistant/components/clicksend/manifest.json index 6a28b3c30ca..18f048d1efc 100644 --- a/homeassistant/components/clicksend/manifest.json +++ b/homeassistant/components/clicksend/manifest.json @@ -1,6 +1,6 @@ { "domain": "clicksend", - "name": "Clicksend", + "name": "ClickSend SMS", "documentation": "https://www.home-assistant.io/integrations/clicksend", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/clicksend/notify.py b/homeassistant/components/clicksend/notify.py index 87fc217ac42..42136e9a09c 100644 --- a/homeassistant/components/clicksend/notify.py +++ b/homeassistant/components/clicksend/notify.py @@ -6,6 +6,7 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -15,8 +16,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" diff --git a/homeassistant/components/clicksend_tts/manifest.json b/homeassistant/components/clicksend_tts/manifest.json index 8aa3eacf405..75b9ec2619f 100644 --- a/homeassistant/components/clicksend_tts/manifest.json +++ b/homeassistant/components/clicksend_tts/manifest.json @@ -1,6 +1,6 @@ { "domain": "clicksend_tts", - "name": "Clicksend tts", + "name": "ClickSend TTS", "documentation": "https://www.home-assistant.io/integrations/clicksend_tts", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/clicksend_tts/notify.py b/homeassistant/components/clicksend_tts/notify.py index ba30c61e937..400e72a7d0c 100644 --- a/homeassistant/components/clicksend_tts/notify.py +++ b/homeassistant/components/clicksend_tts/notify.py @@ -6,6 +6,7 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import ( CONF_API_KEY, CONF_RECIPIENT, @@ -14,8 +15,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) BASE_API_URL = "https://rest.clicksend.com/v3" diff --git a/homeassistant/components/climate/.translations/bg.json b/homeassistant/components/climate/.translations/bg.json index d7901d29884..ac1b05b096a 100644 --- a/homeassistant/components/climate/.translations/bg.json +++ b/homeassistant/components/climate/.translations/bg.json @@ -4,7 +4,7 @@ "set_hvac_mode": "\u041f\u0440\u043e\u043c\u044f\u043d\u0430 \u043d\u0430 \u0440\u0435\u0436\u0438\u043c \u043d\u0430 \u041e\u0412\u041a \u043d\u0430 {entity_name}", "set_preset_mode": "\u041f\u0440\u043e\u043c\u0435\u043d\u0438 \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043d\u0430 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d \u043d\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u0447\u0435\u043d \u041e\u0412\u041a \u0440\u0435\u0436\u0438\u043c", "is_preset_mode": "{entity_name} \u0435 \u0432 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u043d\u043e \u0437\u0430\u0434\u0430\u0434\u0435\u043d \u0440\u0435\u0436\u0438\u043c" }, diff --git a/homeassistant/components/climate/.translations/ca.json b/homeassistant/components/climate/.translations/ca.json index 743729041ab..bde91c26b7e 100644 --- a/homeassistant/components/climate/.translations/ca.json +++ b/homeassistant/components/climate/.translations/ca.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Canvia el mode HVAC de {entity_name}", "set_preset_mode": "Canvia la configuraci\u00f3 preestablerta de {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} est\u00e0 configurat/ada en un mode HVAC espec\u00edfic", "is_preset_mode": "{entity_name} est\u00e0 configurat/ada en un mode preestablert espec\u00edfic" }, diff --git a/homeassistant/components/climate/.translations/da.json b/homeassistant/components/climate/.translations/da.json new file mode 100644 index 00000000000..78731dd1577 --- /dev/null +++ b/homeassistant/components/climate/.translations/da.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Skift af klimaanl\u00e6gstilstand p\u00e5 {entity_name}", + "set_preset_mode": "Skift af forudindstilling p\u00e5 {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} er indstillet til en bestemt klimaanl\u00e6gstilstand", + "is_preset_mode": "{entity_name} er indstillet til en bestemt forudindstillet tilstand" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} m\u00e5lte luftfugtighed \u00e6ndret", + "current_temperature_changed": "{entity_name} m\u00e5lte temperatur \u00e6ndret", + "hvac_mode_changed": "{entity_name} klimaanl\u00e6gstilstand \u00e6ndret" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/de.json b/homeassistant/components/climate/.translations/de.json index 75ffe328fc8..444c2cc460b 100644 --- a/homeassistant/components/climate/.translations/de.json +++ b/homeassistant/components/climate/.translations/de.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "set_hvac_mode": "HVAC-Modus auf {entity_name} \u00e4ndern", + "set_preset_mode": "Voreinstellung von {entity_name} \u00e4ndern" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} ist auf einen bestimmten HVAC-Modus festgelegt", + "is_preset_mode": "{entity_name} ist auf einen bestimmten voreingestellten Modus eingestellt" + }, "trigger_type": { "current_humidity_changed": "Gemessene Luftfeuchtigkeit von {entity_name} ge\u00e4ndert", - "current_temperature_changed": "Gemessene Temperatur von {entity_name} ge\u00e4ndert" + "current_temperature_changed": "Gemessene Temperatur von {entity_name} ge\u00e4ndert", + "hvac_mode_changed": "{entity_name} HVAC-Modus ge\u00e4ndert" } } } \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/en.json b/homeassistant/components/climate/.translations/en.json index 942d9a2761f..2a56426e988 100644 --- a/homeassistant/components/climate/.translations/en.json +++ b/homeassistant/components/climate/.translations/en.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Change HVAC mode on {entity_name}", "set_preset_mode": "Change preset on {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", "is_preset_mode": "{entity_name} is set to a specific preset mode" }, diff --git a/homeassistant/components/climate/.translations/es.json b/homeassistant/components/climate/.translations/es.json index baae9b97436..e873427e694 100644 --- a/homeassistant/components/climate/.translations/es.json +++ b/homeassistant/components/climate/.translations/es.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Cambiar el modo HVAC de {entity_name}.", "set_preset_mode": "Cambiar la configuraci\u00f3n prefijada de {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", "is_preset_mode": "{entity_name} se establece en un modo predeterminado espec\u00edfico" }, diff --git a/homeassistant/components/climate/.translations/fr.json b/homeassistant/components/climate/.translations/fr.json index db29f8424d5..0358a60f180 100644 --- a/homeassistant/components/climate/.translations/fr.json +++ b/homeassistant/components/climate/.translations/fr.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Changer le mode HVAC sur {entity_name}.", "set_preset_mode": "Changer les pr\u00e9r\u00e9glages de {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} est d\u00e9fini sur un mode HVAC sp\u00e9cifique", "is_preset_mode": "{entity_name} est d\u00e9fini sur un mode pr\u00e9d\u00e9fini sp\u00e9cifique" }, diff --git a/homeassistant/components/climate/.translations/it.json b/homeassistant/components/climate/.translations/it.json index 34ecbf5e9f2..25a09b7d66d 100644 --- a/homeassistant/components/climate/.translations/it.json +++ b/homeassistant/components/climate/.translations/it.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Cambia modalit\u00e0 HVAC su {entity_name}", "set_preset_mode": "Modifica preimpostazione su {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 HVAC specifica", "is_preset_mode": "{entity_name} \u00e8 impostato su una modalit\u00e0 preimpostata specifica" }, diff --git a/homeassistant/components/climate/.translations/ko.json b/homeassistant/components/climate/.translations/ko.json new file mode 100644 index 00000000000..299172958e8 --- /dev/null +++ b/homeassistant/components/climate/.translations/ko.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "{entity_name} \uc758 HVAC \ubaa8\ub4dc \ubcc0\uacbd", + "set_preset_mode": "{entity_name} \uc758 \uc0ac\uc804 \uc124\uc815 \ubcc0\uacbd" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 HVAC \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74", + "is_preset_mode": "{entity_name} \uc774(\uac00) \ud2b9\uc815 \uc0ac\uc804 \uc124\uc815 \ubaa8\ub4dc\ub85c \uc124\uc815\ub418\uc5b4\uc788\uc73c\uba74" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} \uc774(\uac00) \uc2b5\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c", + "current_temperature_changed": "{entity_name} \uc774(\uac00) \uc628\ub3c4 \ubcc0\ud654\ub97c \uac10\uc9c0\ud560 \ub54c", + "hvac_mode_changed": "{entity_name} HVAC \ubaa8\ub4dc\uac00 \ubcc0\uacbd\ub420 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/lb.json b/homeassistant/components/climate/.translations/lb.json index 72ab7efc623..2b6ca061fd8 100644 --- a/homeassistant/components/climate/.translations/lb.json +++ b/homeassistant/components/climate/.translations/lb.json @@ -4,8 +4,8 @@ "set_hvac_mode": "HVAC Modus \u00e4nnere fir {entity_name}", "set_preset_mode": "Preset \u00e4nnere fir {entity_name}" }, - "condtion_type": { - "is_hvac_mode": "\n{entity_name} ass op e spezifesche HVAC Modus gesat", + "condition_type": { + "is_hvac_mode": "{entity_name} ass op e spezifesche HVAC Modus gesat", "is_preset_mode": "{entity_name} ass op e spezifesche preset Modus gesat" }, "trigger_type": { diff --git a/homeassistant/components/climate/.translations/nl.json b/homeassistant/components/climate/.translations/nl.json new file mode 100644 index 00000000000..87e16c1c885 --- /dev/null +++ b/homeassistant/components/climate/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_hvac_mode": "Wijzig de HVAC-modus op {entity_name}", + "set_preset_mode": "Wijzig voorinstelling op {entity_name}" + }, + "condition_type": { + "is_hvac_mode": "{entity_name} is ingesteld op een specifieke HVAC-modus", + "is_preset_mode": "{entity_name} is ingesteld op een specifieke vooraf ingestelde modus" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} gemeten vochtigheid veranderd", + "current_temperature_changed": "{entity_name} gemeten temperatuur veranderd", + "hvac_mode_changed": "{entity_name} HVAC-modus gewijzigd" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/.translations/no.json b/homeassistant/components/climate/.translations/no.json index 2d95c63a6ae..bc6e97b9aa5 100644 --- a/homeassistant/components/climate/.translations/no.json +++ b/homeassistant/components/climate/.translations/no.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Endre HVAC-modus p\u00e5 {entity_name}", "set_preset_mode": "Endre forh\u00e5ndsinnstilling p\u00e5 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} er satt til en spesifikk HVAC-modus", "is_preset_mode": "{entity_name} er satt til en spesifikk forh\u00e5ndsinnstilt modus" }, diff --git a/homeassistant/components/climate/.translations/pl.json b/homeassistant/components/climate/.translations/pl.json index c5b0c483ca9..f2a09eee3ef 100644 --- a/homeassistant/components/climate/.translations/pl.json +++ b/homeassistant/components/climate/.translations/pl.json @@ -4,7 +4,7 @@ "set_hvac_mode": "zmie\u0144 tryb HVAC na {entity_name}", "set_preset_mode": "zmie\u0144 ustawienia dla {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "na {entity_name} jest ustawiony okre\u015blony tryb HVAC", "is_preset_mode": "na {entity_name} jest okre\u015blone ustawienie" }, diff --git a/homeassistant/components/climate/.translations/ru.json b/homeassistant/components/climate/.translations/ru.json index 045f96137d2..6a9c52be209 100644 --- a/homeassistant/components/climate/.translations/ru.json +++ b/homeassistant/components/climate/.translations/ru.json @@ -4,7 +4,7 @@ "set_hvac_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"", "set_preset_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430\u0431\u043e\u0440 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043d\u043e\u043c \u0440\u0435\u0436\u0438\u043c\u0435 \u0440\u0430\u0431\u043e\u0442\u044b", "is_preset_mode": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u043f\u0440\u0435\u0434\u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043d\u0430\u0431\u043e\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a" }, diff --git a/homeassistant/components/climate/.translations/sl.json b/homeassistant/components/climate/.translations/sl.json index 4ba4cb02a4b..ecaf24fed80 100644 --- a/homeassistant/components/climate/.translations/sl.json +++ b/homeassistant/components/climate/.translations/sl.json @@ -4,7 +4,7 @@ "set_hvac_mode": "Spremeni na\u010din HVAC na {entity_name}", "set_preset_mode": "Spremenite prednastavitev na {entity_name}" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} je nastavljen na dolo\u010den na\u010din HVAC", "is_preset_mode": "{entity_name} je nastavljen na dolo\u010den prednastavljeni na\u010din" }, diff --git a/homeassistant/components/climate/.translations/zh-Hant.json b/homeassistant/components/climate/.translations/zh-Hant.json index 1d39eecc056..17e6c955046 100644 --- a/homeassistant/components/climate/.translations/zh-Hant.json +++ b/homeassistant/components/climate/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "set_hvac_mode": "\u8b8a\u66f4 {entity_name} HVAC \u6a21\u5f0f", "set_preset_mode": "\u8b8a\u66f4 {entity_name} \u8a2d\u5b9a\u6a21\u5f0f" }, - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a HVAC \u6a21\u5f0f", "is_preset_mode": "{entity_name} \u8a2d\u5b9a\u70ba\u6307\u5b9a\u8a2d\u5b9a\u6a21\u5f0f" }, diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 6006b2a9a3b..f3aff44ff4d 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -19,9 +19,9 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent @@ -68,8 +68,8 @@ from .const import ( SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) DEFAULT_MIN_TEMP = 7 @@ -172,17 +172,11 @@ class ClimateDevice(Entity): return PRECISION_WHOLE @property - def state_attributes(self) -> Dict[str, Any]: - """Return the optional state attributes.""" + def capability_attributes(self) -> Optional[Dict[str, Any]]: + """Return the capability attributes.""" supported_features = self.supported_features data = { ATTR_HVAC_MODES: self.hvac_modes, - ATTR_CURRENT_TEMPERATURE: show_temp( - self.hass, - self.current_temperature, - self.temperature_unit, - self.precision, - ), ATTR_MIN_TEMP: show_temp( self.hass, self.min_temp, self.temperature_unit, self.precision ), @@ -194,6 +188,34 @@ class ClimateDevice(Entity): if self.target_temperature_step: data[ATTR_TARGET_TEMP_STEP] = self.target_temperature_step + if supported_features & SUPPORT_TARGET_HUMIDITY: + data[ATTR_MIN_HUMIDITY] = self.min_humidity + data[ATTR_MAX_HUMIDITY] = self.max_humidity + + if supported_features & SUPPORT_FAN_MODE: + data[ATTR_FAN_MODES] = self.fan_modes + + if supported_features & SUPPORT_PRESET_MODE: + data[ATTR_PRESET_MODES] = self.preset_modes + + if supported_features & SUPPORT_SWING_MODE: + data[ATTR_SWING_MODES] = self.swing_modes + + return data + + @property + def state_attributes(self) -> Dict[str, Any]: + """Return the optional state attributes.""" + supported_features = self.supported_features + data = { + ATTR_CURRENT_TEMPERATURE: show_temp( + self.hass, + self.current_temperature, + self.temperature_unit, + self.precision, + ), + } + if supported_features & SUPPORT_TARGET_TEMPERATURE: data[ATTR_TEMPERATURE] = show_temp( self.hass, @@ -221,23 +243,18 @@ class ClimateDevice(Entity): if supported_features & SUPPORT_TARGET_HUMIDITY: data[ATTR_HUMIDITY] = self.target_humidity - data[ATTR_MIN_HUMIDITY] = self.min_humidity - data[ATTR_MAX_HUMIDITY] = self.max_humidity if supported_features & SUPPORT_FAN_MODE: data[ATTR_FAN_MODE] = self.fan_mode - data[ATTR_FAN_MODES] = self.fan_modes if self.hvac_action: data[ATTR_HVAC_ACTION] = self.hvac_action if supported_features & SUPPORT_PRESET_MODE: data[ATTR_PRESET_MODE] = self.preset_mode - data[ATTR_PRESET_MODES] = self.preset_modes if supported_features & SUPPORT_SWING_MODE: data[ATTR_SWING_MODE] = self.swing_mode - data[ATTR_SWING_MODES] = self.swing_modes if supported_features & SUPPORT_AUX_HEAT: data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF diff --git a/homeassistant/components/climate/device_action.py b/homeassistant/components/climate/device_action.py index 836e2277461..6f7725ac835 100644 --- a/homeassistant/components/climate/device_action.py +++ b/homeassistant/components/climate/device_action.py @@ -1,17 +1,19 @@ """Provides device automations for Climate.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN, const ACTION_TYPES = {"set_hvac_mode", "set_preset_mode"} diff --git a/homeassistant/components/climate/device_condition.py b/homeassistant/components/climate/device_condition.py index 3a075233942..cf393a035ec 100644 --- a/homeassistant/components/climate/device_condition.py +++ b/homeassistant/components/climate/device_condition.py @@ -1,19 +1,21 @@ """Provide the device automations for Climate.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN, const CONDITION_TYPES = {"is_hvac_mode", "is_preset_mode"} diff --git a/homeassistant/components/climate/device_trigger.py b/homeassistant/components/climate/device_trigger.py index e814bdc88de..4c5dcb0ee04 100644 --- a/homeassistant/components/climate/device_trigger.py +++ b/homeassistant/components/climate/device_trigger.py @@ -1,26 +1,28 @@ """Provides device automations for Climate.""" from typing import List + import voluptuous as vol -from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, - CONF_DEVICE_ID, - CONF_ENTITY_ID, - CONF_FOR, - CONF_ABOVE, - CONF_BELOW, -) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE -from homeassistant.helpers import config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType from homeassistant.components.automation import ( - state as state_automation, - numeric_state as numeric_state_automation, AutomationActionType, + numeric_state as numeric_state_automation, + state as state_automation, ) from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + CONF_ABOVE, + CONF_BELOW, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_FOR, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers import config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType + from . import DOMAIN, const TRIGGER_TYPES = { diff --git a/homeassistant/components/climate/manifest.json b/homeassistant/components/climate/manifest.json index 5933eaf9071..4ac1f55b2b0 100644 --- a/homeassistant/components/climate/manifest.json +++ b/homeassistant/components/climate/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/climate", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index 34e72a27c92..82ca4f4e85c 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -8,20 +8,20 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import ( ATTR_AUX_HEAT, + ATTR_HUMIDITY, + ATTR_HVAC_MODE, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_PRESET_MODE, - ATTR_HVAC_MODE, - ATTR_SWING_MODE, - ATTR_HUMIDITY, + DOMAIN, HVAC_MODES, SERVICE_SET_AUX_HEAT, - SERVICE_SET_TEMPERATURE, - SERVICE_SET_PRESET_MODE, - SERVICE_SET_HVAC_MODE, - SERVICE_SET_SWING_MODE, SERVICE_SET_HUMIDITY, - DOMAIN, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_SWING_MODE, + SERVICE_SET_TEMPERATURE, ) diff --git a/homeassistant/components/climate/strings.json b/homeassistant/components/climate/strings.json index a2ceeff2143..ff071aed083 100644 --- a/homeassistant/components/climate/strings.json +++ b/homeassistant/components/climate/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_hvac_mode": "{entity_name} is set to a specific HVAC mode", "is_preset_mode": "{entity_name} is set to a specific preset mode" }, diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 6d9b70051f5..0a0b9f0fe88 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -33,7 +33,6 @@ from .const import ( CONF_FILTER, CONF_GOOGLE_ACTIONS, CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, - CONF_GOOGLE_ACTIONS_SYNC_URL, CONF_RELAYER, CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, @@ -93,7 +92,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_USER_POOL_ID): str, vol.Optional(CONF_REGION): str, vol.Optional(CONF_RELAYER): str, - vol.Optional(CONF_GOOGLE_ACTIONS_SYNC_URL): vol.Url(), vol.Optional(CONF_SUBSCRIPTION_INFO_URL): vol.Url(), vol.Optional(CONF_CLOUDHOOK_CREATE_URL): vol.Url(), vol.Optional(CONF_REMOTE_API_URL): vol.Url(), @@ -155,10 +153,13 @@ def async_remote_ui_url(hass) -> str: if not async_is_logged_in(hass): raise CloudNotAvailable + if not hass.data[DOMAIN].client.prefs.remote_enabled: + raise CloudNotAvailable + if not hass.data[DOMAIN].remote.instance_domain: raise CloudNotAvailable - return "https://" + hass.data[DOMAIN].remote.instance_domain + return f"https://{hass.data[DOMAIN].remote.instance_domain}" def is_cloudhook_request(request): diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index 9ec1fe634d7..1d0de26918d 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -7,7 +7,7 @@ from hass_nabucasa import account_link from homeassistant.const import MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import event, config_entry_oauth2_flow +from homeassistant.helpers import config_entry_oauth2_flow, event from .const import DOMAIN diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index a1432f196bf..8d1527b1930 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -7,24 +7,23 @@ import aiohttp import async_timeout from hass_nabucasa import cloud_api -from homeassistant.core import callback +from homeassistant.components.alexa import ( + config as alexa_config, + entities as alexa_entities, + errors as alexa_errors, + state_report as alexa_state_report, +) from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.event import async_call_later from homeassistant.util.dt import utcnow -from homeassistant.components.alexa import ( - config as alexa_config, - errors as alexa_errors, - entities as alexa_entities, - state_report as alexa_state_report, -) - from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, - PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE, + PREF_SHOULD_EXPOSE, RequireRelink, ) @@ -79,6 +78,12 @@ class AlexaConfig(alexa_config.AbstractConfig): return self._endpoint + @property + def locale(self): + """Return config locale.""" + # Not clear how to determine locale atm. + return "en-US" + @property def entity_config(self): """Return entity config.""" diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 2192eec8923..056105f8071 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -6,7 +6,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN - WAIT_UNTIL_CHANGE = 3 diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 956d35caf2d..24947ed7952 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -1,27 +1,26 @@ """Interface implementation for cloud client.""" import asyncio +import logging from pathlib import Path from typing import Any, Dict -import logging import aiohttp from hass_nabucasa.client import CloudClient as Interface -from homeassistant.core import callback, Context -from homeassistant.components.google_assistant import smart_home as ga -from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.util.aiohttp import MockRequest from homeassistant.components.alexa import ( - smart_home as alexa_sh, errors as alexa_errors, + smart_home as alexa_sh, ) +from homeassistant.components.google_assistant import smart_home as ga +from homeassistant.core import Context, callback +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.aiohttp import MockRequest -from . import utils, alexa_config, google_config +from . import alexa_config, google_config, utils from .const import DISPATCHER_REMOTE_UPDATE from .prefs import CloudPreferences - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 406263c85f8..3d930f0c2e5 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -31,7 +31,6 @@ CONF_FILTER = "filter" CONF_GOOGLE_ACTIONS = "google_actions" CONF_RELAYER = "relayer" CONF_USER_POOL_ID = "user_pool_id" -CONF_GOOGLE_ACTIONS_SYNC_URL = "google_actions_sync_url" CONF_SUBSCRIPTION_INFO_URL = "subscription_info_url" CONF_CLOUDHOOK_CREATE_URL = "cloudhook_create_url" CONF_REMOTE_API_URL = "remote_api_url" diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 6753d74ba45..1074aaa68b3 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -2,19 +2,19 @@ import asyncio import logging -import async_timeout +from hass_nabucasa import cloud_api from hass_nabucasa.google_report_state import ErrorResponse -from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.components.google_assistant.helpers import AbstractConfig +from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES from homeassistant.helpers import entity_registry from .const import ( - PREF_SHOULD_EXPOSE, - DEFAULT_SHOULD_EXPOSE, CONF_ENTITY_CONFIG, - PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA, + DEFAULT_SHOULD_EXPOSE, + PREF_DISABLE_2FA, + PREF_SHOULD_EXPOSE, ) _LOGGER = logging.getLogger(__name__) @@ -126,21 +126,9 @@ class CloudGoogleConfig(AbstractConfig): if self._sync_entities_lock.locked(): return 200 - websession = self.hass.helpers.aiohttp_client.async_get_clientsession() - async with self._sync_entities_lock: - with async_timeout.timeout(10): - await self._cloud.auth.async_check_token() - - _LOGGER.debug("Requesting sync") - - with async_timeout.timeout(30): - req = await websession.post( - self._cloud.google_actions_sync_url, - headers={"authorization": self._cloud.id_token}, - ) - _LOGGER.debug("Finished requesting syncing: %s", req.status) - return req.status + resp = await cloud_api.async_google_actions_request_sync(self._cloud) + return resp.status async def _async_prefs_updated(self, prefs): """Handle updated preferences.""" diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index d808fe72d39..9afaad422ba 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -583,7 +583,7 @@ async def alexa_sync(hass, connection, msg): connection.send_error( msg["id"], "alexa_relink", - "Please go to the Alexa app and re-link the Home Assistant " "skill.", + "Please go to the Alexa app and re-link the Home Assistant skill.", ) return diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index ec9a556af0a..34ef7a6dfa5 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -1,8 +1,9 @@ { "domain": "cloud", - "name": "Cloud", + "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.30"], + "requirements": ["hass-nabucasa==0.31"], "dependencies": ["http", "webhook"], + "after_dependencies": ["alexa", "google_assistant"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index e96ee9527fb..a7d1b59fd39 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -2,31 +2,31 @@ from ipaddress import ip_address from typing import Optional -from homeassistant.core import callback -from homeassistant.auth.models import User from homeassistant.auth.const import GROUP_ID_ADMIN +from homeassistant.auth.models import User +from homeassistant.core import callback from homeassistant.util.logging import async_create_catching_coro from .const import ( + DEFAULT_ALEXA_REPORT_STATE, + DEFAULT_GOOGLE_REPORT_STATE, DOMAIN, + PREF_ALEXA_ENTITY_CONFIGS, + PREF_ALEXA_REPORT_STATE, + PREF_ALIASES, + PREF_CLOUD_USER, + PREF_CLOUDHOOKS, + PREF_DISABLE_2FA, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE, - PREF_GOOGLE_SECURE_DEVICES_PIN, - PREF_CLOUDHOOKS, - PREF_CLOUD_USER, PREF_GOOGLE_ENTITY_CONFIGS, - PREF_OVERRIDE_NAME, - PREF_DISABLE_2FA, - PREF_ALIASES, - PREF_SHOULD_EXPOSE, - PREF_ALEXA_ENTITY_CONFIGS, - PREF_ALEXA_REPORT_STATE, - PREF_USERNAME, - DEFAULT_ALEXA_REPORT_STATE, - PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_LOCAL_WEBHOOK_ID, - DEFAULT_GOOGLE_REPORT_STATE, + PREF_GOOGLE_REPORT_STATE, + PREF_GOOGLE_SECURE_DEVICES_PIN, + PREF_OVERRIDE_NAME, + PREF_SHOULD_EXPOSE, + PREF_USERNAME, InvalidTrustedNetworks, InvalidTrustedProxies, ) diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index acca36afae9..6c069ce16d7 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -52,7 +52,7 @@ class CloudProvider(Provider): """NabuCasa speech API provider.""" def __init__(self, cloud: Cloud) -> None: - """Hass NabuCasa Speech to text.""" + """Home Assistant NabuCasa Speech to text.""" self.cloud = cloud @property diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 338b97d2bd9..ea769c6a054 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -1,7 +1,7 @@ """Support for the cloud for text to speech service.""" -from hass_nabucasa.voice import VoiceError from hass_nabucasa import Cloud +from hass_nabucasa.voice import VoiceError import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/cloud/utils.py b/homeassistant/components/cloud/utils.py index 5040baada9a..36599b42ad3 100644 --- a/homeassistant/components/cloud/utils.py +++ b/homeassistant/components/cloud/utils.py @@ -1,7 +1,7 @@ """Helper functions for cloud components.""" from typing import Any, Dict -from aiohttp import web, payload +from aiohttp import payload, web def aiohttp_serialize_response(response: web.Response) -> Dict[str, Any]: diff --git a/homeassistant/components/cloudflare/manifest.json b/homeassistant/components/cloudflare/manifest.json index 78bc6de99c8..44beaaa213a 100644 --- a/homeassistant/components/cloudflare/manifest.json +++ b/homeassistant/components/cloudflare/manifest.json @@ -2,11 +2,7 @@ "domain": "cloudflare", "name": "Cloudflare", "documentation": "https://www.home-assistant.io/integrations/cloudflare", - "requirements": [ - "pycfdns==0.0.1" - ], + "requirements": ["pycfdns==0.0.1"], "dependencies": [], - "codeowners": [ - "@ludeeus" - ] + "codeowners": ["@ludeeus"] } diff --git a/homeassistant/components/cmus/manifest.json b/homeassistant/components/cmus/manifest.json index fe5b8e155c2..22585f7766b 100644 --- a/homeassistant/components/cmus/manifest.json +++ b/homeassistant/components/cmus/manifest.json @@ -1,10 +1,8 @@ { "domain": "cmus", - "name": "Cmus", + "name": "cmus", "documentation": "https://www.home-assistant.io/integrations/cmus", - "requirements": [ - "pycmus==0.1.1" - ], + "requirements": ["pycmus==0.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/co2signal/manifest.json b/homeassistant/components/co2signal/manifest.json index f07813b3db5..5caab7fe89c 100644 --- a/homeassistant/components/co2signal/manifest.json +++ b/homeassistant/components/co2signal/manifest.json @@ -1,10 +1,8 @@ { "domain": "co2signal", - "name": "Co2signal", + "name": "CO2 Signal", "documentation": "https://www.home-assistant.io/integrations/co2signal", - "requirements": [ - "co2signal==0.4.2" - ], + "requirements": ["co2signal==0.4.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 67869e6b88c..d52c0867e24 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -94,5 +94,5 @@ class CoinbaseData: self.exchange_rates = self.client.get_exchange_rates() except AuthenticationError as coinbase_error: _LOGGER.error( - "Authentication error connecting" " to coinbase: %s", coinbase_error + "Authentication error connecting to coinbase: %s", coinbase_error ) diff --git a/homeassistant/components/coinbase/manifest.json b/homeassistant/components/coinbase/manifest.json index 2da323f0815..dfd05475703 100644 --- a/homeassistant/components/coinbase/manifest.json +++ b/homeassistant/components/coinbase/manifest.json @@ -2,9 +2,7 @@ "domain": "coinbase", "name": "Coinbase", "documentation": "https://www.home-assistant.io/integrations/coinbase", - "requirements": [ - "coinbase==2.1.0" - ], + "requirements": ["coinbase==2.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/coinmarketcap/manifest.json b/homeassistant/components/coinmarketcap/manifest.json index ec9aec6c654..2aa7e64587a 100644 --- a/homeassistant/components/coinmarketcap/manifest.json +++ b/homeassistant/components/coinmarketcap/manifest.json @@ -1,10 +1,8 @@ { "domain": "coinmarketcap", - "name": "Coinmarketcap", + "name": "CoinMarketCap", "documentation": "https://www.home-assistant.io/integrations/coinmarketcap", - "requirements": [ - "coinmarketcap==5.0.3" - ], + "requirements": ["coinmarketcap==5.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/comed_hourly_pricing/manifest.json b/homeassistant/components/comed_hourly_pricing/manifest.json index 89fbb84e82d..27698a7b94a 100644 --- a/homeassistant/components/comed_hourly_pricing/manifest.json +++ b/homeassistant/components/comed_hourly_pricing/manifest.json @@ -1,6 +1,6 @@ { "domain": "comed_hourly_pricing", - "name": "Comed hourly pricing", + "name": "ComEd Hourly Pricing", "documentation": "https://www.home-assistant.io/integrations/comed_hourly_pricing", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 091b7f7bcdd..c55b6895e80 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -1,10 +1,8 @@ { "domain": "comfoconnect", - "name": "Comfoconnect", + "name": "Zehnder ComfoAir Q", "documentation": "https://www.home-assistant.io/integrations/comfoconnect", - "requirements": [ - "pycomfoconnect==0.3" - ], + "requirements": ["pycomfoconnect==0.3"], "dependencies": [], "codeowners": ["@michaelarnauts"] } diff --git a/homeassistant/components/command_line/cover.py b/homeassistant/components/command_line/cover.py index c4413e78a00..1d996614caa 100644 --- a/homeassistant/components/command_line/cover.py +++ b/homeassistant/components/command_line/cover.py @@ -4,15 +4,15 @@ import subprocess import voluptuous as vol -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import ( CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, CONF_COMMAND_STATE, CONF_COMMAND_STOP, CONF_COVERS, - CONF_VALUE_TEMPLATE, CONF_FRIENDLY_NAME, + CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/command_line/manifest.json b/homeassistant/components/command_line/manifest.json index 4d7dfc8994f..9d625ebcc7e 100644 --- a/homeassistant/components/command_line/manifest.json +++ b/homeassistant/components/command_line/manifest.json @@ -1,6 +1,6 @@ { "domain": "command_line", - "name": "Command line", + "name": "Command Line", "documentation": "https://www.home-assistant.io/integrations/command_line", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/command_line/notify.py b/homeassistant/components/command_line/notify.py index e2581c8f065..21653171f34 100644 --- a/homeassistant/components/command_line/notify.py +++ b/homeassistant/components/command_line/notify.py @@ -4,11 +4,10 @@ import subprocess import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_COMMAND, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/command_line/switch.py b/homeassistant/components/command_line/switch.py index 937e859197a..62dcbe2f15a 100644 --- a/homeassistant/components/command_line/switch.py +++ b/homeassistant/components/command_line/switch.py @@ -4,21 +4,20 @@ import subprocess import voluptuous as vol -import homeassistant.helpers.config_validation as cv - from homeassistant.components.switch import ( - SwitchDevice, - PLATFORM_SCHEMA, ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SwitchDevice, ) from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_SWITCHES, - CONF_VALUE_TEMPLATE, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_COMMAND_STATE, + CONF_FRIENDLY_NAME, + CONF_SWITCHES, + CONF_VALUE_TEMPLATE, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/concord232/manifest.json b/homeassistant/components/concord232/manifest.json index f5ff021b6d1..e0060490cfe 100644 --- a/homeassistant/components/concord232/manifest.json +++ b/homeassistant/components/concord232/manifest.json @@ -2,9 +2,7 @@ "domain": "concord232", "name": "Concord232", "documentation": "https://www.home-assistant.io/integrations/concord232", - "requirements": [ - "concord232==0.15" - ], + "requirements": ["concord232==0.15"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index 5a66c1fc5d4..5873cdc3271 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -6,11 +6,11 @@ import os import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.const import EVENT_COMPONENT_LOADED, CONF_ID +from homeassistant.const import CONF_ID, EVENT_COMPONENT_LOADED from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import ATTR_COMPONENT -from homeassistant.util.yaml import load_yaml, dump +from homeassistant.util.yaml import dump, load_yaml DOMAIN = "config" SECTIONS = ( diff --git a/homeassistant/components/config/area_registry.py b/homeassistant/components/config/area_registry.py index 9c8853ac782..81daf35339e 100644 --- a/homeassistant/components/config/area_registry.py +++ b/homeassistant/components/config/area_registry.py @@ -9,7 +9,6 @@ from homeassistant.components.websocket_api.decorators import ( from homeassistant.core import callback from homeassistant.helpers.area_registry import async_get_registry - WS_TYPE_LIST = "config/area_registry/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( {vol.Required("type"): WS_TYPE_LIST} diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 977bae36083..361367ffb4d 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -3,7 +3,6 @@ import voluptuous as vol from homeassistant.components import websocket_api - WS_TYPE_LIST = "config/auth/list" SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( {vol.Required("type"): WS_TYPE_LIST} diff --git a/homeassistant/components/config/auth_provider_homeassistant.py b/homeassistant/components/config/auth_provider_homeassistant.py index 817675db238..dec7fb24d27 100644 --- a/homeassistant/components/config/auth_provider_homeassistant.py +++ b/homeassistant/components/config/auth_provider_homeassistant.py @@ -4,7 +4,6 @@ import voluptuous as vol from homeassistant.auth.providers import homeassistant as auth_ha from homeassistant.components import websocket_api - WS_TYPE_CREATE = "config/auth_provider/homeassistant/create" SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( { diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 0e9b4053b7b..d7bb1ef9883 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -4,8 +4,8 @@ import uuid from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.automation.config import async_validate_config_item -from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.config import AUTOMATION_CONFIG_PATH +from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 81065665e34..22df26cce4e 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -23,16 +23,13 @@ async def async_setup(hass): hass.http.register_view(ConfigManagerFlowResourceView(hass.config_entries.flow)) hass.http.register_view(ConfigManagerAvailableFlowView) - hass.http.register_view( - OptionManagerFlowIndexView(hass.config_entries.options.flow) - ) - hass.http.register_view( - OptionManagerFlowResourceView(hass.config_entries.options.flow) - ) + hass.http.register_view(OptionManagerFlowIndexView(hass.config_entries.options)) + hass.http.register_view(OptionManagerFlowResourceView(hass.config_entries.options)) hass.components.websocket_api.async_register_command(config_entries_progress) hass.components.websocket_api.async_register_command(system_options_list) hass.components.websocket_api.async_register_command(system_options_update) + hass.components.websocket_api.async_register_command(ignore_config_flow) return True @@ -284,3 +281,37 @@ async def system_options_update(hass, connection, msg): hass.config_entries.async_update_entry(entry, system_options=changes) connection.send_result(msg["id"], entry.system_options.as_dict()) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({"type": "config_entries/ignore_flow", "flow_id": str}) +async def ignore_config_flow(hass, connection, msg): + """Ignore a config flow.""" + flow = next( + ( + flw + for flw in hass.config_entries.flow.async_progress() + if flw["flow_id"] == msg["flow_id"] + ), + None, + ) + + if flow is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" + ) + return + + if "unique_id" not in flow["context"]: + connection.send_error( + msg["id"], "no_unique_id", "Specified flow has no unique ID." + ) + return + + await hass.config_entries.flow.async_init( + flow["handler"], + context={"source": config_entries.SOURCE_IGNORE}, + data={"unique_id": flow["context"]["unique_id"]}, + ) + connection.send_result(msg["id"]) diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py index 073f8f23d6c..e9ceb7eac57 100644 --- a/homeassistant/components/config/core.py +++ b/homeassistant/components/config/core.py @@ -2,10 +2,10 @@ import voluptuous as vol +from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.config import async_check_ha_config_file -from homeassistant.components import websocket_api -from homeassistant.const import CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL +from homeassistant.const import CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC from homeassistant.helpers import config_validation as cv from homeassistant.util import location diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 125b2260f08..458a9dd3ecb 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -1,15 +1,15 @@ """HTTP views to interact with the entity registry.""" import voluptuous as vol -from homeassistant.core import callback -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.components import websocket_api from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.websocket_api.decorators import ( async_response, require_admin, ) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_get_registry async def async_setup(hass): diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index d104cd2e1df..d95891af655 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -1,7 +1,7 @@ """Provide configuration end points for Groups.""" from homeassistant.components.group import DOMAIN, GROUP_SCHEMA -from homeassistant.const import SERVICE_RELOAD from homeassistant.config import GROUP_CONFIG_PATH +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView diff --git a/homeassistant/components/config/manifest.json b/homeassistant/components/config/manifest.json index c6e99b43c49..809db4ffecc 100644 --- a/homeassistant/components/config/manifest.json +++ b/homeassistant/components/config/manifest.json @@ -3,10 +3,7 @@ "name": "Config", "documentation": "https://www.home-assistant.io/integrations/config", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/config/scene.py b/homeassistant/components/config/scene.py index 6e77dae0826..79a30177e47 100644 --- a/homeassistant/components/config/scene.py +++ b/homeassistant/components/config/scene.py @@ -3,8 +3,8 @@ from collections import OrderedDict import uuid from homeassistant.components.scene import DOMAIN, PLATFORM_SCHEMA -from homeassistant.const import CONF_ID, SERVICE_RELOAD from homeassistant.config import SCENE_CONFIG_PATH +from homeassistant.const import CONF_ID, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index e63651d8f2a..032774de473 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,7 +1,7 @@ """Provide configuration end points for scripts.""" from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA -from homeassistant.const import SERVICE_RELOAD from homeassistant.config import SCRIPT_CONFIG_PATH +from homeassistant.const import SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index f3b2a41e917..78333d96355 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -9,14 +9,14 @@ the user has submitted configuration information. import functools as ft import logging -from homeassistant.core import callback as async_callback from homeassistant.const import ( - EVENT_TIME_CHANGED, - ATTR_FRIENDLY_NAME, ATTR_ENTITY_PICTURE, + ATTR_FRIENDLY_NAME, + EVENT_TIME_CHANGED, ) -from homeassistant.loader import bind_hass +from homeassistant.core import callback as async_callback from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/configurator/manifest.json b/homeassistant/components/configurator/manifest.json index 10c067d4a22..56079887450 100644 --- a/homeassistant/components/configurator/manifest.json +++ b/homeassistant/components/configurator/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/configurator", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index ec5868e86fe..158a365981b 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -11,7 +11,7 @@ from homeassistant.helpers import config_validation as cv, intent from homeassistant.loader import bind_hass from .agent import AbstractConversationAgent -from .default_agent import async_register, DefaultAgent +from .default_agent import DefaultAgent, async_register _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 0d6d67cf254..7e2decb2bff 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -3,10 +3,7 @@ "name": "Conversation", "documentation": "https://www.home-assistant.io/integrations/conversation", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/coolmaster/.translations/da.json b/homeassistant/components/coolmaster/.translations/da.json index 8f50a0eb6dd..882bc5de359 100644 --- a/homeassistant/components/coolmaster/.translations/da.json +++ b/homeassistant/components/coolmaster/.translations/da.json @@ -1,9 +1,17 @@ { "config": { + "error": { + "connection_error": "Kunne ikke oprette forbindelse til CoolMasterNet-instansen. Tjek din v\u00e6rt.", + "no_units": "Kunne ikke finde nogen klimaanl\u00e6g i CoolMasterNet-v\u00e6rt." + }, "step": { "user": { "data": { - "heat_cool": "Underst\u00f8t automatisk varm/k\u00f8l tilstand", + "cool": "Underst\u00f8tter k\u00f8lingstilstand", + "dry": "Underst\u00f8tter t\u00f8rringstilstand", + "fan_only": "Underst\u00f8tter kun-bl\u00e6ser-tilstand", + "heat": "Underst\u00f8tter varmetilstand", + "heat_cool": "Underst\u00f8tter automatisk varm/k\u00f8l-tilstand", "host": "V\u00e6rt", "off": "Kan slukkes" }, diff --git a/homeassistant/components/coolmaster/.translations/de.json b/homeassistant/components/coolmaster/.translations/de.json index 66c6911cf10..c312de14935 100644 --- a/homeassistant/components/coolmaster/.translations/de.json +++ b/homeassistant/components/coolmaster/.translations/de.json @@ -1,11 +1,23 @@ { "config": { + "error": { + "connection_error": "Verbindung zur CoolMasterNet-Instanz fehlgeschlagen. Bitte \u00fcberpr\u00fcfen Sie Ihren Host.", + "no_units": "Es wurden keine HVAC-Ger\u00e4te im CoolMasterNet-Host gefunden." + }, "step": { "user": { "data": { + "cool": "Unterst\u00fctzt K\u00fchl-Modus", + "dry": "Unterst\u00fctzt Trockenmodus", + "fan_only": "Unterst\u00fctzt Fan-Only-Modus", + "heat": "Unterst\u00fctzt Heiz-Modus", + "heat_cool": "Unterst\u00fctzung automatische Heiz-/K\u00fchlmodus", + "host": "Host", "off": "Kann ausgeschaltet werden" - } + }, + "title": "Richten Sie Ihre CoolMasterNet-Verbindungsdaten ein." } - } + }, + "title": "CoolMasterNet" } } \ No newline at end of file diff --git a/homeassistant/components/coolmaster/.translations/ko.json b/homeassistant/components/coolmaster/.translations/ko.json index ff6ddf0acfe..4d96e606c7b 100644 --- a/homeassistant/components/coolmaster/.translations/ko.json +++ b/homeassistant/components/coolmaster/.translations/ko.json @@ -13,7 +13,7 @@ "heat": "\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", "heat_cool": "\uc790\ub3d9 \ub0c9/\ub09c\ubc29 \ubaa8\ub4dc \uc9c0\uc6d0", "host": "\ud638\uc2a4\ud2b8", - "off": "\uc804\uc6d0\uc744 \ub04c \uc218 \uc788\uc2b4" + "off": "\uc804\uc6d0 \ub044\uae30 \ud5c8\uc6a9" }, "title": "CoolMasterNet \uc5f0\uacb0 \uc0c1\uc138\uc815\ubcf4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694." } diff --git a/homeassistant/components/coolmaster/.translations/nl.json b/homeassistant/components/coolmaster/.translations/nl.json index 02b65cdfff9..e5b1683790f 100644 --- a/homeassistant/components/coolmaster/.translations/nl.json +++ b/homeassistant/components/coolmaster/.translations/nl.json @@ -1,11 +1,18 @@ { "config": { "error": { - "connection_error": "Kan geen verbinding maken met CoolMasterNet-instantie. Controleer uw host" + "connection_error": "Kan geen verbinding maken met CoolMasterNet-instantie. Controleer uw host", + "no_units": "Kon geen HVAC units vinden in CoolMasterNet host." }, "step": { "user": { "data": { + "cool": "Ondersteuning afkoelen modus", + "dry": "Ondersteuning droog modus", + "fan_only": "Ondersteunt alleen ventilatormodus", + "heat": "Ondersteuning warmtemodus", + "heat_cool": "Ondersteuning van automatische warmte/koelmodus", + "host": "Host", "off": "Kan uitgeschakeld worden" }, "title": "Stel uw CoolMasterNet-verbindingsgegevens in." diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py index fe52ea17b28..e9cef562647 100644 --- a/homeassistant/components/coolmaster/config_flow.py +++ b/homeassistant/components/coolmaster/config_flow.py @@ -3,7 +3,7 @@ from pycoolmasternet import CoolMasterNet import voluptuous as vol -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.const import CONF_HOST, CONF_PORT # pylint: disable=unused-import diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 124a1e4a5b9..0041895a290 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -1,13 +1,9 @@ { "domain": "coolmaster", - "name": "Coolmaster", + "name": "CoolMasterNet", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", - "requirements": [ - "pycoolmasternet==0.0.4" - ], + "requirements": ["pycoolmasternet==0.0.4"], "dependencies": [], - "codeowners": [ - "@OnFreund" - ] + "codeowners": ["@OnFreund"] } diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index c2f61d0c1b4..5580518a9a3 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -3,8 +3,7 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME, CONF_MAXIMUM, CONF_MINIMUM - +from homeassistant.const import CONF_ICON, CONF_MAXIMUM, CONF_MINIMUM, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity @@ -33,10 +32,17 @@ SERVICE_RESET = "reset" SERVICE_CONFIGURE = "configure" +def _none_to_empty_dict(value): + if value is None: + return {} + return value + + CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( - vol.Any( + vol.All( + _none_to_empty_dict, { vol.Optional(CONF_ICON): cv.icon, vol.Optional( @@ -52,7 +58,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_RESTORE, default=True): cv.boolean, vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int, }, - None, ) ) }, @@ -71,12 +76,12 @@ async def async_setup(hass, config): cfg = {} name = cfg.get(CONF_NAME) - initial = cfg.get(CONF_INITIAL) - restore = cfg.get(CONF_RESTORE) - step = cfg.get(CONF_STEP) + initial = cfg[CONF_INITIAL] + restore = cfg[CONF_RESTORE] + step = cfg[CONF_STEP] icon = cfg.get(CONF_ICON) - minimum = cfg.get(CONF_MINIMUM) - maximum = cfg.get(CONF_MAXIMUM) + minimum = cfg[CONF_MINIMUM] + maximum = cfg[CONF_MAXIMUM] entities.append( Counter(object_id, name, initial, minimum, maximum, restore, step, icon) diff --git a/homeassistant/components/counter/manifest.json b/homeassistant/components/counter/manifest.json index 3fd533054d8..f22c7b252df 100644 --- a/homeassistant/components/counter/manifest.json +++ b/homeassistant/components/counter/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/counter", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py index ac5045d68e7..b37fcea719e 100644 --- a/homeassistant/components/counter/reproduce_state.py +++ b/homeassistant/components/counter/reproduce_state.py @@ -12,9 +12,9 @@ from . import ( ATTR_MAXIMUM, ATTR_MINIMUM, ATTR_STEP, - VALUE, DOMAIN, SERVICE_CONFIGURE, + VALUE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json index e603723b564..64b89be5267 100644 --- a/homeassistant/components/cover/.translations/da.json +++ b/homeassistant/components/cover/.translations/da.json @@ -4,7 +4,17 @@ "is_closed": "{entity_name} er lukket", "is_closing": "{entity_name} lukker", "is_open": "{entity_name} er \u00e5ben", - "is_opening": "{entity_name} \u00e5bnes" + "is_opening": "{entity_name} \u00e5bnes", + "is_position": "Aktuel {entity_name} position er", + "is_tilt_position": "Aktuel {entity_name} vippeposition er" + }, + "trigger_type": { + "closed": "{entity_name} lukket", + "closing": "{entity_name} lukning", + "opened": "{entity_name} \u00e5bnet", + "opening": "{entity_name} \u00e5bning", + "position": "{entity_name} position \u00e6ndres", + "tilt_position": "{entity_name} vippeposition \u00e6ndres" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json index ba692f15e47..24589c733b8 100644 --- a/homeassistant/components/cover/.translations/de.json +++ b/homeassistant/components/cover/.translations/de.json @@ -4,13 +4,17 @@ "is_closed": "{entity_name} ist geschlossen", "is_closing": "{entity_name} wird geschlossen", "is_open": "{entity_name} ist offen", - "is_opening": "{entity_name} wird ge\u00f6ffnet" + "is_opening": "{entity_name} wird ge\u00f6ffnet", + "is_position": "Die Aktuelle Position von {entity_name} ist", + "is_tilt_position": "Die Aktuelle Neigungsposition von {entity_name} ist" }, "trigger_type": { "closed": "{entity_name} geschlossen", "closing": "{entity_name} wird geschlossen", "opened": "{entity_name} ge\u00f6ffnet", - "opening": "{entity_name} wird ge\u00f6ffnet" + "opening": "{entity_name} wird ge\u00f6ffnet", + "position": "{entity_name} ver\u00e4ndert die Position", + "tilt_position": "{entity_name} ver\u00e4ndert die Neigungsposition" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/hu.json b/homeassistant/components/cover/.translations/hu.json new file mode 100644 index 00000000000..d460c53109d --- /dev/null +++ b/homeassistant/components/cover/.translations/hu.json @@ -0,0 +1,20 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} z\u00e1rva van", + "is_closing": "{entity_name} z\u00e1r\u00f3dik", + "is_open": "{entity_name} nyitva van", + "is_opening": "{entity_name} ny\u00edlik", + "is_position": "{entity_name} jelenlegi poz\u00edci\u00f3ja", + "is_tilt_position": "{entity_name} jelenlegi d\u00f6nt\u00e9si poz\u00edci\u00f3ja" + }, + "trigger_type": { + "closed": "{entity_name} bez\u00e1r\u00f3dott", + "closing": "{entity_name} z\u00e1r\u00f3dik", + "opened": "{entity_name} kiny\u00edlt", + "opening": "{entity_name} ny\u00edlik", + "position": "{entity_name} poz\u00edci\u00f3ja v\u00e1ltozik", + "tilt_position": "{entity_name} d\u00f6nt\u00e9si poz\u00edci\u00f3ja v\u00e1ltozik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 6a59bb9f6ae..145938b6f24 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -1,20 +1,20 @@ { "device_automation": { "condition_type": { - "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", - "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", - "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", - "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4", - "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58", - "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30" + "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", + "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc774\uba74", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", + "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc774\uba74", + "is_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uc704\uce58\uac00 ~ \uc774\uba74", + "is_tilt_position": "\ud604\uc7ac {entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 ~ \uc774\uba74" }, "trigger_type": { - "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", - "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911", - "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", - "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911", - "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58 \ubcc0\ud654", - "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30 \ubcc0\ud654" + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", + "closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc77c \ub54c", + "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c", + "opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9ac\ub294 \uc911\uc77c \ub54c", + "position": "{entity_name} \uac1c\ud3d0 \uc704\uce58\uac00 \ubcc0\ud560 \ub54c", + "tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30\uac00 \ubcc0\ud560 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json index 043e5a42d2a..ebe81486cf5 100644 --- a/homeassistant/components/cover/.translations/ru.json +++ b/homeassistant/components/cover/.translations/ru.json @@ -9,7 +9,9 @@ "is_tilt_position": "{entity_name} \u0432 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438 \u043d\u0430\u043a\u043b\u043e\u043d\u0430" }, "trigger_type": { + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0442\u043e", "closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0442\u043e", "opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435", "tilt_position": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u043d\u0430\u043a\u043b\u043e\u043d" diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 0b8fbfa9dd2..2fe4022fb39 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -6,31 +6,29 @@ from typing import Any import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, + SERVICE_TOGGLE, + SERVICE_TOGGLE_COVER_TILT, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.components import group -from homeassistant.const import ( - SERVICE_OPEN_COVER, - SERVICE_CLOSE_COVER, - SERVICE_SET_COVER_POSITION, - SERVICE_STOP_COVER, - SERVICE_TOGGLE, - SERVICE_OPEN_COVER_TILT, - SERVICE_CLOSE_COVER_TILT, - SERVICE_STOP_COVER_TILT, - SERVICE_SET_COVER_TILT_POSITION, - SERVICE_TOGGLE_COVER_TILT, - STATE_OPEN, - STATE_CLOSED, - STATE_OPENING, - STATE_CLOSING, -) - +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -39,9 +37,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "cover" SCAN_INTERVAL = timedelta(seconds=15) -GROUP_NAME_ALL_COVERS = "all covers" -ENTITY_ID_ALL_COVERS = group.ENTITY_ID_FORMAT.format("all_covers") - ENTITY_ID_FORMAT = DOMAIN + ".{}" # Refer to the cover dev docs for device class descriptions @@ -83,16 +78,15 @@ ATTR_TILT_POSITION = "tilt_position" @bind_hass -def is_closed(hass, entity_id=None): +def is_closed(hass, entity_id): """Return if the cover is closed based on the statemachine.""" - entity_id = entity_id or ENTITY_ID_ALL_COVERS return hass.states.is_state(entity_id, STATE_CLOSED) async def async_setup(hass, config): """Track states and offer events for covers.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_COVERS + _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py index 487f815afb5..ec6da84e5f6 100644 --- a/homeassistant/components/cover/device_condition.py +++ b/homeassistant/components/cover/device_condition.py @@ -1,5 +1,6 @@ """Provides device automations for Cover.""" from typing import Any, Dict, List + import voluptuous as vol from homeassistant.const import ( @@ -8,14 +9,14 @@ from homeassistant.const import ( CONF_ABOVE, CONF_BELOW, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_OPEN, + CONF_TYPE, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import ( @@ -24,8 +25,9 @@ from homeassistant.helpers import ( entity_registry, template, ) -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import ( DOMAIN, SUPPORT_CLOSE, diff --git a/homeassistant/components/cover/device_trigger.py b/homeassistant/components/cover/device_trigger.py index 4f256a87dc5..988427003e7 100644 --- a/homeassistant/components/cover/device_trigger.py +++ b/homeassistant/components/cover/device_trigger.py @@ -1,30 +1,32 @@ """Provides device automations for Cover.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import ( + AutomationActionType, + numeric_state as numeric_state_automation, + state as state_automation, +) +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_ABOVE, CONF_BELOW, - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import ( - state as state_automation, - numeric_state as numeric_state_automation, - AutomationActionType, -) -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import ( DOMAIN, SUPPORT_CLOSE, diff --git a/homeassistant/components/cover/intent.py b/homeassistant/components/cover/intent.py index f8d13e6a90e..36402025bfa 100644 --- a/homeassistant/components/cover/intent.py +++ b/homeassistant/components/cover/intent.py @@ -2,7 +2,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -from . import DOMAIN, SERVICE_OPEN_COVER, SERVICE_CLOSE_COVER +from . import DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER INTENT_OPEN_COVER = "HassOpenCover" INTENT_CLOSE_COVER = "HassCloseCover" diff --git a/homeassistant/components/cover/manifest.json b/homeassistant/components/cover/manifest.json index 1d82dcb5b0c..aa43e934dc9 100644 --- a/homeassistant/components/cover/manifest.json +++ b/homeassistant/components/cover/manifest.json @@ -3,10 +3,7 @@ "name": "Cover", "documentation": "https://www.home-assistant.io/integrations/cover", "requirements": [], - "dependencies": [ - "group" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["group"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/cppm_tracker/manifest.json b/homeassistant/components/cppm_tracker/manifest.json index b2abc40dca2..8407aee07d5 100644 --- a/homeassistant/components/cppm_tracker/manifest.json +++ b/homeassistant/components/cppm_tracker/manifest.json @@ -1,10 +1,8 @@ { "domain": "cppm_tracker", - "name": "Cppm tracker", + "name": "Aruba ClearPass", "documentation": "https://www.home-assistant.io/integrations/cppm_tracker", - "requirements": [ - "clearpasspy==1.0.2" - ], + "requirements": ["clearpasspy==1.0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/cpuspeed/manifest.json b/homeassistant/components/cpuspeed/manifest.json index 7950cad9b8b..7e8f44648f1 100644 --- a/homeassistant/components/cpuspeed/manifest.json +++ b/homeassistant/components/cpuspeed/manifest.json @@ -1,12 +1,8 @@ { "domain": "cpuspeed", - "name": "Cpuspeed", + "name": "CPU Speed", "documentation": "https://www.home-assistant.io/integrations/cpuspeed", - "requirements": [ - "py-cpuinfo==5.0.0" - ], + "requirements": ["py-cpuinfo==5.0.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/crimereports/manifest.json b/homeassistant/components/crimereports/manifest.json index c5cc45d3192..6d64c313039 100644 --- a/homeassistant/components/crimereports/manifest.json +++ b/homeassistant/components/crimereports/manifest.json @@ -1,10 +1,8 @@ { "domain": "crimereports", - "name": "Crimereports", + "name": "Crime Reports", "documentation": "https://www.home-assistant.io/integrations/crimereports", - "requirements": [ - "crimereports==1.0.1" - ], + "requirements": ["crimereports==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/cups/manifest.json b/homeassistant/components/cups/manifest.json index e30d64510ff..d9b193e6dc6 100644 --- a/homeassistant/components/cups/manifest.json +++ b/homeassistant/components/cups/manifest.json @@ -1,12 +1,8 @@ { "domain": "cups", - "name": "Cups", + "name": "CUPS", "documentation": "https://www.home-assistant.io/integrations/cups", - "requirements": [ - "pycups==1.9.73" - ], + "requirements": ["pycups==1.9.73"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index 17a246561a5..7581891af6a 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -1,14 +1,14 @@ """Details about printers which are connected to CUPS.""" +from datetime import timedelta import importlib import logging -from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/currencylayer/manifest.json b/homeassistant/components/currencylayer/manifest.json index 2db35dead60..162091de9ad 100644 --- a/homeassistant/components/currencylayer/manifest.json +++ b/homeassistant/components/currencylayer/manifest.json @@ -1,6 +1,6 @@ { "domain": "currencylayer", - "name": "Currencylayer", + "name": "currencylayer", "documentation": "https://www.home-assistant.io/integrations/currencylayer", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/currencylayer/sensor.py b/homeassistant/components/currencylayer/sensor.py index d4660d70286..cbad07c0284 100644 --- a/homeassistant/components/currencylayer/sensor.py +++ b/homeassistant/components/currencylayer/sensor.py @@ -5,15 +5,15 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_BASE, - CONF_QUOTE, ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_BASE, + CONF_NAME, + CONF_QUOTE, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index 00a517f701f..c9ab31597d7 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_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 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index f81cb171580..52d1b516d32 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -1,14 +1,10 @@ { "domain": "daikin", - "name": "Daikin", + "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": [ - "pydaikin==1.6.1" - ], + "requirements": ["pydaikin==1.6.1"], "dependencies": [], - "codeowners": [ - "@fredrike", - "@rofrantz" - ] + "codeowners": ["@fredrike", "@rofrantz"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/danfoss_air/manifest.json b/homeassistant/components/danfoss_air/manifest.json index 189b685d4ce..bbfbd3791b2 100644 --- a/homeassistant/components/danfoss_air/manifest.json +++ b/homeassistant/components/danfoss_air/manifest.json @@ -1,10 +1,8 @@ { "domain": "danfoss_air", - "name": "Danfoss air", + "name": "Danfoss Air", "documentation": "https://www.home-assistant.io/integrations/danfoss_air", - "requirements": [ - "pydanfossair==0.1.0" - ], + "requirements": ["pydanfossair==0.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/darksky/manifest.json b/homeassistant/components/darksky/manifest.json index 0046e51463e..94123ceba85 100644 --- a/homeassistant/components/darksky/manifest.json +++ b/homeassistant/components/darksky/manifest.json @@ -1,12 +1,8 @@ { "domain": "darksky", - "name": "Darksky", + "name": "Dark Sky", "documentation": "https://www.home-assistant.io/integrations/darksky", - "requirements": [ - "python-forecastio==1.4.0" - ], + "requirements": ["python-forecastio==1.4.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index cd8417e3e84..9f99b37a201 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -1,12 +1,11 @@ """Support for Dark Sky weather service.""" -import logging from datetime import timedelta +import logging import forecastio -import voluptuous as vol from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -15,9 +14,10 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_NAME, - UNIT_UV_INDEX, CONF_SCAN_INTERVAL, + UNIT_UV_INDEX, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -385,7 +385,7 @@ CONDITION_PICTURES = { ], "partly-cloudy-night": [ "/static/images/darksky/weather-cloudy.svg", - "mdi:weather-partly-cloudy", + "mdi:weather-night-partly-cloudy", ], } @@ -750,7 +750,7 @@ class DarkSkyAlertSensor(Entity): for i, alert in enumerate(data): for attr in ALERTS_ATTRS: if multiple_alerts: - dkey = attr + "_" + str(i) + dkey = f"{attr}_{i!s}" else: dkey = attr alerts[dkey] = getattr(alert, attr) diff --git a/homeassistant/components/datadog/manifest.json b/homeassistant/components/datadog/manifest.json index 36de2ff2101..4df780b200f 100644 --- a/homeassistant/components/datadog/manifest.json +++ b/homeassistant/components/datadog/manifest.json @@ -2,9 +2,7 @@ "domain": "datadog", "name": "Datadog", "documentation": "https://www.home-assistant.io/integrations/datadog", - "requirements": [ - "datadog==0.15.0" - ], + "requirements": ["datadog==0.15.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ddwrt/manifest.json b/homeassistant/components/ddwrt/manifest.json index 874b24e370b..d50fd262729 100644 --- a/homeassistant/components/ddwrt/manifest.json +++ b/homeassistant/components/ddwrt/manifest.json @@ -1,6 +1,6 @@ { "domain": "ddwrt", - "name": "Ddwrt", + "name": "DD-WRT", "documentation": "https://www.home-assistant.io/integrations/ddwrt", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index ec9c4dc35b1..ed1f0b06e64 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Bridge er allerede konfigureret", - "already_in_progress": "Bro konfiguration er allerede i gang.", - "no_bridges": "Ingen deConz bridge fundet", - "not_deconz_bridge": "Ikke en deCONZ bro", - "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ forekomst", - "updated_instance": "Opdaterede deCONZ instans med ny v\u00e6rtsadresse" + "already_in_progress": "Konfigurationsflow for bro er allerede i gang.", + "no_bridges": "Ingen deConz-bridge fundet", + "not_deconz_bridge": "Ikke en deCONZ-bro", + "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n deCONZ-instans", + "updated_instance": "Opdaterede deCONZ-instans med ny v\u00e6rtadresse" }, "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8gle" @@ -16,28 +16,28 @@ "hassio_confirm": { "data": { "allow_clip_sensor": "Tillad import af virtuelle sensorer", - "allow_deconz_groups": "Tillad import af deCONZ grupper" + "allow_deconz_groups": "Tillad import af deCONZ-grupper" }, - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ gateway leveret af Hass.io add-on {addon}?", - "title": "deCONZ Zigbee-gateway via Hass.io add-on" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ-gateway'en leveret af Hass.io-tilf\u00f8jelsen {addon}?", + "title": "deCONZ Zigbee-gateway via Hass.io-tilf\u00f8jelse" }, "init": { "data": { "host": "V\u00e6rt", "port": "Port" }, - "title": "Definer deCONZ gateway" + "title": "Definer deCONZ-gateway" }, "link": { "description": "L\u00e5s din deCONZ-gateway op for at registrere dig med Home Assistant. \n\n 1. G\u00e5 til deCONZ settings -> Gateway -> Advanced\n 2. Tryk p\u00e5 knappen \"Authenticate app\"", - "title": "Link med deCONZ" + "title": "Forbind med deCONZ" }, "options": { "data": { "allow_clip_sensor": "Tillad import af virtuelle sensorer", - "allow_deconz_groups": "Tillad importering af deCONZ grupper" + "allow_deconz_groups": "Tillad import af deCONZ-grupper" }, - "title": "Ekstra konfiguration valgmuligheder for deCONZ" + "title": "Ekstra konfigurationsindstillinger for deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -54,25 +54,59 @@ "dim_up": "D\u00e6mp op", "left": "Venstre", "open": "\u00c5ben", - "right": "H\u00f8jre" + "right": "H\u00f8jre", + "side_1": "Side 1", + "side_2": "Side 2", + "side_3": "Side 3", + "side_4": "Side 4", + "side_5": "Side 5", + "side_6": "Side 6", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" }, "trigger_type": { - "remote_gyro_activated": "Enhed rystet" + "remote_awakened": "Enheden v\u00e6kket", + "remote_button_double_press": "\"{subtype}\"-knappen er dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen trykket p\u00e5 konstant", + "remote_button_long_release": "\"{subtype}\"-knappen frigivet efter langt tryk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen firedobbelt-klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt-klikket", + "remote_button_rotated": "Knap roteret \"{subtype}\"", + "remote_button_rotation_stopped": "Knaprotation \"{subtype}\" er stoppet", + "remote_button_short_press": "\"{subtype}\"-knappen trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knappen frigivet", + "remote_button_triple_press": "\"{subtype}\"-knappen tredobbeltklikkes", + "remote_double_tap": "Enheden \"{subtype}\" dobbelttappet", + "remote_double_tap_any_side": "Enhed dobbelttappet p\u00e5 enhver side", + "remote_falling": "Enheden er i frit fald", + "remote_flip_180_degrees": "Enhed vendt 180 grader", + "remote_flip_90_degrees": "Enhed vendt 90 grader", + "remote_gyro_activated": "Enhed rystet", + "remote_moved": "Enheden flyttede med \"{subtype}\" op", + "remote_moved_any_side": "Enhed flyttet med enhver side opad", + "remote_rotate_from_side_1": "Enhed roteret fra \"side 1\" til \"{subtype}\"", + "remote_rotate_from_side_2": "Enhed roteret fra \"side 2\" til \"{subtype}\"", + "remote_rotate_from_side_3": "Enhed roteret fra \"side 3\" til \"{subtype}\"", + "remote_rotate_from_side_4": "Enhed roteret fra \"side 4\" til \"{subtype}\"", + "remote_rotate_from_side_5": "Enhed roteret fra \"side 5\" til \"{subtype}\"", + "remote_rotate_from_side_6": "Enhed roteret fra \"side 6\" til \"{subtype}\"", + "remote_turned_clockwise": "Enhed drejet med uret", + "remote_turned_counter_clockwise": "Enhed drejet mod uret" } }, "options": { "step": { "async_step_deconz_devices": { "data": { - "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", - "allow_deconz_groups": "Tillad deCONZ lys grupper" + "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillad deCONZ-lysgrupper" }, "description": "Konfigurer synligheden af deCONZ-enhedstyper" }, "deconz_devices": { "data": { - "allow_clip_sensor": "Tillad deCONZ CLIP sensorer", - "allow_deconz_groups": "Tillad deCONZ lys grupper" + "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", + "allow_deconz_groups": "Tillad deCONZ-lysgrupper" }, "description": "Konfigurer synligheden af deCONZ-enhedstyper" } diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 2bf0667cadb..d177448f4fd 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -55,10 +55,17 @@ "left": "Links", "open": "Offen", "right": "Rechts", + "side_1": "Seite 1", + "side_2": "Seite 2", + "side_3": "Seite 3", + "side_4": "Seite 4", + "side_5": "Seite 5", + "side_6": "Seite 6", "turn_off": "Ausschalten", "turn_on": "Einschalten" }, "trigger_type": { + "remote_awakened": "Ger\u00e4t aufgeweckt", "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", @@ -69,7 +76,15 @@ "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", "remote_button_short_release": "\"{subtype}\" Taste losgelassen", "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt", - "remote_gyro_activated": "Ger\u00e4t ersch\u00fcttert" + "remote_double_tap": "Ger\u00e4t \"{subtype}\" doppelt getippt", + "remote_falling": "Ger\u00e4t im freien Fall", + "remote_gyro_activated": "Ger\u00e4t ersch\u00fcttert", + "remote_rotate_from_side_1": "Ger\u00e4t von \"Seite 1\" auf \"{subtype}\" gedreht", + "remote_rotate_from_side_2": "Ger\u00e4t von \"Seite 2\" auf \"{subtype}\" gedreht", + "remote_rotate_from_side_3": "Ger\u00e4t von \"Seite 3\" auf \"{subtype}\" gedreht", + "remote_rotate_from_side_4": "Ger\u00e4t von \"Seite 4\" auf \"{subtype}\" gedreht", + "remote_rotate_from_side_5": "Ger\u00e4t von \"Seite 5\" auf \"{subtype}\" gedreht", + "remote_rotate_from_side_6": "Ger\u00e4t von \"Seite 6\" auf \"{subtype}\" gedreht" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 63798bab52b..b3d9e00bfe6 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -18,7 +18,7 @@ "allow_clip_sensor": "Allow importing virtual sensors", "allow_deconz_groups": "Allow importing deCONZ groups" }, - "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the hass.io add-on {addon}?", + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?", "title": "deCONZ Zigbee gateway via Hass.io add-on" }, "init": { @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" button released", "remote_button_triple_press": "\"{subtype}\" button triple clicked", "remote_double_tap": "Device \"{subtype}\" double tapped", + "remote_double_tap_any_side": "Device double tapped on any side", "remote_falling": "Device in free fall", + "remote_flip_180_degrees": "Device flipped 180 degrees", + "remote_flip_90_degrees": "Device flipped 90 degrees", "remote_gyro_activated": "Device shaken", "remote_moved": "Device moved with \"{subtype}\" up", + "remote_moved_any_side": "Device moved with any side up", "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", "remote_rotate_from_side_4": "Device rotated from \"side 4\" to \"{subtype}\"", "remote_rotate_from_side_5": "Device rotated from \"side 5\" to \"{subtype}\"", - "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"" + "remote_rotate_from_side_6": "Device rotated from \"side 6\" to \"{subtype}\"", + "remote_turned_clockwise": "Device turned clockwise", + "remote_turned_counter_clockwise": "Device turned counter clockwise" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 47fd99c48a2..6f5513d9729 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -72,20 +72,26 @@ "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", - "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtipo}\" detenido", + "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtype}\" detenido", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_double_tap": "Dispositivo \" {subtype} \" doble pulsaci\u00f3n", + "remote_double_tap_any_side": "Dispositivo con doble toque en cualquier lado", "remote_falling": "Dispositivo en ca\u00edda libre", + "remote_flip_180_degrees": "Dispositivo volteado 180 grados", + "remote_flip_90_degrees": "Dispositivo volteado 90 grados", "remote_gyro_activated": "Dispositivo sacudido", "remote_moved": "Dispositivo movido con \"{subtipo}\" hacia arriba", + "remote_moved_any_side": "Dispositivo movido con cualquier lado hacia arriba", "remote_rotate_from_side_1": "Dispositivo girado del \"lado 1\" al \" {subtype} \"", "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \" {subtype} \"", "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \" {subtype} \"", "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \" {subtype} \"", "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \" {subtype} \"", - "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"" + "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \" {subtype} \"", + "remote_turned_clockwise": "Dispositivo girado en el sentido de las agujas del reloj", + "remote_turned_counter_clockwise": "Dispositivo girado en sentido contrario a las agujas del reloj" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 4d49bd18d1e..1a4232e0817 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -76,7 +76,16 @@ "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", - "remote_gyro_activated": "Appareil secou\u00e9" + "remote_double_tap": "Appareil \"{subtype}\" tapot\u00e9 deux fois", + "remote_falling": "Appareil en chute libre", + "remote_gyro_activated": "Appareil secou\u00e9", + "remote_moved": "Appareil d\u00e9plac\u00e9 avec \"{subtype}\" vers le haut", + "remote_rotate_from_side_1": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 1\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_2": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 2\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_3": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 3\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_4": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 4\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_5": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 5\" \u00e0 \"{subtype}\"", + "remote_rotate_from_side_6": "Appareil tourn\u00e9 de \"c\u00f4t\u00e9 6\" \u00e0 \"{subtype}\"" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 33d49dfca46..980409d6987 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -18,8 +18,8 @@ "allow_clip_sensor": "Consenti l'importazione di sensori virtuali", "allow_deconz_groups": "Consenti l'importazione di gruppi deCONZ" }, - "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo hass.io {addon} ?", - "title": "Gateway Zigbee deCONZ tramite l'add-on Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo di Hass.io: {addon}?", + "title": "Gateway Pigmee deCONZ tramite il componente aggiuntivo di Hass.io" }, "init": { "data": { @@ -77,15 +77,21 @@ "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", "remote_double_tap": "Dispositivo \"{subtype}\" toccato due volte", + "remote_double_tap_any_side": "Dispositivo toccato due volte su qualsiasi lato", "remote_falling": "Dispositivo in caduta libera", + "remote_flip_180_degrees": "Dispositivo capovolto di 180 gradi", + "remote_flip_90_degrees": "Dispositivo capovolto di 90 gradi", "remote_gyro_activated": "Dispositivo in vibrazione", "remote_moved": "Dispositivo spostato con \"{subtype}\" verso l'alto", + "remote_moved_any_side": "Dispositivo spostato con qualsiasi lato verso l'alto", "remote_rotate_from_side_1": "Dispositivo ruotato da \"lato 1\" a \"{subtype}\"", "remote_rotate_from_side_2": "Dispositivo ruotato da \"lato 2\" a \"{subtype}\"", "remote_rotate_from_side_3": "Dispositivo ruotato da \"lato 3\" a \"{subtype}\"", "remote_rotate_from_side_4": "Dispositivo ruotato da \"lato 4\" a \"{subtype}\"", "remote_rotate_from_side_5": "Dispositivo ruotato da \"lato 5\" a \"{subtype}\"", - "remote_rotate_from_side_6": "Dispositivo ruotato da \"lato 6\" a \"{subtype}\"" + "remote_rotate_from_side_6": "Dispositivo ruotato da \"lato 6\" a \"{subtype}\"", + "remote_turned_clockwise": "Dispositivo ruotato in senso orario", + "remote_turned_counter_clockwise": "Dispositivo ruotato in senso antiorario" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index fede936b964..5cf1cb32ca2 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -18,7 +18,7 @@ "allow_clip_sensor": "\uac00\uc0c1 \uc13c\uc11c \uac00\uc838\uc624\uae30 \ud5c8\uc6a9", "allow_deconz_groups": "deCONZ \uadf8\ub8f9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9" }, - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Hass.io \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, "init": { @@ -65,27 +65,27 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "remote_awakened": "\uae30\uae30 \uc808\uc804 \ubaa8\ub4dc \ud574\uc81c\ub428", - "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", - "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", - "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", - "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", - "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804", - "remote_button_rotation_stopped": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804 \uc815\uc9c0", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", - "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", - "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14\ud0ed \ub428", - "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9d0", - "remote_gyro_activated": "\uae30\uae30 \ud754\ub4e6", - "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc784", - "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428", - "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub428" + "remote_awakened": "\uae30\uae30 \uc808\uc804 \ubaa8\ub4dc \ud574\uc81c\ub420 \ub54c", + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_rotated": "\"{subtype}\" \ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\ub420 \ub54c", + "remote_button_rotation_stopped": "\"{subtype}\" \ub85c \ubc84\ud2bc\uc774 \ud68c\uc804\uc744 \uba48\ucd9c \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_double_tap": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \ub354\ube14 \ud0ed \ub420 \ub54c", + "remote_falling": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc9c8 \ub54c", + "remote_gyro_activated": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c", + "remote_moved": "\uae30\uae30\uc758 \"{subtype}\" \uac00 \uc704\ub85c \ud5a5\ud55c\ucc44\ub85c \uc6c0\uc9c1\uc77c \ub54c", + "remote_rotate_from_side_1": "\"\uba74 1\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_2": "\"\uba74 2\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_3": "\"\uba74 3\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_4": "\"\uba74 4\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_5": "\"\uba74 5\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "remote_rotate_from_side_6": "\"\uba74 6\" \uc5d0\uc11c \"{subtype}\" \ub85c \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 07f88732c62..4b04cfa03ce 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", "remote_double_tap": "Apparat \"{subtype}\" zwee mol gedr\u00e9ckt", + "remote_double_tap_any_side": "Apparat gouf 2 mol ugetippt op enger S\u00e4it", "remote_falling": "Apparat am fr\u00e4ie Fall", + "remote_flip_180_degrees": "Apparat \u00ebm 180 Grad gedr\u00e9int", + "remote_flip_90_degrees": "Apparat \u00ebm 90 Grad gedr\u00e9int", "remote_gyro_activated": "Apparat ger\u00ebselt", "remote_moved": "Apparat beweegt mat \"{subtype}\" erop", + "remote_moved_any_side": "Apparat gouf mat enger S\u00e4it bewegt", "remote_rotate_from_side_1": "Apparat rot\u00e9iert vun der \"S\u00e4it 1\" op \"{subtype}\"", "remote_rotate_from_side_2": "Apparat rot\u00e9iert vun der \"S\u00e4it 2\" op \"{subtype}\"", "remote_rotate_from_side_3": "Apparat rot\u00e9iert vun der \"S\u00e4it 3\" op \"{subtype}\"", "remote_rotate_from_side_4": "Apparat rot\u00e9iert vun der \"S\u00e4it 4\" op \"{subtype}\"", "remote_rotate_from_side_5": "Apparat rot\u00e9iert vun der \"S\u00e4it 5\" op \"{subtype}\"", - "remote_rotate_from_side_6": "Apparat rot\u00e9iert vun der \"S\u00e4it\" 6 op \"{subtype}\"" + "remote_rotate_from_side_6": "Apparat rot\u00e9iert vun der \"S\u00e4it\" 6 op \"{subtype}\"", + "remote_turned_clockwise": "Apparat mam Auere Wee gedr\u00e9int", + "remote_turned_counter_clockwise": "Apparat g\u00e9int den Auere Wee gedr\u00e9int" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 2c1dd687454..d6133542c64 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -18,7 +18,7 @@ "allow_clip_sensor": "Tillat import av virtuelle sensorer", "allow_deconz_groups": "Tillat import av deCONZ grupper" }, - "description": "\u00d8nsker du \u00e5 konfigurere Home Assistent for \u00e5 koble til deCONZ gateway gitt av Hass.io tillegget {addon}?", + "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegget {addon} ?", "title": "deCONZ Zigbee gateway via Hass.io tillegg" }, "init": { @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", "remote_double_tap": "Enheten \" {subtype} \" dobbeltklikket", + "remote_double_tap_any_side": "Enheten dobbeltklikket p\u00e5 alle sider", "remote_falling": "Enheten er i fritt fall", + "remote_flip_180_degrees": "Enheten er snudd 180 grader", + "remote_flip_90_degrees": "Enheten er snudd 90 grader", "remote_gyro_activated": "Enhet er ristet", "remote_moved": "Enheten ble flyttet med \"{under type}\" opp", + "remote_moved_any_side": "Enheten flyttet med alle sider opp", "remote_rotate_from_side_1": "Enheten rotert fra \"side 1\" til \" {subtype} \"", "remote_rotate_from_side_2": "Enheten rotert fra \"side 2\" til \" {subtype} \"", "remote_rotate_from_side_3": "Enheten rotert fra \"side 3\" til \" {subtype} \"", "remote_rotate_from_side_4": "Enheten rotert fra \"side 4\" til \" {subtype} \"", "remote_rotate_from_side_5": "Enheten rotert fra \"side 5\" til \" {subtype} \"", - "remote_rotate_from_side_6": "Enheten rotert fra \"side 6\" til \" {subtype} \"" + "remote_rotate_from_side_6": "Enheten rotert fra \"side 6\" til \" {subtype} \"", + "remote_turned_clockwise": "Enheten dreide med klokken", + "remote_turned_counter_clockwise": "Enheten dreide mot klokken" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index eafecf87d03..df85e7b8d1d 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -77,15 +77,21 @@ "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", "remote_double_tap": "urz\u0105dzenie \"{subtype}\" zostanie dwukrotnie pukni\u0119te", + "remote_double_tap_any_side": "urz\u0105dzenie dwukrotnie pukni\u0119te z dowolnej strony", "remote_falling": "urz\u0105dzenie zarejestruje swobodny spadek", + "remote_flip_180_degrees": "urz\u0105dzenie odwr\u00f3cone o 180 stopni", + "remote_flip_90_degrees": "urz\u0105dzenie odwr\u00f3cone o 90 stopni", "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", "remote_moved": "urz\u0105dzenie poruszone z \"{subtype}\" w g\u00f3r\u0119", + "remote_moved_any_side": "urz\u0105dzenie przesuni\u0119te dowoln\u0105 stron\u0105 do g\u00f3ry", "remote_rotate_from_side_1": "urz\u0105dzenie obr\u00f3cone ze \"strona 1\" na \"{subtype}\"", "remote_rotate_from_side_2": "urz\u0105dzenie obr\u00f3cone ze \"strona 2\" na \"{subtype}\"", "remote_rotate_from_side_3": "urz\u0105dzenie obr\u00f3cone ze \"strona 3\" na \"{subtype}\"", "remote_rotate_from_side_4": "urz\u0105dzenie obr\u00f3cone ze \"strona 4\" na \"{subtype}\"", "remote_rotate_from_side_5": "urz\u0105dzenie obr\u00f3cone ze \"strona 5\" na \"{subtype}\"", - "remote_rotate_from_side_6": "urz\u0105dzenie obr\u00f3cone ze \"strona 6\" na \"{subtype}\"" + "remote_rotate_from_side_6": "urz\u0105dzenie obr\u00f3cone ze \"strona 6\" na \"{subtype}\"", + "remote_turned_clockwise": "urz\u0105dzenie obr\u00f3cone zgodnie z ruchem wskaz\u00f3wek zegara", + "remote_turned_counter_clockwise": "urz\u0105dzenie obr\u00f3cone przeciwnie do ruchu wskaz\u00f3wek zegara" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index f0398e530bc..29b584fb9bb 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ.", - "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." + "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", "remote_button_triple_press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", "remote_double_tap": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \"{subtype}\" \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_double_tap_any_side": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \u0434\u0432\u0430\u0436\u0434\u044b", "remote_falling": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u043c \u043f\u0430\u0434\u0435\u043d\u0438\u0438", + "remote_flip_180_degrees": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 180 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432", + "remote_flip_90_degrees": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u043e\u0432", "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", "remote_moved": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438, \u043a\u043e\u0433\u0434\u0430 \"{subtype}\" \u0441\u0432\u0435\u0440\u0445\u0443", + "remote_moved_any_side": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0434\u0432\u0438\u043d\u0443\u043b\u0438", "remote_rotate_from_side_1": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 1 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_2": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 2 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_3": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 3 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_4": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 4 \u043d\u0430 \"{subtype}\"", "remote_rotate_from_side_5": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 5 \u043d\u0430 \"{subtype}\"", - "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 6 \u043d\u0430 \"{subtype}\"" + "remote_rotate_from_side_6": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0441 \u0413\u0440\u0430\u043d\u0438 6 \u043d\u0430 \"{subtype}\"", + "remote_turned_clockwise": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0435", + "remote_turned_counter_clockwise": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u0440\u043e\u0442\u0438\u0432 \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0438" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 0edfc11af55..385de6f0f01 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -18,7 +18,7 @@ "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev", "allow_deconz_groups": "Dovoli uvoz deCONZ skupin" }, - "description": "\u017delite konfigurirati Home Assistant-a za povezavo z deCONZ prehodom, ki ga ponuja hass.io dodatek {addon} ?", + "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s prehodom deCONZ, ki ga ponuja dodatek Hass.io {addon} ?", "title": "deCONZ Zigbee prehod preko dodatka Hass.io" }, "init": { @@ -77,15 +77,21 @@ "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", "remote_double_tap": "Naprava \"{subtype}\" dvakrat dotaknjena", + "remote_double_tap_any_side": "Naprava je bila dvojno tapnjena na katerokoli stran", "remote_falling": "Naprava v prostem padu", + "remote_flip_180_degrees": "Naprava se je obrnila za 180 stopinj", + "remote_flip_90_degrees": "Naprava se je obrnila za 90 stopinj", "remote_gyro_activated": "Naprava se je pretresla", "remote_moved": "Naprava je premaknjena s \"{subtype}\" navzgor", + "remote_moved_any_side": "Naprava se je premikala s katero koli stranjo navzgor", "remote_rotate_from_side_1": "Naprava je zasukana iz \"strani 1\" v \"{subtype}\"", "remote_rotate_from_side_2": "Naprava je zasukana iz \"strani 2\" v \"{subtype}\"", "remote_rotate_from_side_3": "Naprava je zasukana iz \"strani 3\" v \"{subtype}\"", "remote_rotate_from_side_4": "Naprava je zasukana iz \"strani 4\" v \"{subtype}\"", "remote_rotate_from_side_5": "Naprava je zasukana iz \"strani 5\" v \"{subtype}\"", - "remote_rotate_from_side_6": "Naprava je zasukana iz \"strani 6\" v \"{subtype}\"" + "remote_rotate_from_side_6": "Naprava je zasukana iz \"strani 6\" v \"{subtype}\"", + "remote_turned_clockwise": "Naprava se je obrnila v smeri urinega kazalca", + "remote_turned_counter_clockwise": "Naprava se je obrnila v nasprotni smeri urinega kazalca" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 0a0e40a8d1e..96ab68a8dbb 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -18,7 +18,7 @@ "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668", "allow_deconz_groups": "\u5141\u8a31\u532f\u5165 deCONZ \u7fa4\u7d44" }, - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u7d44\u4ef6 {addon} \u4e4b deCONZ \u9598\u9053\u5668\uff1f", + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u6574\u5408 {addon} \u4e4b deCONZ \u9598\u9053\u5668\uff1f", "title": "\u900f\u904e Hass.io \u9644\u52a0\u7d44\u4ef6 deCONZ Zigbee \u9598\u9053\u5668" }, "init": { @@ -77,15 +77,21 @@ "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", "remote_double_tap": "\u8a2d\u5099 \"{subtype}\" \u96d9\u6572", + "remote_double_tap_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u96d9\u9ede\u9078", "remote_falling": "\u8a2d\u5099\u81ea\u7531\u843d\u4e0b", + "remote_flip_180_degrees": "\u8a2d\u5099\u65cb\u8f49 180 \u5ea6", + "remote_flip_90_degrees": "\u8a2d\u5099\u65cb\u8f49 90 \u5ea6", "remote_gyro_activated": "\u8a2d\u5099\u6416\u6643", "remote_moved": "\u8a2d\u5099\u79fb\u52d5\u81f3 \"{subtype}\" \u671d\u4e0a", + "remote_moved_any_side": "\u8a2d\u5099\u4efb\u4e00\u9762\u671d\u4e0a", "remote_rotate_from_side_1": "\u8a2d\u5099\u7531\u300c\u7b2c 1 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_2": "\u8a2d\u5099\u7531\u300c\u7b2c 2 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_3": "\u8a2d\u5099\u7531\u300c\u7b2c 3 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_4": "\u8a2d\u5099\u7531\u300c\u7b2c 4 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", "remote_rotate_from_side_5": "\u8a2d\u5099\u7531\u300c\u7b2c 5 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", - "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d" + "remote_rotate_from_side_6": "\u8a2d\u5099\u7531\u300c\u7b2c 6 \u9762\u300d\u65cb\u8f49\u81f3\u300c{subtype}\u300d", + "remote_turned_clockwise": "\u8a2d\u5099\u9806\u6642\u91dd\u65cb\u8f49", + "remote_turned_counter_clockwise": "\u8a2d\u5099\u9006\u6642\u91dd\u65cb\u8f49" } }, "options": { diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 0ea91d10b19..096bc6c2904 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -4,8 +4,8 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_STOP from .config_flow import get_master_gateway -from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, CONF_UUID, DOMAIN -from .gateway import DeconzGateway, get_gateway_from_config_entry +from .const import CONF_MASTER_GATEWAY, DOMAIN +from .gateway import DeconzGateway from .services import async_setup_services, async_unload_services CONFIG_SCHEMA = vol.Schema( @@ -35,13 +35,16 @@ async def async_setup_entry(hass, config_entry): if not await gateway.async_setup(): return False - hass.data[DOMAIN][gateway.bridgeid] = gateway + # 0.104 introduced config entry unique id, this makes upgrading possible + if config_entry.unique_id is None: + hass.config_entries.async_update_entry( + config_entry, unique_id=gateway.api.config.bridgeid + ) + + hass.data[DOMAIN][config_entry.unique_id] = gateway await gateway.async_update_device_registry() - if CONF_UUID not in config_entry.data: - await async_add_uuid_to_config_entry(hass, config_entry) - await async_setup_services(hass) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, gateway.shutdown) @@ -51,7 +54,7 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload deCONZ config entry.""" - gateway = hass.data[DOMAIN].pop(config_entry.data[CONF_BRIDGEID]) + gateway = hass.data[DOMAIN].pop(config_entry.unique_id) if not hass.data[DOMAIN]: await async_unload_services(hass) @@ -74,11 +77,3 @@ async def async_update_master_gateway(hass, config_entry): options = {**config_entry.options, CONF_MASTER_GATEWAY: master} hass.config_entries.async_update_entry(config_entry, options=options) - - -async def async_add_uuid_to_config_entry(hass, config_entry): - """Add UUID to config entry to help discovery identify entries.""" - gateway = get_gateway_from_config_entry(hass, config_entry) - config = {**config_entry.data, CONF_UUID: gateway.api.config.uuid} - - hass.config_entries.async_update_entry(config_entry, data=config) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 0fdc5904c2d..6261473bb0e 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -45,7 +45,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - async_add_sensor(gateway.api.sensors.values()) + async_add_sensor( + [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)] + ) class DeconzBinarySensor(DeconzDevice, BinarySensorDevice): diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 62b8190ff63..dd37cc31fae 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,22 +1,27 @@ """Config flow to configure deCONZ component.""" import asyncio +from urllib.parse import urlparse import async_timeout from pydeconz.errors import RequestError, ResponseError -from pydeconz.utils import async_discovery, async_get_api_key, async_get_gateway_config +from pydeconz.utils import ( + async_discovery, + async_get_api_key, + async_get_bridge_id, + normalize_bridge_id, +) import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import ( - _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, CONF_BRIDGEID, - CONF_UUID, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_PORT, @@ -25,16 +30,6 @@ from .const import ( DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" -ATTR_UUID = "udn" - - -@callback -def configured_gateways(hass): - """Return a set of all configured gateways.""" - return { - entry.data[CONF_BRIDGEID]: entry - for entry in hass.config_entries.async_entries(DOMAIN) - } @callback @@ -61,6 +56,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the deCONZ config flow.""" + self.bridge_id = None self.bridges = [] self.deconz_config = {} @@ -78,7 +74,11 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: for bridge in self.bridges: if bridge[CONF_HOST] == user_input[CONF_HOST]: - self.deconz_config = bridge + self.bridge_id = bridge[CONF_BRIDGEID] + self.deconz_config = { + CONF_HOST: bridge[CONF_HOST], + CONF_PORT: bridge[CONF_PORT], + } return await self.async_step_link() self.deconz_config = user_input @@ -94,8 +94,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridges = [] if len(self.bridges) == 1: - self.deconz_config = self.bridges[0] - return await self.async_step_link() + return await self.async_step_user(self.bridges[0]) if len(self.bridges) > 1: hosts = [] @@ -140,23 +139,30 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _create_entry(self): """Create entry for gateway.""" - if CONF_BRIDGEID not in self.deconz_config: + if not self.bridge_id: session = aiohttp_client.async_get_clientsession(self.hass) try: with async_timeout.timeout(10): - gateway_config = await async_get_gateway_config( + self.bridge_id = await async_get_bridge_id( session, **self.deconz_config ) - self.deconz_config[CONF_BRIDGEID] = gateway_config.bridgeid - self.deconz_config[CONF_UUID] = gateway_config.uuid + + for entry in self.hass.config_entries.async_entries(DOMAIN): + if self.bridge_id == entry.unique_id: + return self._update_entry( + entry, + host=self.deconz_config[CONF_HOST], + port=self.deconz_config[CONF_PORT], + api_key=self.deconz_config[CONF_API_KEY], + ) + + await self.async_set_unique_id(self.bridge_id) except asyncio.TimeoutError: return self.async_abort(reason="no_bridges") - return self.async_create_entry( - title="deCONZ-" + self.deconz_config[CONF_BRIDGEID], data=self.deconz_config - ) + return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) def _update_entry(self, entry, host, port, api_key=None): """Update existing entry.""" @@ -178,39 +184,28 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" - # Import it here, because only now do we know ssdp integration loaded. - # pylint: disable=import-outside-toplevel - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL - - if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL: + if ( + discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) + != DECONZ_MANUFACTURERURL + ): return self.async_abort(reason="not_deconz_bridge") - uuid = discovery_info[ATTR_UUID].replace("uuid:", "") - - _LOGGER.debug("deCONZ gateway discovered (%s)", uuid) + self.bridge_id = normalize_bridge_id(discovery_info[ssdp.ATTR_UPNP_SERIAL]) + parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) for entry in self.hass.config_entries.async_entries(DOMAIN): - if uuid == entry.data.get(CONF_UUID): + if self.bridge_id == entry.unique_id: if entry.source == "hassio": return self.async_abort(reason="already_configured") - return self._update_entry( - entry, discovery_info[CONF_HOST], entry.data.get(CONF_PORT) - ) - - bridgeid = discovery_info[ATTR_SERIAL] - if any( - bridgeid == flow["context"][CONF_BRIDGEID] - for flow in self._async_in_progress() - ): - return self.async_abort(reason="already_in_progress") + return self._update_entry(entry, parsed_url.hostname, parsed_url.port) + await self.async_set_unique_id(self.bridge_id) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context[CONF_BRIDGEID] = bridgeid - self.context["title_placeholders"] = {"host": discovery_info[CONF_HOST]} + self.context["title_placeholders"] = {"host": parsed_url.hostname} self.deconz_config = { - CONF_HOST: discovery_info[CONF_HOST], - CONF_PORT: discovery_info[CONF_PORT], + CONF_HOST: parsed_url.hostname, + CONF_PORT: parsed_url.port, } return await self.async_step_link() @@ -220,18 +215,18 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the discovery component. """ - bridgeid = user_input[CONF_SERIAL] - gateway_entries = configured_gateways(self.hass) + self.bridge_id = normalize_bridge_id(user_input[CONF_SERIAL]) + gateway = self.hass.data.get(DOMAIN, {}).get(self.bridge_id) - if bridgeid in gateway_entries: - entry = gateway_entries[bridgeid] + if gateway: return self._update_entry( - entry, + gateway.config_entry, user_input[CONF_HOST], user_input[CONF_PORT], user_input[CONF_API_KEY], ) + await self.async_set_unique_id(self.bridge_id) self._hassio_discovery = user_input return await self.async_step_hassio_confirm() @@ -242,7 +237,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.deconz_config = { CONF_HOST: self._hassio_discovery[CONF_HOST], CONF_PORT: self._hassio_discovery[CONF_PORT], - CONF_BRIDGEID: self._hassio_discovery[CONF_SERIAL], CONF_API_KEY: self._hassio_discovery[CONF_API_KEY], } diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index ad23a564272..41ef80b367f 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -6,7 +6,6 @@ _LOGGER = logging.getLogger(__package__) DOMAIN = "deconz" CONF_BRIDGEID = "bridgeid" -CONF_UUID = "uuid" DEFAULT_PORT = 80 DEFAULT_ALLOW_CLIP_SENSOR = False @@ -26,10 +25,10 @@ SUPPORTED_PLATFORMS = [ "switch", ] -NEW_GROUP = "group" -NEW_LIGHT = "light" -NEW_SCENE = "scene" -NEW_SENSOR = "sensor" +NEW_GROUP = "groups" +NEW_LIGHT = "lights" +NEW_SCENE = "scenes" +NEW_SENSOR = "sensors" NEW_DEVICE = { NEW_GROUP: "deconz_new_group_{}", @@ -50,3 +49,5 @@ COVER_TYPES = DAMPERS + WINDOW_COVERS POWER_PLUGS = ["On/Off plug-in unit", "Smart plug"] SIRENS = ["Warning device"] SWITCH_TYPES = POWER_PLUGS + SIRENS + +CONF_GESTURE = "gesture" diff --git a/homeassistant/components/deconz/deconz_event.py b/homeassistant/components/deconz/deconz_event.py index 31588db1f23..3c2442994a5 100644 --- a/homeassistant/components/deconz/deconz_event.py +++ b/homeassistant/components/deconz/deconz_event.py @@ -3,7 +3,7 @@ from homeassistant.const import CONF_EVENT, CONF_ID from homeassistant.core import callback from homeassistant.util import slugify -from .const import _LOGGER +from .const import _LOGGER, CONF_GESTURE from .deconz_device import DeconzBase CONF_DECONZ_EVENT = "deconz_event" @@ -47,6 +47,8 @@ class DeconzEvent(DeconzBase): CONF_UNIQUE_ID: self.serial, CONF_EVENT: self._device.state, } + if self._device.gesture: + data[CONF_GESTURE] = self._device.gesture self.gateway.hass.bus.async_fire(CONF_DECONZ_EVENT, data) async def async_update_device_registry(self): diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index b6691548b87..e8322c18e9a 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -15,9 +15,7 @@ from homeassistant.const import ( ) 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 +from .deconz_event import CONF_DECONZ_EVENT, CONF_GESTURE, CONF_UNIQUE_ID CONF_SUBTYPE = "subtype" @@ -36,6 +34,12 @@ CONF_MOVE = "remote_moved" CONF_DOUBLE_TAP = "remote_double_tap" CONF_SHAKE = "remote_gyro_activated" CONF_FREE_FALL = "remote_falling" +CONF_FLIP_90 = "remote_flip_90_degrees" +CONF_FLIP_180 = "remote_flip_180_degrees" +CONF_MOVE_ANY = "remote_moved_any_side" +CONF_DOUBLE_TAP_ANY = "remote_double_tap_any_side" +CONF_TURN_CW = "remote_turned_clockwise" +CONF_TURN_CCW = "remote_turned_counter_clockwise" CONF_ROTATE_FROM_SIDE_1 = "remote_rotate_from_side_1" CONF_ROTATE_FROM_SIDE_2 = "remote_rotate_from_side_2" CONF_ROTATE_FROM_SIDE_3 = "remote_rotate_from_side_3" @@ -67,189 +71,203 @@ CONF_SIDE_6 = "side_6" HUE_DIMMER_REMOTE_MODEL_GEN1 = "RWL020" HUE_DIMMER_REMOTE_MODEL_GEN2 = "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, + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1000}, + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_PRESS, CONF_DIM_UP): {CONF_EVENT: 2000}, + (CONF_SHORT_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3000}, + (CONF_SHORT_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4000}, + (CONF_SHORT_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 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, + (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 34}, + (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 16}, + (CONF_SHORT_PRESS, CONF_BUTTON_3): {CONF_EVENT: 17}, + (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 18}, } SYMFONISK_SOUND_CONTROLLER_MODEL = "SYMFONISK Sound Controller" SYMFONISK_SOUND_CONTROLLER = { - (CONF_SHORT_PRESS, CONF_TURN_ON): 1002, - (CONF_DOUBLE_PRESS, CONF_TURN_ON): 1004, - (CONF_TRIPLE_PRESS, CONF_TURN_ON): 1005, - (CONF_ROTATED, CONF_LEFT): 2001, - (CONF_ROTATION_STOPPED, CONF_LEFT): 2003, - (CONF_ROTATED, CONF_RIGHT): 3001, - (CONF_ROTATION_STOPPED, CONF_RIGHT): 3003, + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1005}, + (CONF_ROTATED, CONF_LEFT): {CONF_EVENT: 2001}, + (CONF_ROTATION_STOPPED, CONF_LEFT): {CONF_EVENT: 2003}, + (CONF_ROTATED, CONF_RIGHT): {CONF_EVENT: 3001}, + (CONF_ROTATION_STOPPED, CONF_RIGHT): {CONF_EVENT: 3003}, } 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, + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHORT_PRESS, CONF_TURN_OFF): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_TURN_OFF): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_TURN_OFF): {CONF_EVENT: 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, + (CONF_SHORT_PRESS, CONF_OPEN): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_OPEN): {CONF_EVENT: 1003}, + (CONF_SHORT_PRESS, CONF_CLOSE): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_CLOSE): {CONF_EVENT: 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, + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_SHORT_PRESS, CONF_DIM_UP): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_DIM_UP): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_DIM_UP): {CONF_EVENT: 2003}, + (CONF_SHORT_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_DIM_DOWN): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_DIM_DOWN): {CONF_EVENT: 3003}, + (CONF_SHORT_PRESS, CONF_LEFT): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_LEFT): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_LEFT): {CONF_EVENT: 4003}, + (CONF_SHORT_PRESS, CONF_RIGHT): {CONF_EVENT: 5002}, + (CONF_LONG_PRESS, CONF_RIGHT): {CONF_EVENT: 5001}, + (CONF_LONG_RELEASE, CONF_RIGHT): {CONF_EVENT: 5003}, } TRADFRI_WIRELESS_DIMMER_MODEL = "TRADFRI wireless dimmer" TRADFRI_WIRELESS_DIMMER = { - (CONF_ROTATED, CONF_LEFT): 3002, - (CONF_ROTATED, CONF_RIGHT): 2002, + (CONF_ROTATED, CONF_LEFT): {CONF_EVENT: 3002}, + (CONF_ROTATED, CONF_RIGHT): {CONF_EVENT: 2002}, } AQARA_CUBE_MODEL = "lumi.sensor_cube" +AQARA_CUBE_MODEL_ALT1 = "lumi.sensor_cube.aqgl01" AQARA_CUBE = { - (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_2): 6002, - (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_3): 3002, - (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_4): 4002, - (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_5): 1002, - (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_6): 5002, - (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_1): 2006, - (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_3): 3006, - (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_4): 4006, - (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_5): 1006, - (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_6): 5006, - (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_1): 2003, - (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_2): 6003, - (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_4): 4003, - (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_5): 1003, - (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_6): 5003, - (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_1): 2004, - (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_2): 6004, - (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_3): 3004, - (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_5): 1004, - (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_6): 5004, - (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_1): 2001, - (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_2): 6001, - (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_3): 3001, - (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_4): 4001, - (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_6): 5001, - (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_1): 2005, - (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_2): 6005, - (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_3): 3005, - (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_4): 4005, - (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_5): 1005, - (CONF_MOVE, CONF_SIDE_1): 2000, - (CONF_MOVE, CONF_SIDE_2): 6000, - (CONF_MOVE, CONF_SIDE_3): 3000, - (CONF_MOVE, CONF_SIDE_4): 4000, - (CONF_MOVE, CONF_SIDE_5): 1000, - (CONF_MOVE, CONF_SIDE_6): 5000, - (CONF_DOUBLE_TAP, CONF_SIDE_1): 2002, - (CONF_DOUBLE_TAP, CONF_SIDE_2): 6002, - (CONF_DOUBLE_TAP, CONF_SIDE_3): 3003, - (CONF_DOUBLE_TAP, CONF_SIDE_4): 4004, - (CONF_DOUBLE_TAP, CONF_SIDE_5): 1001, - (CONF_DOUBLE_TAP, CONF_SIDE_6): 5005, - (CONF_AWAKE, ""): 7000, - (CONF_FREE_FALL, ""): 7008, - (CONF_SHAKE, ""): 7007, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_2): {CONF_EVENT: 6002}, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_3): {CONF_EVENT: 3002}, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_4): {CONF_EVENT: 4002}, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_5): {CONF_EVENT: 1002}, + (CONF_ROTATE_FROM_SIDE_1, CONF_SIDE_6): {CONF_EVENT: 5002}, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_1): {CONF_EVENT: 2006}, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_3): {CONF_EVENT: 3006}, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_4): {CONF_EVENT: 4006}, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_5): {CONF_EVENT: 1006}, + (CONF_ROTATE_FROM_SIDE_2, CONF_SIDE_6): {CONF_EVENT: 5006}, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_1): {CONF_EVENT: 2003}, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_2): {CONF_EVENT: 6003}, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_4): {CONF_EVENT: 4003}, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_5): {CONF_EVENT: 1003}, + (CONF_ROTATE_FROM_SIDE_3, CONF_SIDE_6): {CONF_EVENT: 5003}, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_1): {CONF_EVENT: 2004}, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_2): {CONF_EVENT: 6004}, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_3): {CONF_EVENT: 3004}, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_5): {CONF_EVENT: 1004}, + (CONF_ROTATE_FROM_SIDE_4, CONF_SIDE_6): {CONF_EVENT: 5004}, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_1): {CONF_EVENT: 2001}, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_2): {CONF_EVENT: 6001}, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_3): {CONF_EVENT: 3001}, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_4): {CONF_EVENT: 4001}, + (CONF_ROTATE_FROM_SIDE_5, CONF_SIDE_6): {CONF_EVENT: 5001}, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_1): {CONF_EVENT: 2005}, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_2): {CONF_EVENT: 6005}, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_3): {CONF_EVENT: 3005}, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_4): {CONF_EVENT: 4005}, + (CONF_ROTATE_FROM_SIDE_6, CONF_SIDE_5): {CONF_EVENT: 1005}, + (CONF_MOVE, CONF_SIDE_1): {CONF_EVENT: 2000}, + (CONF_MOVE, CONF_SIDE_2): {CONF_EVENT: 6000}, + (CONF_MOVE, CONF_SIDE_3): {CONF_EVENT: 3000}, + (CONF_MOVE, CONF_SIDE_4): {CONF_EVENT: 4000}, + (CONF_MOVE, CONF_SIDE_5): {CONF_EVENT: 1000}, + (CONF_MOVE, CONF_SIDE_6): {CONF_EVENT: 5000}, + (CONF_DOUBLE_TAP, CONF_SIDE_1): {CONF_EVENT: 2002}, + (CONF_DOUBLE_TAP, CONF_SIDE_2): {CONF_EVENT: 6002}, + (CONF_DOUBLE_TAP, CONF_SIDE_3): {CONF_EVENT: 3003}, + (CONF_DOUBLE_TAP, CONF_SIDE_4): {CONF_EVENT: 4004}, + (CONF_DOUBLE_TAP, CONF_SIDE_5): {CONF_EVENT: 1001}, + (CONF_DOUBLE_TAP, CONF_SIDE_6): {CONF_EVENT: 5005}, + (CONF_AWAKE, ""): {CONF_GESTURE: 0}, + (CONF_SHAKE, ""): {CONF_GESTURE: 1}, + (CONF_FREE_FALL, ""): {CONF_GESTURE: 2}, + (CONF_FLIP_90, ""): {CONF_GESTURE: 3}, + (CONF_FLIP_180, ""): {CONF_GESTURE: 4}, + (CONF_MOVE_ANY, ""): {CONF_GESTURE: 5}, + (CONF_DOUBLE_TAP_ANY, ""): {CONF_GESTURE: 6}, + (CONF_TURN_CW, ""): {CONF_GESTURE: 7}, + (CONF_TURN_CCW, ""): {CONF_GESTURE: 8}, } 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, + (CONF_SHORT_PRESS, CONF_LEFT): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_LEFT): {CONF_EVENT: 1001}, + (CONF_DOUBLE_PRESS, CONF_LEFT): {CONF_EVENT: 1004}, + (CONF_SHORT_PRESS, CONF_RIGHT): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_RIGHT): {CONF_EVENT: 2001}, + (CONF_DOUBLE_PRESS, CONF_RIGHT): {CONF_EVENT: 2004}, + (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3001}, + (CONF_DOUBLE_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3004}, } AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL = "lumi.sensor_86sw2" AQARA_DOUBLE_WALL_SWITCH_WXKG02LM = { - (CONF_SHORT_PRESS, CONF_LEFT): 1002, - (CONF_SHORT_PRESS, CONF_RIGHT): 2002, - (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): 3002, + (CONF_SHORT_PRESS, CONF_LEFT): {CONF_EVENT: 1002}, + (CONF_SHORT_PRESS, CONF_RIGHT): {CONF_EVENT: 2002}, + (CONF_SHORT_PRESS, CONF_BOTH_BUTTONS): {CONF_EVENT: 3002}, +} + +AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL = "lumi.remote.b186acn01" +AQARA_SINGLE_WALL_SWITCH_WXKG03LM = { + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, } 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, + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 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, + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1000}, + (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1005}, + (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1006}, + (CONF_QUINTUPLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1010}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 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, + (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, + (CONF_LONG_PRESS, CONF_TURN_ON): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1003}, + (CONF_SHAKE, ""): {CONF_EVENT: 1007}, } AQARA_SQUARE_SWITCH_WXKG11LM_2016_MODEL = "lumi.sensor_switch.aq2" AQARA_SQUARE_SWITCH_WXKG11LM_2016 = { - (CONF_SHORT_PRESS, 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_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, + (CONF_DOUBLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1004}, + (CONF_TRIPLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1005}, + (CONF_QUADRUPLE_PRESS, CONF_TURN_ON): {CONF_EVENT: 1006}, } REMOTES = { @@ -262,8 +280,10 @@ REMOTES = { TRADFRI_REMOTE_MODEL: TRADFRI_REMOTE, TRADFRI_WIRELESS_DIMMER_MODEL: TRADFRI_WIRELESS_DIMMER, AQARA_CUBE_MODEL: AQARA_CUBE, + AQARA_CUBE_MODEL_ALT1: AQARA_CUBE, AQARA_DOUBLE_WALL_SWITCH_MODEL: AQARA_DOUBLE_WALL_SWITCH, AQARA_DOUBLE_WALL_SWITCH_WXKG02LM_MODEL: AQARA_DOUBLE_WALL_SWITCH_WXKG02LM, + AQARA_SINGLE_WALL_SWITCH_WXKG03LM_MODEL: AQARA_SINGLE_WALL_SWITCH_WXKG03LM, AQARA_MINI_SWITCH_MODEL: AQARA_MINI_SWITCH, AQARA_ROUND_SWITCH_MODEL: AQARA_ROUND_SWITCH, AQARA_SQUARE_SWITCH_MODEL: AQARA_SQUARE_SWITCH, @@ -277,10 +297,8 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( 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(): + for gateway in hass.data.get(DOMAIN, {}).values(): - gateway = get_gateway_from_config_entry(hass, config_entry) for deconz_event in gateway.events: if device_id == deconz_event.device_id: @@ -326,7 +344,7 @@ async def async_attach_trigger(hass, config, action, automation_info): event_config = { event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, - event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, + event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, **trigger}, } event_config = event.TRIGGER_SCHEMA(event_config) diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 0c77285a6fe..04452cc313c 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,7 +4,7 @@ import asyncio import async_timeout from pydeconz import DeconzSession, errors -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client @@ -22,7 +22,6 @@ from .const import ( _LOGGER, CONF_ALLOW_CLIP_SENSOR, CONF_ALLOW_DECONZ_GROUPS, - CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, @@ -36,19 +35,19 @@ from .errors import AuthenticationRequired, CannotConnect @callback def get_gateway_from_config_entry(hass, config_entry): """Return gateway with a matching bridge id.""" - return hass.data[DOMAIN][config_entry.data[CONF_BRIDGEID]] + return hass.data[DOMAIN][config_entry.unique_id] class DeconzGateway: """Manages a single deCONZ gateway.""" - def __init__(self, hass, config_entry): + def __init__(self, hass, config_entry) -> None: """Initialize the system.""" self.hass = hass self.config_entry = config_entry + self.available = True self.api = None - self.deconz_ids = {} self.events = [] self.listeners = [] @@ -56,7 +55,7 @@ class DeconzGateway: @property def bridgeid(self) -> str: """Return the unique identifier of the gateway.""" - return self.config_entry.data[CONF_BRIDGEID] + return self.config_entry.unique_id @property def master(self) -> bool: @@ -77,7 +76,7 @@ class DeconzGateway: CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS ) - async def async_update_device_registry(self): + async def async_update_device_registry(self) -> None: """Update device registry.""" device_registry = await self.hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( @@ -90,13 +89,11 @@ class DeconzGateway: sw_version=self.api.config.swversion, ) - async def async_setup(self): + async def async_setup(self) -> bool: """Set up a deCONZ gateway.""" - hass = self.hass - try: self.api = await get_gateway( - hass, + self.hass, self.config_entry.data, self.async_add_device_callback, self.async_connection_status_callback, @@ -105,13 +102,13 @@ class DeconzGateway: except CannotConnect: raise ConfigEntryNotReady - except Exception: # pylint: disable=broad-except - _LOGGER.error("Error connecting with deCONZ gateway") + except Exception as err: # pylint: disable=broad-except + _LOGGER.error("Error connecting with deCONZ gateway: %s", err) return False for component in SUPPORTED_PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup( + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( self.config_entry, component ) ) @@ -124,7 +121,7 @@ class DeconzGateway: return True @staticmethod - async def async_new_address(hass, entry): + async def async_new_address(hass, entry) -> None: """Handle signals of gateway getting new address. This is a static method because a class method (bound method), @@ -137,23 +134,23 @@ class DeconzGateway: gateway.api.start() @property - def signal_reachable(self): + def signal_reachable(self) -> str: """Gateway specific event to signal a change in connection status.""" return f"deconz-reachable-{self.bridgeid}" @callback - def async_connection_status_callback(self, available): + def async_connection_status_callback(self, available) -> None: """Handle signals of gateway connection status.""" self.available = available async_dispatcher_send(self.hass, self.signal_reachable, True) @property - def signal_options_update(self): + def signal_options_update(self) -> str: """Event specific per deCONZ entry to signal new options.""" return f"deconz-options-{self.bridgeid}" @staticmethod - async def async_options_updated(hass, entry): + async def async_options_updated(hass, entry) -> None: """Triggered by config entry options updates.""" gateway = get_gateway_from_config_entry(hass, entry) @@ -161,12 +158,12 @@ class DeconzGateway: async_dispatcher_send(hass, gateway.signal_options_update, registry) @callback - def async_signal_new_device(self, device_type): + def async_signal_new_device(self, device_type) -> str: """Gateway specific event to signal new device.""" return NEW_DEVICE[device_type].format(self.bridgeid) @callback - def async_add_device_callback(self, device_type, device): + def async_add_device_callback(self, device_type, device) -> None: """Handle event of new device creation in deCONZ.""" if not isinstance(device, list): device = [device] @@ -175,7 +172,7 @@ class DeconzGateway: ) @callback - def shutdown(self, event): + def shutdown(self, event) -> None: """Wrap the call to deconz.close. Used as an argument to EventBus.async_listen_once. @@ -206,20 +203,21 @@ class DeconzGateway: async def get_gateway( hass, config, async_add_device_callback, async_connection_status_callback -): +) -> DeconzSession: """Create a gateway object and verify configuration.""" session = aiohttp_client.async_get_clientsession(hass) deconz = DeconzSession( - hass.loop, session, - **config, + config[CONF_HOST], + config[CONF_PORT], + config[CONF_API_KEY], async_add_device=async_add_device_callback, connection_status=async_connection_status_callback, ) try: with async_timeout.timeout(10): - await deconz.async_load_parameters() + await deconz.initialize() return deconz except errors.Unauthorized: @@ -234,7 +232,7 @@ async def get_gateway( class DeconzEntityHandler: """Platform entity handler to help with updating disabled by.""" - def __init__(self, gateway): + def __init__(self, gateway) -> None: """Create an entity handler.""" self.gateway = gateway self._entities = [] @@ -246,12 +244,12 @@ class DeconzEntityHandler: ) @callback - def add_entity(self, entity): + def add_entity(self, entity) -> None: """Add a new entity to handler.""" self._entities.append(entity) @callback - def update_entity_registry(self, entity_registry): + def update_entity_registry(self, entity_registry) -> None: """Update entity registry disabled by status.""" for entity in self._entities: diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 64902002600..a327d7106fc 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -1,18 +1,15 @@ { "domain": "deconz", - "name": "Deconz", + "name": "deCONZ", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", - "requirements": [ - "pydeconz==64" - ], + "requirements": ["pydeconz==67"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], "dependencies": [], - "codeowners": [ - "@kane610" - ] + "codeowners": ["@kane610"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 4ffaba9b499..8194dd145dc 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -1,5 +1,13 @@ """Support for deCONZ sensors.""" -from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch, Thermostat +from pydeconz.sensor import ( + Battery, + Consumption, + Daylight, + LightLevel, + Power, + Switch, + Thermostat, +) from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback @@ -53,7 +61,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - elif new and not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: + elif ( + new + and sensor.BINARY is False + and sensor.type not in Battery.ZHATYPE + Thermostat.ZHATYPE + ): new_sensor = DeconzSensor(sensor, gateway) entity_handler.add_entity(new_sensor) @@ -76,7 +88,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) - async_add_sensor(gateway.api.sensors.values()) + async_add_sensor( + [gateway.api.sensors[key] for key in sorted(gateway.api.sensors, key=int)] + ) class DeconzSensor(DeconzDevice): diff --git a/homeassistant/components/deconz/services.py b/homeassistant/components/deconz/services.py index 8efdc2718bc..9d133acdb1d 100644 --- a/homeassistant/components/deconz/services.py +++ b/homeassistant/components/deconz/services.py @@ -4,7 +4,15 @@ import voluptuous as vol from homeassistant.helpers import config_validation as cv from .config_flow import get_master_gateway -from .const import _LOGGER, CONF_BRIDGEID, DOMAIN +from .const import ( + _LOGGER, + CONF_BRIDGEID, + DOMAIN, + NEW_GROUP, + NEW_LIGHT, + NEW_SCENE, + NEW_SENSOR, +) DECONZ_SERVICES = "deconz_services" @@ -105,7 +113,7 @@ async def async_configure_service(hass, data): _LOGGER.error("Could not find the entity %s", entity_id) return - await gateway.api.async_put_state(field, data) + await gateway.api.request("put", field, json=data) async def async_refresh_devices_service(hass, data): @@ -119,10 +127,10 @@ async def async_refresh_devices_service(hass, data): scenes = set(gateway.api.scenes.keys()) sensors = set(gateway.api.sensors.keys()) - await gateway.api.async_load_parameters() + await gateway.api.refresh_state() gateway.async_add_device_callback( - "group", + NEW_GROUP, [ group for group_id, group in gateway.api.groups.items() @@ -131,7 +139,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "light", + NEW_LIGHT, [ light for light_id, light in gateway.api.lights.items() @@ -140,7 +148,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "scene", + NEW_SCENE, [ scene for scene_id, scene in gateway.api.scenes.items() @@ -149,7 +157,7 @@ async def async_refresh_devices_service(hass, data): ) gateway.async_add_device_callback( - "sensor", + NEW_SENSOR, [ sensor for sensor_id, sensor in gateway.api.sensors.items() diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index a00e10f3e7e..b61ea6236da 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -23,7 +23,7 @@ }, "hassio_confirm": { "title": "deCONZ Zigbee gateway via Hass.io add-on", - "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the hass.io add-on {addon}?", + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?", "data": { "allow_clip_sensor": "Allow importing virtual sensors", "allow_deconz_groups": "Allow importing deCONZ groups" @@ -70,6 +70,12 @@ "remote_moved": "Device moved with \"{subtype}\" up", "remote_double_tap": "Device \"{subtype}\" double tapped", "remote_gyro_activated": "Device shaken", + "remote_flip_90_degrees": "Device flipped 90 degrees", + "remote_flip_180_degrees": "Device flipped 180 degrees", + "remote_moved_any_side": "Device moved with any side up", + "remote_double_tap_any_side": "Device double tapped on any side", + "remote_turned_clockwise": "Device turned clockwise", + "remote_turned_counter_clockwise": "Device turned counter clockwise", "remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"", "remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"", "remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"", diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 6ca427f2476..f4035352e51 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -62,8 +62,7 @@ def retry(method): return method(device, *args, **kwargs) except (decora.decoraException, AttributeError, BTLEException): _LOGGER.warning( - "Decora connect error for device %s. " "Reconnecting...", - device.name, + "Decora connect error for device %s. Reconnecting...", device.name, ) # pylint: disable=protected-access device._switch.connect() diff --git a/homeassistant/components/decora/manifest.json b/homeassistant/components/decora/manifest.json index 5142b5fb2e2..e16632718d1 100644 --- a/homeassistant/components/decora/manifest.json +++ b/homeassistant/components/decora/manifest.json @@ -1,11 +1,8 @@ { "domain": "decora", - "name": "Decora", + "name": "Leviton Decora", "documentation": "https://www.home-assistant.io/integrations/decora", - "requirements": [ - "bluepy==1.1.4", - "decora==0.6" - ], + "requirements": ["bluepy==1.3.0", "decora==0.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 6171f65ef24..7d8aa104bb0 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -2,17 +2,22 @@ import logging +# pylint: disable=import-error +from decora_wifi import DecoraWiFiSession +from decora_wifi.models.person import Person +from decora_wifi.models.residence import Residence +from decora_wifi.models.residential_account import ResidentialAccount import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_TRANSITION, - Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_TRANSITION, + Light, ) -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -28,11 +33,6 @@ NOTIFICATION_TITLE = "myLeviton Decora Setup" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Decora WiFi platform.""" - # pylint: disable=import-error - from decora_wifi import DecoraWiFiSession - from decora_wifi.models.person import Person - from decora_wifi.models.residential_account import ResidentialAccount - from decora_wifi.models.residence import Residence email = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) diff --git a/homeassistant/components/decora_wifi/manifest.json b/homeassistant/components/decora_wifi/manifest.json index 14b8829fae8..d340fb00d94 100644 --- a/homeassistant/components/decora_wifi/manifest.json +++ b/homeassistant/components/decora_wifi/manifest.json @@ -1,10 +1,8 @@ { "domain": "decora_wifi", - "name": "Decora wifi", + "name": "Leviton Decora Wi-Fi", "documentation": "https://www.home-assistant.io/integrations/decora_wifi", - "requirements": [ - "decora_wifi==1.4" - ], + "requirements": ["decora_wifi==1.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index a240599c420..c0a27b667c5 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -1,6 +1,6 @@ { "domain": "default_config", - "name": "Default config", + "name": "Default Config", "documentation": "https://www.home-assistant.io/integrations/default_config", "requirements": [], "dependencies": [ diff --git a/homeassistant/components/deluge/manifest.json b/homeassistant/components/deluge/manifest.json index b2eb3ada65f..cefc645725e 100644 --- a/homeassistant/components/deluge/manifest.json +++ b/homeassistant/components/deluge/manifest.json @@ -2,9 +2,7 @@ "domain": "deluge", "name": "Deluge", "documentation": "https://www.home-assistant.io/integrations/deluge", - "requirements": [ - "deluge-client==1.7.1" - ], + "requirements": ["deluge-client==1.7.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/demo/.translations/da.json b/homeassistant/components/demo/.translations/da.json new file mode 100644 index 00000000000..ef01fcb4f3c --- /dev/null +++ b/homeassistant/components/demo/.translations/da.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demo" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/hu.json b/homeassistant/components/demo/.translations/hu.json new file mode 100644 index 00000000000..51f0cd00642 --- /dev/null +++ b/homeassistant/components/demo/.translations/hu.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Dem\u00f3" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ja.json b/homeassistant/components/demo/.translations/ja.json new file mode 100644 index 00000000000..529170b111d --- /dev/null +++ b/homeassistant/components/demo/.translations/ja.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\u30c7\u30e2" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/ko.json b/homeassistant/components/demo/.translations/ko.json new file mode 100644 index 00000000000..d20943c7b36 --- /dev/null +++ b/homeassistant/components/demo/.translations/ko.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "\ub370\ubaa8" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/.translations/pt-BR.json b/homeassistant/components/demo/.translations/pt-BR.json new file mode 100644 index 00000000000..8183f28aed3 --- /dev/null +++ b/homeassistant/components/demo/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Demonstra\u00e7\u00e3o" + } +} \ No newline at end of file diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 05febfad603..b6845d9d6a4 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -4,8 +4,8 @@ import logging import time from homeassistant import bootstrap, config_entries -import homeassistant.core as ha from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +import homeassistant.core as ha DOMAIN = "demo" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/demo/binary_sensor.py b/homeassistant/components/demo/binary_sensor.py index c1e42807f6d..0f6dfa9f357 100644 --- a/homeassistant/components/demo/binary_sensor.py +++ b/homeassistant/components/demo/binary_sensor.py @@ -1,5 +1,6 @@ """Demo platform that has two fake binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice + from . import DOMAIN diff --git a/homeassistant/components/demo/calendar.py b/homeassistant/components/demo/calendar.py index 4ae836466f0..42cb2b137a1 100644 --- a/homeassistant/components/demo/calendar.py +++ b/homeassistant/components/demo/calendar.py @@ -1,9 +1,8 @@ """Demo platform that has two fake binary sensors.""" import copy -import homeassistant.util.dt as dt_util - from homeassistant.components.calendar import CalendarEventDevice, get_date +import homeassistant.util.dt as dt_util def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/demo/climate.py b/homeassistant/components/demo/climate.py index f4affed7ced..0edcf618ba6 100644 --- a/homeassistant/components/demo/climate.py +++ b/homeassistant/components/demo/climate.py @@ -1,11 +1,13 @@ """Demo platform that offers a fake climate device.""" import logging + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, + HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, @@ -18,9 +20,9 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_AUTO, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + from . import DOMAIN SUPPORT_FLAGS = 0 diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index 20a8747aaa5..20e3a52aa8d 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -1,6 +1,4 @@ """Demo platform for the cover component.""" -from homeassistant.helpers.event import track_utc_time_change - from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -8,6 +6,8 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, CoverDevice, ) +from homeassistant.helpers.event import track_utc_time_change + from . import DOMAIN diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 500d5f6a5ce..966ba51cacb 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -1,6 +1,4 @@ """Demo fan platform that has a fake fan.""" -from homeassistant.const import STATE_OFF - from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -10,6 +8,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.const import STATE_OFF FULL_SUPPORT = SUPPORT_SET_SPEED | SUPPORT_OSCILLATE | SUPPORT_DIRECTION LIMITED_SUPPORT = SUPPORT_SET_SPEED diff --git a/homeassistant/components/demo/geo_location.py b/homeassistant/components/demo/geo_location.py index 6a7aa7ddce1..6fc8e9c2e89 100644 --- a/homeassistant/components/demo/geo_location.py +++ b/homeassistant/components/demo/geo_location.py @@ -5,9 +5,8 @@ from math import cos, pi, radians, sin import random from typing import Optional -from homeassistant.helpers.event import track_time_interval - from homeassistant.components.geo_location import GeolocationEvent +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/demo/image_processing.py b/homeassistant/components/demo/image_processing.py index 348045e47b2..9183609509e 100644 --- a/homeassistant/components/demo/image_processing.py +++ b/homeassistant/components/demo/image_processing.py @@ -1,10 +1,10 @@ """Support for the demo image processing.""" from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - ATTR_CONFIDENCE, - ATTR_NAME, ATTR_AGE, + ATTR_CONFIDENCE, ATTR_GENDER, + ATTR_NAME, + ImageProcessingFaceEntity, ) from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, diff --git a/homeassistant/components/demo/lock.py b/homeassistant/components/demo/lock.py index 923469f045c..5074741d83d 100644 --- a/homeassistant/components/demo/lock.py +++ b/homeassistant/components/demo/lock.py @@ -1,7 +1,6 @@ """Demo lock platform that has two fake locks.""" -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED - from homeassistant.components.lock import SUPPORT_OPEN, LockDevice +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index a4802c3b67b..a3a647e0974 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -3,13 +3,7 @@ "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", "requirements": [], - "dependencies": [ - "conversation", - "zone", - "group", - "configurator" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["conversation", "zone", "group", "configurator"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/demo/media_player.py b/homeassistant/components/demo/media_player.py index 9d7c3892af8..33fe4ee3647 100644 --- a/homeassistant/components/demo/media_player.py +++ b/homeassistant/components/demo/media_player.py @@ -340,7 +340,7 @@ class DemoMusicPlayer(AbstractDemoPlayer): @property def media_image_url(self): """Return the image url of current playing media.""" - return "https://graph.facebook.com/v2.5/107771475912710/" "picture?type=large" + return "https://graph.facebook.com/v2.5/107771475912710/picture?type=large" @property def media_title(self): diff --git a/homeassistant/components/demo/sensor.py b/homeassistant/components/demo/sensor.py index bf5df94e74c..d2b2464468b 100644 --- a/homeassistant/components/demo/sensor.py +++ b/homeassistant/components/demo/sensor.py @@ -1,11 +1,12 @@ """Demo platform that has a couple of fake sensors.""" from homeassistant.const import ( ATTR_BATTERY_LEVEL, - TEMP_CELSIUS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity + from . import DOMAIN diff --git a/homeassistant/components/demo/switch.py b/homeassistant/components/demo/switch.py index 23006cff875..5c651198f5c 100644 --- a/homeassistant/components/demo/switch.py +++ b/homeassistant/components/demo/switch.py @@ -1,6 +1,7 @@ """Demo platform that has two fake switches.""" from homeassistant.components.switch import SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME + from . import DOMAIN diff --git a/homeassistant/components/demo/water_heater.py b/homeassistant/components/demo/water_heater.py index c3fff26c992..f9aca141245 100644 --- a/homeassistant/components/demo/water_heater.py +++ b/homeassistant/components/demo/water_heater.py @@ -1,12 +1,11 @@ """Demo platform that offers a fake water heater device.""" -from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT - from homeassistant.components.water_heater import ( SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice, ) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT SUPPORT_FLAGS_HEATER = ( SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE | SUPPORT_AWAY_MODE diff --git a/homeassistant/components/denon/manifest.json b/homeassistant/components/denon/manifest.json index 92b2aebab40..1c4e8b652f5 100644 --- a/homeassistant/components/denon/manifest.json +++ b/homeassistant/components/denon/manifest.json @@ -1,6 +1,6 @@ { "domain": "denon", - "name": "Denon", + "name": "Denon Network Receivers", "documentation": "https://www.home-assistant.io/integrations/denon", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index 7bed8423e8f..cd9d6e8feb7 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -4,7 +4,7 @@ import telnetlib import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/denonavr/__init__.py b/homeassistant/components/denonavr/__init__.py index dee84449d13..8877a7dfb3b 100644 --- a/homeassistant/components/denonavr/__init__.py +++ b/homeassistant/components/denonavr/__init__.py @@ -1 +1,35 @@ """The denonavr component.""" +import voluptuous as vol + +from homeassistant.const import ATTR_ENTITY_ID +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send + +DOMAIN = "denonavr" + +SERVICE_GET_COMMAND = "get_command" +ATTR_COMMAND = "command" + +CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) + +GET_COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) + +SERVICE_TO_METHOD = { + SERVICE_GET_COMMAND: {"method": "get_command", "schema": GET_COMMAND_SCHEMA} +} + + +def setup(hass, config): + """Set up the denonavr platform.""" + + def service_handler(service): + method = SERVICE_TO_METHOD.get(service.service) + data = service.data.copy() + data["method"] = method["method"] + dispatcher_send(hass, DOMAIN, data) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]["schema"] + hass.services.register(DOMAIN, service, service_handler, schema=schema) + + return True diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 9e084c78e21..1ecbe5b939f 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -1,10 +1,8 @@ { "domain": "denonavr", - "name": "Denonavr", + "name": "Denon AVR Network Receivers", "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": [ - "denonavr==0.7.10" - ], + "requirements": ["denonavr==0.7.11"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 1725b2d105c..350d065f9d9 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -24,16 +24,21 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_TIMEOUT, CONF_ZONE, + ENTITY_MATCH_ALL, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -89,7 +94,6 @@ NewHost = namedtuple("NewHost", ["host", "name"]) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Denon platform.""" - # Initialize list with receivers to be started receivers = [] @@ -190,6 +194,21 @@ class DenonDevice(MediaPlayerDevice): self._sound_mode_support and SUPPORT_SELECT_SOUND_MODE ) + async def async_added_to_hass(self): + """Register signal handler.""" + async_dispatcher_connect(self.hass, DOMAIN, self.signal_handler) + + def signal_handler(self, data): + """Handle domain-specific signal by calling appropriate method.""" + entity_ids = data[ATTR_ENTITY_ID] + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: + params = { + key: value + for key, value in data.items() + if key not in ["entity_id", "method"] + } + getattr(self, data["method"])(**params) + def update(self): """Get the latest status information from device.""" self._receiver.update() @@ -345,9 +364,17 @@ class DenonDevice(MediaPlayerDevice): return attributes def media_play_pause(self): - """Simulate play pause media player.""" + """Play or pause the media player.""" return self._receiver.toggle_play_pause() + def media_play(self): + """Send play command.""" + return self._receiver.play() + + def media_pause(self): + """Send pause command.""" + return self._receiver.pause() + def media_previous_track(self): """Send previous track command.""" return self._receiver.previous_track() @@ -398,3 +425,7 @@ class DenonDevice(MediaPlayerDevice): def mute_volume(self, mute): """Send mute command.""" return self._receiver.mute(mute) + + def get_command(self, command, **kwargs): + """Send generic command.""" + self._receiver.send_get_command(command) diff --git a/homeassistant/components/denonavr/services.yaml b/homeassistant/components/denonavr/services.yaml new file mode 100644 index 00000000000..889adc3af05 --- /dev/null +++ b/homeassistant/components/denonavr/services.yaml @@ -0,0 +1,11 @@ +# Describes the format for available webostv services + +get_command: + description: 'Send a generic http get command.' + fields: + entity_id: + description: Name(s) of the denonavr entities where to run the API method. + example: 'media_player.living_room_receiver' + command: + description: Endpoint of the command, including associated parameters. + example: '/goform/formiPhoneAppDirect.xml?RCKSK0410370' diff --git a/homeassistant/components/deutsche_bahn/manifest.json b/homeassistant/components/deutsche_bahn/manifest.json index 9a2bf666016..7bca10f761d 100644 --- a/homeassistant/components/deutsche_bahn/manifest.json +++ b/homeassistant/components/deutsche_bahn/manifest.json @@ -1,10 +1,8 @@ { "domain": "deutsche_bahn", - "name": "Deutsche bahn", + "name": "Deutsche Bahn", "documentation": "https://www.home-assistant.io/integrations/deutsche_bahn", - "requirements": [ - "schiene==0.23" - ], + "requirements": ["schiene==0.23"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index ad7b40f78db..204518b2ce3 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -2,9 +2,8 @@ from datetime import timedelta import logging -import voluptuous as vol - import schiene +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 80e64033295..56e087f0e5f 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -1,22 +1,21 @@ """Helpers for device automations.""" import asyncio import logging -from typing import Any, List, MutableMapping from types import ModuleType +from typing import Any, List, MutableMapping import voluptuous as vol import voluptuous_serialize -from homeassistant.const import CONF_PLATFORM, CONF_DOMAIN, CONF_DEVICE_ID from homeassistant.components import websocket_api +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.loader import async_get_integration, IntegrationNotFound +from homeassistant.loader import IntegrationNotFound, async_get_integration from .exceptions import InvalidDeviceAutomationConfig - # mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = "device_automation" @@ -174,13 +173,13 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom return capabilities -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "device_automation/action/list", vol.Required("device_id"): str, } ) +@websocket_api.async_response async def websocket_device_automation_list_actions(hass, connection, msg): """Handle request for device actions.""" device_id = msg["device_id"] @@ -188,13 +187,13 @@ async def websocket_device_automation_list_actions(hass, connection, msg): 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, } ) +@websocket_api.async_response async def websocket_device_automation_list_conditions(hass, connection, msg): """Handle request for device conditions.""" device_id = msg["device_id"] @@ -202,13 +201,13 @@ async def websocket_device_automation_list_conditions(hass, connection, msg): 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, } ) +@websocket_api.async_response async def websocket_device_automation_list_triggers(hass, connection, msg): """Handle request for device triggers.""" device_id = msg["device_id"] @@ -216,13 +215,13 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): connection.send_result(msg["id"], triggers) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "device_automation/action/capabilities", vol.Required("action"): dict, } ) +@websocket_api.async_response async def websocket_device_automation_get_action_capabilities(hass, connection, msg): """Handle request for device action capabilities.""" action = msg["action"] @@ -232,13 +231,13 @@ async def websocket_device_automation_get_action_capabilities(hass, connection, connection.send_result(msg["id"], capabilities) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "device_automation/condition/capabilities", vol.Required("condition"): dict, } ) +@websocket_api.async_response async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): """Handle request for device condition capabilities.""" condition = msg["condition"] @@ -248,13 +247,13 @@ async def websocket_device_automation_get_condition_capabilities(hass, connectio connection.send_result(msg["id"], capabilities) -@websocket_api.async_response @websocket_api.websocket_command( { vol.Required("type"): "device_automation/trigger/capabilities", vol.Required("trigger"): dict, } ) +@websocket_api.async_response async def websocket_device_automation_get_trigger_capabilities(hass, connection, msg): """Handle request for device trigger capabilities.""" trigger = msg["trigger"] diff --git a/homeassistant/components/device_automation/manifest.json b/homeassistant/components/device_automation/manifest.json index 50b1f9d357a..291ade0f607 100644 --- a/homeassistant/components/device_automation/manifest.json +++ b/homeassistant/components/device_automation/manifest.json @@ -1,12 +1,9 @@ { "domain": "device_automation", - "name": "Device automation", + "name": "Device Automation", "documentation": "https://www.home-assistant.io/integrations/device_automation", "requirements": [], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["webhook"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 5f01f4d9d71..7d84eb921e9 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -1,11 +1,11 @@ """Device automation helpers for toggle entity.""" from typing import Any, Dict, List + import voluptuous as vol -from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import ( - state as state_automation, AutomationActionType, + state as state_automation, ) from homeassistant.components.device_automation.const import ( CONF_IS_OFF, @@ -24,11 +24,12 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_TYPE, ) -from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import TRIGGER_BASE_SCHEMA +from . import TRIGGER_BASE_SCHEMA # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/components/device_sun_light_trigger/__init__.py b/homeassistant/components/device_sun_light_trigger/__init__.py index 9a058cfacc1..af6abf544c6 100644 --- a/homeassistant/components/device_sun_light_trigger/__init__.py +++ b/homeassistant/components/device_sun_light_trigger/__init__.py @@ -1,11 +1,9 @@ """Support to turn on lights based on the states.""" -import logging from datetime import timedelta +import logging import voluptuous as vol -from homeassistant.core import callback -import homeassistant.util.dt as dt_util from homeassistant.components.light import ( ATTR_PROFILE, ATTR_TRANSITION, @@ -20,12 +18,14 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_state_change, ) -from homeassistant.helpers.sun import is_up, get_astral_event_next -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.sun import get_astral_event_next, is_up +import homeassistant.util.dt as dt_util DOMAIN = "device_sun_light_trigger" CONF_DEVICE_GROUP = "device_group" @@ -66,23 +66,41 @@ async def async_setup(hass, config): 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_group = conf.get(CONF_LIGHT_GROUP) 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)) + + device_group = conf.get(CONF_DEVICE_GROUP) + + if device_group is None: + device_entity_ids = hass.states.async_entity_ids(device_tracker.DOMAIN) + else: + 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") return False # Get the light IDs from the specified group - light_ids = group.get_entity_ids(light_group, light.DOMAIN) + if light_group is None: + light_ids = hass.states.async_entity_ids(light.DOMAIN) + else: + light_ids = group.get_entity_ids(light_group, light.DOMAIN) if not light_ids: logger.error("No lights found to turn on") return False + @callback + def anyone_home(): + """Test if anyone is home.""" + return any(device_tracker.is_on(dt_id) for dt_id in device_entity_ids) + + @callback + def any_light_on(): + """Test if any light on.""" + return any(light.is_on(light_id) for light_id in light_ids) + def calc_time_for_light_when_sunset(): """Calculate the time when to start fading lights in when sun sets. @@ -97,7 +115,7 @@ async def async_setup(hass, config): def async_turn_on_before_sunset(light_id): """Turn on lights.""" - if not device_tracker.is_on() or light.is_on(light_id): + if not anyone_home() or light.is_on(light_id): return hass.async_create_task( hass.services.async_call( @@ -153,7 +171,7 @@ async def async_setup(hass, config): @callback def check_light_on_dev_state_change(entity, old_state, new_state): """Handle tracked device state changes.""" - lights_are_on = group.is_on(light_group) + lights_are_on = any_light_on() light_needed = not (lights_are_on or is_up(hass)) # These variables are needed for the elif check @@ -208,7 +226,12 @@ async def async_setup(hass, config): @callback def turn_off_lights_when_all_leave(entity, old_state, new_state): """Handle device group state change.""" - if not group.is_on(light_group): + # Make sure there is not someone home + if anyone_home(): + return + + # Check if any light is on + if not any_light_on(): return logger.info("Everyone has left but there are lights on. Turning them off") @@ -219,7 +242,11 @@ async def async_setup(hass, config): ) async_track_state_change( - hass, device_group, turn_off_lights_when_all_leave, STATE_HOME, STATE_NOT_HOME + hass, + device_entity_ids, + turn_off_lights_when_all_leave, + STATE_HOME, + STATE_NOT_HOME, ) return True diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index 3bea097b831..702f8704564 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -1,13 +1,9 @@ { "domain": "device_sun_light_trigger", - "name": "Device sun light trigger", + "name": "Presence-based Lights", "documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger", "requirements": [], - "dependencies": [ - "device_tracker", - "group", - "light", - "person" - ], - "codeowners": [] + "dependencies": ["device_tracker", "group", "light", "person"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/device_tracker/.translations/bg.json b/homeassistant/components/device_tracker/.translations/bg.json index 471cbc6a53a..68affa5afd0 100644 --- a/homeassistant/components/device_tracker/.translations/bg.json +++ b/homeassistant/components/device_tracker/.translations/bg.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u0435 \u0443 \u0434\u043e\u043c\u0430", "is_not_home": "{entity_name} \u043d\u0435 \u0435 \u0443 \u0434\u043e\u043c\u0430" } diff --git a/homeassistant/components/device_tracker/.translations/ca.json b/homeassistant/components/device_tracker/.translations/ca.json index de5aed41e3c..3a95841559b 100644 --- a/homeassistant/components/device_tracker/.translations/ca.json +++ b/homeassistant/components/device_tracker/.translations/ca.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u00e9s a casa", "is_not_home": "{entity_name} no \u00e9s a casa" } diff --git a/homeassistant/components/device_tracker/.translations/cs.json b/homeassistant/components/device_tracker/.translations/cs.json index 778ea0208c4..7e82f1a34f8 100644 --- a/homeassistant/components/device_tracker/.translations/cs.json +++ b/homeassistant/components/device_tracker/.translations/cs.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} je doma", "is_not_home": "{entity_name} nen\u00ed doma" } diff --git a/homeassistant/components/device_tracker/.translations/da.json b/homeassistant/components/device_tracker/.translations/da.json new file mode 100644 index 00000000000..d714b5b7d31 --- /dev/null +++ b/homeassistant/components/device_tracker/.translations/da.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_home": "{entity_name} er hjemme", + "is_not_home": "{entity_name} er ikke hjemme" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/de.json b/homeassistant/components/device_tracker/.translations/de.json index 7e72bd5595a..90a81db6b90 100644 --- a/homeassistant/components/device_tracker/.translations/de.json +++ b/homeassistant/components/device_tracker/.translations/de.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} ist Zuhause", "is_not_home": "{entity_name} ist nicht zu Hause" } diff --git a/homeassistant/components/device_tracker/.translations/en.json b/homeassistant/components/device_tracker/.translations/en.json index 25045e62b15..1022608477e 100644 --- a/homeassistant/components/device_tracker/.translations/en.json +++ b/homeassistant/components/device_tracker/.translations/en.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is home", "is_not_home": "{entity_name} is not home" } diff --git a/homeassistant/components/device_tracker/.translations/es.json b/homeassistant/components/device_tracker/.translations/es.json index 00bda928b56..cfbf7bcfe3e 100644 --- a/homeassistant/components/device_tracker/.translations/es.json +++ b/homeassistant/components/device_tracker/.translations/es.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est\u00e1 en casa", "is_not_home": "{entity_name} no est\u00e1 en casa" } diff --git a/homeassistant/components/device_tracker/.translations/fr.json b/homeassistant/components/device_tracker/.translations/fr.json index bf9033170c1..4c59d5ea1c8 100644 --- a/homeassistant/components/device_tracker/.translations/fr.json +++ b/homeassistant/components/device_tracker/.translations/fr.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est \u00e0 la maison", "is_not_home": "{entity_name} n'est pas \u00e0 la maison" } diff --git a/homeassistant/components/device_tracker/.translations/it.json b/homeassistant/components/device_tracker/.translations/it.json index e2d35296152..112afc6689f 100644 --- a/homeassistant/components/device_tracker/.translations/it.json +++ b/homeassistant/components/device_tracker/.translations/it.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u00e8 in casa", "is_not_home": "{entity_name} non \u00e8 in casa" } diff --git a/homeassistant/components/device_tracker/.translations/ko.json b/homeassistant/components/device_tracker/.translations/ko.json index d258f67db22..1834767222a 100644 --- a/homeassistant/components/device_tracker/.translations/ko.json +++ b/homeassistant/components/device_tracker/.translations/ko.json @@ -1,8 +1,8 @@ { "device_automation": { - "condtion_type": { - "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc2b5\ub2c8\ub2e4", - "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c\uc911\uc785\ub2c8\ub2e4" + "condition_type": { + "is_home": "{entity_name} \uc774(\uac00) \uc9d1\uc5d0 \uc788\uc73c\uba74", + "is_not_home": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74" } } } \ No newline at end of file diff --git a/homeassistant/components/device_tracker/.translations/lb.json b/homeassistant/components/device_tracker/.translations/lb.json index 98a066ef8e8..2c49f692662 100644 --- a/homeassistant/components/device_tracker/.translations/lb.json +++ b/homeassistant/components/device_tracker/.translations/lb.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} ass doheem", "is_not_home": "{entity_name} ass net doheem" } diff --git a/homeassistant/components/device_tracker/.translations/nl.json b/homeassistant/components/device_tracker/.translations/nl.json index d4de8b1f66a..31ab788f171 100644 --- a/homeassistant/components/device_tracker/.translations/nl.json +++ b/homeassistant/components/device_tracker/.translations/nl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is thuis", "is_not_home": "{entity_name} is niet thuis" } diff --git a/homeassistant/components/device_tracker/.translations/no.json b/homeassistant/components/device_tracker/.translations/no.json index 7034378b066..d714b5b7d31 100644 --- a/homeassistant/components/device_tracker/.translations/no.json +++ b/homeassistant/components/device_tracker/.translations/no.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} er hjemme", "is_not_home": "{entity_name} er ikke hjemme" } diff --git a/homeassistant/components/device_tracker/.translations/pl.json b/homeassistant/components/device_tracker/.translations/pl.json index 8f0f7953a2d..3930031ad38 100644 --- a/homeassistant/components/device_tracker/.translations/pl.json +++ b/homeassistant/components/device_tracker/.translations/pl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "urz\u0105dzenie {entity_name} jest w domu", "is_not_home": "urz\u0105dzenie {entity_name} jest poza domem" } diff --git a/homeassistant/components/device_tracker/.translations/pt.json b/homeassistant/components/device_tracker/.translations/pt.json index 952eb4b1475..8a8f662183a 100644 --- a/homeassistant/components/device_tracker/.translations/pt.json +++ b/homeassistant/components/device_tracker/.translations/pt.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} est\u00e1 em casa", "is_not_home": "{entity_name} n\u00e3o est\u00e1 em casa" } diff --git a/homeassistant/components/device_tracker/.translations/ru.json b/homeassistant/components/device_tracker/.translations/ru.json index 50a48ce942b..58767361fd4 100644 --- a/homeassistant/components/device_tracker/.translations/ru.json +++ b/homeassistant/components/device_tracker/.translations/ru.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u0434\u043e\u043c\u0430", "is_not_home": "{entity_name} \u043d\u0435 \u0434\u043e\u043c\u0430" } diff --git a/homeassistant/components/device_tracker/.translations/sl.json b/homeassistant/components/device_tracker/.translations/sl.json index f4784fbc664..11d876883d3 100644 --- a/homeassistant/components/device_tracker/.translations/sl.json +++ b/homeassistant/components/device_tracker/.translations/sl.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} je doma", "is_not_home": "{entity_name} ni doma" } diff --git a/homeassistant/components/device_tracker/.translations/zh-Hant.json b/homeassistant/components/device_tracker/.translations/zh-Hant.json index 4092031434c..456e09ebf0e 100644 --- a/homeassistant/components/device_tracker/.translations/zh-Hant.json +++ b/homeassistant/components/device_tracker/.translations/zh-Hant.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} \u5728\u5bb6", "is_not_home": "{entity_name} \u4e0d\u5728\u5bb6" } diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index 25e33d2a2db..7b42554b4c1 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -3,21 +3,18 @@ import asyncio import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.components import group +from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import GPSType, ConfigType, HomeAssistantType - from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME +from homeassistant.helpers.typing import ConfigType, GPSType, HomeAssistantType +from homeassistant.loader import bind_hass from . import legacy, setup from .config_entry import ( # noqa: F401 pylint: disable=unused-import async_setup_entry, async_unload_entry, ) -from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import from .const import ( ATTR_ATTRIBUTES, ATTR_BATTERY, @@ -38,13 +35,12 @@ from .const import ( DEFAULT_TRACK_NEW, DOMAIN, PLATFORM_TYPE_LEGACY, - SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_BLUETOOTH, + SOURCE_TYPE_BLUETOOTH_LE, SOURCE_TYPE_GPS, SOURCE_TYPE_ROUTER, ) - -ENTITY_ID_ALL_DEVICES = group.ENTITY_ID_FORMAT.format("all_devices") +from .legacy import DeviceScanner # noqa: F401 pylint: disable=unused-import SERVICE_SEE = "see" @@ -98,11 +94,9 @@ SERVICE_SEE_PAYLOAD_SCHEMA = vol.Schema( @bind_hass -def is_on(hass: HomeAssistantType, entity_id: str = None): +def is_on(hass: HomeAssistantType, entity_id: str): """Return the state if any or a specified device is home.""" - entity = entity_id or ENTITY_ID_ALL_DEVICES - - return hass.states.is_state(entity, STATE_HOME) + return hass.states.is_state(entity_id, STATE_HOME) def see( @@ -149,8 +143,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): if setup_tasks: await asyncio.wait(setup_tasks) - tracker.async_setup_group() - async def async_platform_discovered(p_type, info): """Load a platform.""" platform = await setup.async_create_platform_type(hass, config, p_type, {}) diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 9e53c2e0cea..6c5cacac591 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -3,12 +3,12 @@ from typing import Optional from homeassistant.components import zone from homeassistant.const import ( - STATE_NOT_HOME, - STATE_HOME, + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, + STATE_HOME, + STATE_NOT_HOME, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/device_tracker/device_condition.py b/homeassistant/components/device_tracker/device_condition.py index 6379aca6c0b..9bdfc12db39 100644 --- a/homeassistant/components/device_tracker/device_condition.py +++ b/homeassistant/components/device_tracker/device_condition.py @@ -1,21 +1,23 @@ """Provides device automations for Device tracker.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_NOT_HOME, + CONF_TYPE, STATE_HOME, + STATE_NOT_HOME, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_home", "is_not_home"} diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index ad7ff3fe3f5..b7d529f18ac 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -8,15 +8,6 @@ import voluptuous as vol from homeassistant import util from homeassistant.components import zone -from homeassistant.components.group import ( - ATTR_ADD_ENTITIES, - ATTR_ENTITIES, - ATTR_OBJECT_ID, - ATTR_VISIBLE, - DOMAIN as DOMAIN_GROUP, - SERVICE_SET, -) -from homeassistant.components.zone import async_active_zone from homeassistant.config import async_log_exception, load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, @@ -60,7 +51,6 @@ from .const import ( ) YAML_DEVICES = "known_devices.yaml" -GROUP_NAME_ALL_DEVICES = "all devices" EVENT_NEW_DEVICE = "device_tracker_new_device" @@ -104,7 +94,6 @@ class DeviceTracker: else defaults.get(CONF_TRACK_NEW, DEFAULT_TRACK_NEW) ) self.defaults = defaults - self.group = None self._is_updating = asyncio.Lock() for dev in devices: @@ -230,21 +219,6 @@ class DeviceTracker: if device.track: await device.async_update_ha_state() - # During init, we ignore the group - if self.group and self.track_new: - self.hass.async_create_task( - self.hass.async_call( - DOMAIN_GROUP, - SERVICE_SET, - { - ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), - ATTR_VISIBLE: False, - ATTR_NAME: GROUP_NAME_ALL_DEVICES, - ATTR_ADD_ENTITIES: [device.entity_id], - }, - ) - ) - self.hass.bus.async_fire( EVENT_NEW_DEVICE, { @@ -271,27 +245,6 @@ class DeviceTracker: update_config, self.hass.config.path(YAML_DEVICES), dev_id, device ) - @callback - def async_setup_group(self): - """Initialize group for all tracked devices. - - This method must be run in the event loop. - """ - entity_ids = [dev.entity_id for dev in self.devices.values() if dev.track] - - self.hass.async_create_task( - self.hass.services.async_call( - DOMAIN_GROUP, - SERVICE_SET, - { - ATTR_OBJECT_ID: util.slugify(GROUP_NAME_ALL_DEVICES), - ATTR_VISIBLE: False, - ATTR_NAME: GROUP_NAME_ALL_DEVICES, - ATTR_ENTITIES: entity_ids, - }, - ) - ) - @callback def async_update_stale(self, now: dt_util.dt.datetime): """Update stale devices. @@ -489,7 +442,7 @@ class Device(RestoreEntity): if self.location_name: self._state = self.location_name elif self.gps is not None and self.source_type == SOURCE_TYPE_GPS: - zone_state = async_active_zone( + zone_state = zone.async_active_zone( self.hass, self.gps[0], self.gps[1], self.gps_accuracy ) if zone_state is None: diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 0e1e0e8cd81..35b9a4a3fdb 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -1,11 +1,9 @@ { "domain": "device_tracker", - "name": "Device tracker", + "name": "Device Tracker", "documentation": "https://www.home-assistant.io/integrations/device_tracker", "requirements": [], - "dependencies": [ - "group", - "zone" - ], - "codeowners": [] + "dependencies": ["group", "zone"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index 6c9f05dead7..42751b1a784 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -1,27 +1,26 @@ """Device tracker helpers.""" import asyncio -from typing import Dict, Any, Callable, Optional from types import ModuleType +from typing import Any, Callable, Dict, Optional import attr -from homeassistant.core import callback -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.helpers import config_per_platform -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util import dt as dt_util from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE - +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform +from homeassistant.util import dt as dt_util from .const import ( - DOMAIN, - PLATFORM_TYPE_LEGACY, CONF_SCAN_INTERVAL, + DOMAIN, + LOGGER, + PLATFORM_TYPE_LEGACY, SCAN_INTERVAL, SOURCE_TYPE_ROUTER, - LOGGER, ) diff --git a/homeassistant/components/device_tracker/strings.json b/homeassistant/components/device_tracker/strings.json index 7e0691654a0..285bac2cb4b 100644 --- a/homeassistant/components/device_tracker/strings.json +++ b/homeassistant/components/device_tracker/strings.json @@ -1,8 +1,8 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_home": "{entity_name} is home", "is_not_home": "{entity_name} is not home" } } -} \ No newline at end of file +} diff --git a/homeassistant/components/dht/manifest.json b/homeassistant/components/dht/manifest.json index e1d9273b797..bb0e2b8f248 100644 --- a/homeassistant/components/dht/manifest.json +++ b/homeassistant/components/dht/manifest.json @@ -1,10 +1,8 @@ { "domain": "dht", - "name": "Dht", + "name": "DHT Sensor", "documentation": "https://www.home-assistant.io/integrations/dht", - "requirements": [ - "Adafruit-DHT==1.4.0" - ], + "requirements": ["Adafruit-DHT==1.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index 648e0e1ed72..26b0493cb99 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -1,13 +1,13 @@ """Support for Adafruit DHT temperature and humidity sensor.""" -import logging from datetime import timedelta +import logging import Adafruit_DHT # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit diff --git a/homeassistant/components/dialogflow/.translations/da.json b/homeassistant/components/dialogflow/.translations/da.json index 2fb203450a5..c682c07a8b9 100644 --- a/homeassistant/components/dialogflow/.translations/da.json +++ b/homeassistant/components/dialogflow/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Dialogflow meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Dialogflow-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 integration med Dialogflow]({dialogflow_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [webhook-integration med Dialogflow]({dialogflow_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/.translations/ko.json b/homeassistant/components/dialogflow/.translations/ko.json index 91f15f1fb77..2010495d959 100644 --- a/homeassistant/components/dialogflow/.translations/ko.json +++ b/homeassistant/components/dialogflow/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Dialogflow \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Dialogflow \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Dialogflow Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 45fee0f867e..ae3c0288aed 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -1,16 +1,15 @@ """Support for Dialogflow webhook.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import intent, template, config_entry_flow +from homeassistant.helpers import config_entry_flow, intent, template from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) SOURCE = "Home Assistant Dialogflow" diff --git a/homeassistant/components/dialogflow/config_flow.py b/homeassistant/components/dialogflow/config_flow.py index 4f785392ffc..fee99898ccc 100644 --- a/homeassistant/components/dialogflow/config_flow.py +++ b/homeassistant/components/dialogflow/config_flow.py @@ -1,7 +1,7 @@ """Config flow for DialogFlow.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/dialogflow/manifest.json b/homeassistant/components/dialogflow/manifest.json index d9e821c82fd..493351c2641 100644 --- a/homeassistant/components/dialogflow/manifest.json +++ b/homeassistant/components/dialogflow/manifest.json @@ -4,8 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/dialogflow", "requirements": [], - "dependencies": [ - "webhook" - ], + "dependencies": ["webhook"], "codeowners": [] } diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index bdb0c348803..33663f121d1 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -1,13 +1,13 @@ """Support for Digital Ocean.""" -import logging from datetime import timedelta +import logging import digitalocean import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/digital_ocean/manifest.json b/homeassistant/components/digital_ocean/manifest.json index eb19a5c3a85..8bf916a802d 100644 --- a/homeassistant/components/digital_ocean/manifest.json +++ b/homeassistant/components/digital_ocean/manifest.json @@ -1,12 +1,8 @@ { "domain": "digital_ocean", - "name": "Digital ocean", + "name": "Digital Ocean", "documentation": "https://www.home-assistant.io/integrations/digital_ocean", - "requirements": [ - "python-digitalocean==1.13.2" - ], + "requirements": ["python-digitalocean==1.13.2"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/digitalloggers/manifest.json b/homeassistant/components/digitalloggers/manifest.json index 4c58e090a95..723930666c4 100644 --- a/homeassistant/components/digitalloggers/manifest.json +++ b/homeassistant/components/digitalloggers/manifest.json @@ -1,10 +1,8 @@ { "domain": "digitalloggers", - "name": "Digitalloggers", + "name": "Digital Loggers", "documentation": "https://www.home-assistant.io/integrations/digitalloggers", - "requirements": [ - "dlipower==0.7.165" - ], + "requirements": ["dlipower==0.7.165"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index 10c8ce73a47..824af441688 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -1,17 +1,17 @@ """Support for Digital Loggers DIN III Relays.""" -import logging from datetime import timedelta +import logging import dlipower import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index 8b65d7a680b..adf05621a2c 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -1,10 +1,8 @@ { "domain": "directv", - "name": "Directv", + "name": "DirecTV", "documentation": "https://www.home-assistant.io/integrations/directv", - "requirements": [ - "directpy==0.5" - ], + "requirements": ["directpy==0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index 5dd673ca93f..cd4f910c707 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -129,7 +129,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) else: _LOGGER.debug( - "Adding discovered device %s with" " client address %s", + "Adding discovered device %s with client address %s", str.title(loc["locationName"]), loc["clientAddr"], ) @@ -214,7 +214,7 @@ class DirecTvDevice(MediaPlayerDevice): except requests.RequestException as ex: _LOGGER.error( - "%s: Request error trying to update current status: " "%s", + "%s: Request error trying to update current status: %s", self.entity_id, ex, ) diff --git a/homeassistant/components/discogs/manifest.json b/homeassistant/components/discogs/manifest.json index 5a1c670508a..61080b12c20 100644 --- a/homeassistant/components/discogs/manifest.json +++ b/homeassistant/components/discogs/manifest.json @@ -2,11 +2,7 @@ "domain": "discogs", "name": "Discogs", "documentation": "https://www.home-assistant.io/integrations/discogs", - "requirements": [ - "discogs_client==2.2.2" - ], + "requirements": ["discogs_client==2.2.2"], "dependencies": [], - "codeowners": [ - "@thibmaek" - ] + "codeowners": ["@thibmaek"] } diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index b5bf7eab6cc..a9aeea27aef 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,9 +2,7 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": [ - "discord.py==1.2.5" - ], + "requirements": ["discord.py==1.2.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index f35cf5b0ce9..864b7da5e55 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -5,15 +5,14 @@ import os.path import discord import voluptuous as vol -from homeassistant.const import CONF_TOKEN -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_TOKEN +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 884f6680d7c..965782d1228 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -6,19 +6,19 @@ Will emit EVENT_PLATFORM_DISCOVERED whenever a new service has been discovered. Knows which components handle certain types, will make sure they are loaded before the EVENT_PLATFORM_DISCOVERED is fired. """ -import json from datetime import timedelta +import json import logging from netdisco.discovery import NetworkDiscovery import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import async_discover, async_load_platform from homeassistant.helpers.event import async_track_point_in_utc_time -from homeassistant.helpers.discovery import async_load_platform, async_discover import homeassistant.util.dt as dt_util DOMAIN = "discovery" diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 56d968189cf..83a6222d357 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,9 +2,8 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": [ - "netdisco==2.6.0" - ], + "requirements": ["netdisco==2.6.0"], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 6e5c49e7aba..430878ca44f 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -10,10 +10,12 @@ from homeassistant.components.image_processing import ( CONF_SOURCE, ImageProcessingFaceEntity, ) +from homeassistant.core import split_entity_id # pylint: disable=unused-import -from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa: F401 -from homeassistant.core import split_entity_id +from homeassistant.components.image_processing import ( # noqa: F401, isort:skip + PLATFORM_SCHEMA, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dlib_face_detect/manifest.json b/homeassistant/components/dlib_face_detect/manifest.json index 431afc080f4..672368e0f8c 100644 --- a/homeassistant/components/dlib_face_detect/manifest.json +++ b/homeassistant/components/dlib_face_detect/manifest.json @@ -1,10 +1,8 @@ { "domain": "dlib_face_detect", - "name": "Dlib face detect", + "name": "Dlib Face Detect", "documentation": "https://www.home-assistant.io/integrations/dlib_face_detect", - "requirements": [ - "face_recognition==1.2.3" - ], + "requirements": ["face_recognition==1.2.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index d5b55b6a68c..d6fbf106b0c 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -1,20 +1,20 @@ """Component that will help set the Dlib face detect processing.""" -import logging import io +import logging # pylint: disable=import-error import face_recognition import voluptuous as vol -from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - PLATFORM_SCHEMA, - CONF_SOURCE, + CONF_CONFIDENCE, CONF_ENTITY_ID, CONF_NAME, - CONF_CONFIDENCE, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingFaceEntity, ) +from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dlib_face_identify/manifest.json b/homeassistant/components/dlib_face_identify/manifest.json index 2f764ae2817..f6c85a7e9a7 100644 --- a/homeassistant/components/dlib_face_identify/manifest.json +++ b/homeassistant/components/dlib_face_identify/manifest.json @@ -1,10 +1,8 @@ { "domain": "dlib_face_identify", - "name": "Dlib face identify", + "name": "Dlib Face Identify", "documentation": "https://www.home-assistant.io/integrations/dlib_face_identify", - "requirements": [ - "face_recognition==1.2.3" - ], + "requirements": ["face_recognition==1.2.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index b0b8c60121a..7f5ff6cfd02 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -1,10 +1,8 @@ { "domain": "dlink", - "name": "Dlink", + "name": "D-Link Wi-Fi Smart Plugs", "documentation": "https://www.home-assistant.io/integrations/dlink", - "requirements": [ - "pyW215==0.6.0" - ], + "requirements": ["pyW215==0.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index ac09b1afb48..8380c3b10fb 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -1,10 +1,8 @@ { "domain": "dlna_dmr", - "name": "Dlna dmr", + "name": "DLNA Digital Media Renderer", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", - "requirements": [ - "async-upnp-client==0.14.12" - ], + "requirements": ["async-upnp-client==0.14.12"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index a9ea9b21d59..28843aacbe4 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -198,7 +198,7 @@ class DlnaDmrDevice(MediaPlayerDevice): """Representation of a DLNA DMR device.""" def __init__(self, dmr_device, name=None): - """Initializer.""" + """Initialize DLNA DMR device.""" self._device = dmr_device self._name = name @@ -219,7 +219,7 @@ class DlnaDmrDevice(MediaPlayerDevice): return self._available async def _async_on_hass_stop(self, event): - """Event handler on HASS stop.""" + """Event handler on Home Assistant stop.""" with await self.hass.data[DLNA_DMR_DATA]["lock"]: await self._device.async_unsubscribe_services() diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 4f3d84da476..75d747da4ea 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -1,10 +1,8 @@ { "domain": "dnsip", - "name": "Dnsip", + "name": "DNS IP", "documentation": "https://www.home-assistant.io/integrations/dnsip", - "requirements": [ - "aiodns==2.0.0" - ], + "requirements": ["aiodns==2.0.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dominos/manifest.json b/homeassistant/components/dominos/manifest.json index 933af93a77a..0137cafc169 100644 --- a/homeassistant/components/dominos/manifest.json +++ b/homeassistant/components/dominos/manifest.json @@ -1,12 +1,8 @@ { "domain": "dominos", - "name": "Dominos", + "name": "Dominos Pizza", "documentation": "https://www.home-assistant.io/integrations/dominos", - "requirements": [ - "pizzapi==0.0.3" - ], - "dependencies": [ - "http" - ], + "requirements": ["pizzapi==0.0.3"], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index bd631f757d5..9525f9e8ddf 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -3,11 +3,10 @@ import io import logging import time -import voluptuous as vol from PIL import Image, ImageDraw from pydoods import PyDOODS +import voluptuous as vol -from homeassistant.const import CONF_TIMEOUT from homeassistant.components.image_processing import ( CONF_CONFIDENCE, CONF_ENTITY_ID, @@ -16,6 +15,7 @@ from homeassistant.components.image_processing import ( PLATFORM_SCHEMA, ImageProcessingEntity, ) +from homeassistant.const import CONF_TIMEOUT from homeassistant.core import split_entity_id from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index d92ff3d3692..680ee1354eb 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -67,8 +67,14 @@ def setup(hass, config): token = doorstation_config.get(CONF_TOKEN) name = doorstation_config.get(CONF_NAME) or "DoorBird {}".format(index + 1) - device = DoorBird(device_ip, username, password) - status = device.ready() + try: + device = DoorBird(device_ip, username, password) + status = device.ready() + except OSError as oserr: + _LOGGER.error( + "Failed to setup doorbird at %s: %s; not retrying", device_ip, oserr + ) + continue if status[0]: doorstation = ConfiguredDoorBird(device, name, events, custom_url, token) diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index c9cdb32e18a..97b54adb4ab 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -1,12 +1,8 @@ { "domain": "doorbird", - "name": "Doorbird", + "name": "DoorBird", "documentation": "https://www.home-assistant.io/integrations/doorbird", - "requirements": [ - "doorbirdpy==2.0.8" - ], - "dependencies": [], - "codeowners": [ - "@oblogic7" - ] + "requirements": ["doorbirdpy==2.0.8"], + "dependencies": ["http"], + "codeowners": ["@oblogic7"] } diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 03f12314c5a..b8d18d90833 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -1,18 +1,18 @@ """Support for Dovado router.""" -import logging from datetime import timedelta +import logging import dovado import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, CONF_HOST, + CONF_PASSWORD, CONF_PORT, + CONF_USERNAME, DEVICE_DEFAULT_NAME, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dovado/manifest.json b/homeassistant/components/dovado/manifest.json index ac0044c7a89..cc18e48d3b5 100644 --- a/homeassistant/components/dovado/manifest.json +++ b/homeassistant/components/dovado/manifest.json @@ -2,9 +2,7 @@ "domain": "dovado", "name": "Dovado", "documentation": "https://www.home-assistant.io/integrations/dovado", - "requirements": [ - "dovado==0.4.1" - ], + "requirements": ["dovado==0.4.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 9c725d9b3a2..9054943ca52 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -44,7 +44,7 @@ def setup(hass, config): """Listen for download events to download files.""" download_path = config[DOMAIN][CONF_DOWNLOAD_DIR] - # If path is relative, we assume relative to HASS config dir + # If path is relative, we assume relative to Home Assistant config dir if not os.path.isabs(download_path): download_path = hass.config.path(download_path) diff --git a/homeassistant/components/downloader/manifest.json b/homeassistant/components/downloader/manifest.json index 241dadddf4e..fde980fa5ca 100644 --- a/homeassistant/components/downloader/manifest.json +++ b/homeassistant/components/downloader/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/downloader", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/dsmr/manifest.json b/homeassistant/components/dsmr/manifest.json index 250adab263b..8f607dc299e 100644 --- a/homeassistant/components/dsmr/manifest.json +++ b/homeassistant/components/dsmr/manifest.json @@ -1,10 +1,8 @@ { "domain": "dsmr", - "name": "Dsmr", + "name": "DSMR Slimme Meter", "documentation": "https://www.home-assistant.io/integrations/dsmr", - "requirements": [ - "dsmr_parser==0.12" - ], + "requirements": ["dsmr_parser==0.12"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dsmr_reader/__init__.py b/homeassistant/components/dsmr_reader/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/dsmr_reader/manifest.json b/homeassistant/components/dsmr_reader/manifest.json old mode 100755 new mode 100644 index f1c52e02c83..0ec70b027ba --- a/homeassistant/components/dsmr_reader/manifest.json +++ b/homeassistant/components/dsmr_reader/manifest.json @@ -1,12 +1,8 @@ { - "domain": "dsmr_reader", - "name": "DSMR Reader", - "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", - "requirements": [], - "dependencies": [ - "mqtt" - ], - "codeowners": [ - "@depl0y" - ] + "domain": "dsmr_reader", + "name": "DSMR Reader", + "documentation": "https://www.home-assistant.io/integrations/dsmr_reader", + "requirements": [], + "dependencies": ["mqtt"], + "codeowners": ["@depl0y"] } diff --git a/homeassistant/components/dsmr_reader/sensor.py b/homeassistant/components/dsmr_reader/sensor.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/dte_energy_bridge/manifest.json b/homeassistant/components/dte_energy_bridge/manifest.json index f3ccbdeebb2..c056c7cbeb6 100644 --- a/homeassistant/components/dte_energy_bridge/manifest.json +++ b/homeassistant/components/dte_energy_bridge/manifest.json @@ -1,6 +1,6 @@ { "domain": "dte_energy_bridge", - "name": "Dte energy bridge", + "name": "DTE Energy Bridge", "documentation": "https://www.home-assistant.io/integrations/dte_energy_bridge", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/dublin_bus_transport/manifest.json b/homeassistant/components/dublin_bus_transport/manifest.json index 3e377f3a2ea..f4412b6933e 100644 --- a/homeassistant/components/dublin_bus_transport/manifest.json +++ b/homeassistant/components/dublin_bus_transport/manifest.json @@ -1,6 +1,6 @@ { "domain": "dublin_bus_transport", - "name": "Dublin bus transport", + "name": "Dublin Bus", "documentation": "https://www.home-assistant.io/integrations/dublin_bus_transport", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/dublin_bus_transport/sensor.py b/homeassistant/components/dublin_bus_transport/sensor.py index 203cfb1e27c..a5fe8fd6b30 100644 --- a/homeassistant/components/dublin_bus_transport/sensor.py +++ b/homeassistant/components/dublin_bus_transport/sensor.py @@ -7,17 +7,17 @@ https://data.gov.ie/dataset/real-time-passenger-information-rtpi-for-dublin-bus- For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.dublin_public_transport/ """ +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime 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_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://data.dublinked.ie/cgi-bin/rtpi/realtimebusinformation" diff --git a/homeassistant/components/duckdns/__init__.py b/homeassistant/components/duckdns/__init__.py index 171d17faff9..b3da1ec2752 100644 --- a/homeassistant/components/duckdns/__init__.py +++ b/homeassistant/components/duckdns/__init__.py @@ -1,14 +1,14 @@ """Integrate with DuckDNS.""" -import logging from asyncio import iscoroutinefunction from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN -from homeassistant.core import callback, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv 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 diff --git a/homeassistant/components/duckdns/manifest.json b/homeassistant/components/duckdns/manifest.json index 9e0ad6c001c..f6ab4e3a570 100644 --- a/homeassistant/components/duckdns/manifest.json +++ b/homeassistant/components/duckdns/manifest.json @@ -1,6 +1,6 @@ { "domain": "duckdns", - "name": "Duckdns", + "name": "Duck DNS", "documentation": "https://www.home-assistant.io/integrations/duckdns", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/duke_energy/manifest.json b/homeassistant/components/duke_energy/manifest.json index 131063ad81f..cebbf45df11 100644 --- a/homeassistant/components/duke_energy/manifest.json +++ b/homeassistant/components/duke_energy/manifest.json @@ -1,10 +1,8 @@ { "domain": "duke_energy", - "name": "Duke energy", + "name": "Duke Energy", "documentation": "https://www.home-assistant.io/integrations/duke_energy", - "requirements": [ - "pydukeenergy==0.0.6" - ], + "requirements": ["pydukeenergy==0.0.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dunehd/manifest.json b/homeassistant/components/dunehd/manifest.json index 47250b32cbc..0160d5ec918 100644 --- a/homeassistant/components/dunehd/manifest.json +++ b/homeassistant/components/dunehd/manifest.json @@ -1,10 +1,8 @@ { "domain": "dunehd", - "name": "Dunehd", + "name": "DuneHD", "documentation": "https://www.home-assistant.io/integrations/dunehd", - "requirements": [ - "pdunehd==1.3" - ], + "requirements": ["pdunehd==1.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dwd_weather_warnings/manifest.json b/homeassistant/components/dwd_weather_warnings/manifest.json index a35fcbcdf68..19dcf2860d7 100644 --- a/homeassistant/components/dwd_weather_warnings/manifest.json +++ b/homeassistant/components/dwd_weather_warnings/manifest.json @@ -1,6 +1,6 @@ { "domain": "dwd_weather_warnings", - "name": "Dwd weather warnings", + "name": "Deutsche Wetter Dienst (DWD) Weather Warnings", "documentation": "https://www.home-assistant.io/integrations/dwd_weather_warnings", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/dwd_weather_warnings/sensor.py b/homeassistant/components/dwd_weather_warnings/sensor.py index 2484ba70074..695b839d18c 100644 --- a/homeassistant/components/dwd_weather_warnings/sensor.py +++ b/homeassistant/components/dwd_weather_warnings/sensor.py @@ -12,20 +12,20 @@ Unwetterwarnungen (Stufe 3) Warnungen vor markantem Wetter (Stufe 2) Wetterwarnungen (Stufe 1) """ -import logging -import json from datetime import timedelta +import json +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT -from homeassistant.helpers.entity import Entity +from homeassistant.components.rest.sensor import RestData from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, CONF_MONITORED_CONDITIONS +from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME +from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE as HA_USER_AGENT +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.util.dt as dt_util -from homeassistant.components.rest.sensor import RestData _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/dweet/manifest.json b/homeassistant/components/dweet/manifest.json index 75d8cfb6054..be21605196a 100644 --- a/homeassistant/components/dweet/manifest.json +++ b/homeassistant/components/dweet/manifest.json @@ -1,12 +1,8 @@ { "domain": "dweet", - "name": "Dweet", + "name": "dweet.io", "documentation": "https://www.home-assistant.io/integrations/dweet", - "requirements": [ - "dweepy==0.3.0" - ], + "requirements": ["dweepy==0.3.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index a5dde58d30f..fbe7897e6bb 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -89,7 +89,7 @@ def setup(hass, config): # Not yet reliable for device in dyson_devices: _LOGGER.info( - "Trying to connect to device %s with timeout=%i " "and retry=%i", + "Trying to connect to device %s with timeout=%i and retry=%i", device, timeout, retry, diff --git a/homeassistant/components/dyson/manifest.json b/homeassistant/components/dyson/manifest.json index 92940c8c1e1..915c6aa3b79 100644 --- a/homeassistant/components/dyson/manifest.json +++ b/homeassistant/components/dyson/manifest.json @@ -2,9 +2,7 @@ "domain": "dyson", "name": "Dyson", "documentation": "https://www.home-assistant.io/integrations/dyson", - "requirements": [ - "libpurecool==0.5.0" - ], + "requirements": ["libpurecool==0.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index c51f46c7790..2fdd3cd6c1f 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -195,5 +195,5 @@ class DysonAirQualitySensor(DysonSensor): def state(self): """Return Air Quality value.""" if self._device.environmental_state: - return self._device.environmental_state.volatil_organic_compounds + return int(self._device.environmental_state.volatil_organic_compounds) return None diff --git a/homeassistant/components/ebox/manifest.json b/homeassistant/components/ebox/manifest.json index d32206da726..706bca862df 100644 --- a/homeassistant/components/ebox/manifest.json +++ b/homeassistant/components/ebox/manifest.json @@ -1,10 +1,8 @@ { "domain": "ebox", - "name": "Ebox", + "name": "EBox", "documentation": "https://www.home-assistant.io/integrations/ebox", - "requirements": [ - "pyebox==1.1.4" - ], + "requirements": ["pyebox==1.1.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index e4d0bdbcdb1..eafa42ba22a 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -34,7 +34,7 @@ def verify_ebusd_config(config): circuit = config[CONF_CIRCUIT] for condition in config[CONF_MONITORED_CONDITIONS]: if condition not in SENSOR_TYPES[circuit]: - raise vol.Invalid("Condition '" + condition + "' not in '" + circuit + "'.") + raise vol.Invalid(f"Condition '{condition}' not in '{circuit}'.") return config diff --git a/homeassistant/components/ebusd/manifest.json b/homeassistant/components/ebusd/manifest.json index b9be062d3e2..dc3f34e9ed9 100644 --- a/homeassistant/components/ebusd/manifest.json +++ b/homeassistant/components/ebusd/manifest.json @@ -1,10 +1,8 @@ { "domain": "ebusd", - "name": "Ebusd", + "name": "ebusd", "documentation": "https://www.home-assistant.io/integrations/ebusd", - "requirements": [ - "ebusdpy==0.0.16" - ], + "requirements": ["ebusdpy==0.0.16"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ecoal_boiler/__init__.py b/homeassistant/components/ecoal_boiler/__init__.py index ed8e315bfed..b0ca7aec5cc 100644 --- a/homeassistant/components/ecoal_boiler/__init__.py +++ b/homeassistant/components/ecoal_boiler/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.discovery import load_platform _LOGGER = logging.getLogger(__name__) DOMAIN = "ecoal_boiler" -DATA_ECOAL_BOILER = "data_" + DOMAIN +DATA_ECOAL_BOILER = f"data_{DOMAIN}" DEFAULT_USERNAME = "admin" DEFAULT_PASSWORD = "admin" @@ -91,7 +91,7 @@ def setup(hass, hass_config): if ecoal_contr.version is None: # Wrong credentials nor network config _LOGGER.error( - "Unable to read controller status from %s@%s" " (wrong host/credentials)", + "Unable to read controller status from %s@%s (wrong host/credentials)", username, host, ) diff --git a/homeassistant/components/ecoal_boiler/manifest.json b/homeassistant/components/ecoal_boiler/manifest.json index c1bf938dc34..11820f781d7 100644 --- a/homeassistant/components/ecoal_boiler/manifest.json +++ b/homeassistant/components/ecoal_boiler/manifest.json @@ -1,10 +1,8 @@ { "domain": "ecoal_boiler", - "name": "Ecoal boiler", + "name": "eSterownik eCoal.pl Boiler", "documentation": "https://www.home-assistant.io/integrations/ecoal_boiler", - "requirements": [ - "ecoaliface==0.4.0" - ], + "requirements": ["ecoaliface==0.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ecoal_boiler/switch.py b/homeassistant/components/ecoal_boiler/switch.py index 9f286e625a5..00bfd7f3e5b 100644 --- a/homeassistant/components/ecoal_boiler/switch.py +++ b/homeassistant/components/ecoal_boiler/switch.py @@ -38,7 +38,7 @@ class EcoalSwitch(SwitchDevice): # set_() # as attribute name in status instance: # status. - self._contr_set_fun = getattr(self._ecoal_contr, "set_" + state_attr) + self._contr_set_fun = getattr(self._ecoal_contr, f"set_{state_attr}") # No value set, will be read from controller instead self._state = None diff --git a/homeassistant/components/ecobee/.translations/da.json b/homeassistant/components/ecobee/.translations/da.json index 7a42a9470db..614811db45a 100644 --- a/homeassistant/components/ecobee/.translations/da.json +++ b/homeassistant/components/ecobee/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "one_instance_only": "Integrationen underst\u00f8tter kun \u00e9n ecobee forekomst" + "one_instance_only": "Denne integration underst\u00f8tter i \u00f8jeblikket kun en ecobee-instans." }, "error": { "pin_request_failed": "Fejl ved anmodning om pinkode fra ecobee. Kontroller at API-n\u00f8glen er korrekt.", @@ -9,7 +9,8 @@ }, "step": { "authorize": { - "title": "Autoriser app p\u00e5 ecobee.com" + "description": "Godkend denne app p\u00e5 https://www.ecobee.com/consumerportal/index.html med PIN-kode:\n\n{pin}\n\nTryk derefter p\u00e5 Indsend.", + "title": "Godkend app p\u00e5 ecobee.com" }, "user": { "data": { diff --git a/homeassistant/components/ecobee/__init__.py b/homeassistant/components/ecobee/__init__.py index eb65a7ed426..80c3be7954b 100644 --- a/homeassistant/components/ecobee/__init__.py +++ b/homeassistant/components/ecobee/__init__.py @@ -1,9 +1,9 @@ """Support for ecobee.""" import asyncio from datetime import timedelta -import voluptuous as vol -from pyecobee import Ecobee, ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, ExpiredTokenError +from pyecobee import ECOBEE_API_KEY, ECOBEE_REFRESH_TOKEN, Ecobee, ExpiredTokenError +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_API_KEY @@ -11,11 +11,11 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle from .const import ( + _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, ECOBEE_PLATFORMS, - _LOGGER, ) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=180) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 06289572aea..f7a24886b84 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -1,10 +1,10 @@ """Support for Ecobee binary sensors.""" from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_OCCUPANCY, + BinarySensorDevice, ) -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index c583f9696d2..5915e64334f 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -6,37 +6,37 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_AUTO, - HVAC_MODE_OFF, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_AUX_HEAT, - SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_FAN_MODE, - PRESET_AWAY, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_AUTO, FAN_ON, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - SUPPORT_PRESET_MODE, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_AWAY, PRESET_NONE, - CURRENT_HVAC_FAN, - CURRENT_HVAC_DRY, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, ATTR_TEMPERATURE, + STATE_ON, TEMP_FAHRENHEIT, ) -from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv +from homeassistant.util.temperature import convert -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER from .util import ecobee_date, ecobee_time ATTR_COOL_TEMP = "cool_temp" @@ -550,7 +550,7 @@ class Thermostat(ClimateDevice): self.hold_preference(), ) _LOGGER.debug( - "Setting ecobee hold_temp to: heat=%s, is=%s, " "cool=%s, is=%s", + "Setting ecobee hold_temp to: heat=%s, is=%s, cool=%s, is=%s", heat_temp, isinstance(heat_temp, (int, float)), cool_temp, diff --git a/homeassistant/components/ecobee/config_flow.py b/homeassistant/components/ecobee/config_flow.py index 56ce13f7701..bb406d81e3a 100644 --- a/homeassistant/components/ecobee/config_flow.py +++ b/homeassistant/components/ecobee/config_flow.py @@ -1,19 +1,18 @@ """Config flow to configure ecobee.""" -import voluptuous as vol - from pyecobee import ( - Ecobee, - ECOBEE_CONFIG_FILENAME, ECOBEE_API_KEY, + ECOBEE_CONFIG_FILENAME, ECOBEE_REFRESH_TOKEN, + Ecobee, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistantError from homeassistant.util.json import load_json -from .const import CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN, _LOGGER +from .const import _LOGGER, CONF_REFRESH_TOKEN, DATA_ECOBEE_CONFIG, DOMAIN class EcobeeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 05c2d22b594..f380c9bbef3 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -27,7 +27,7 @@ ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] MANUFACTURER = "ecobee" -# Translates ecobee API weatherSymbol to HASS usable names +# Translates ecobee API weatherSymbol to Home Assistant usable names # https://www.ecobee.com/home/developer/api/documentation/v1/objects/WeatherForecast.shtml ECOBEE_WEATHER_SYMBOL_TO_HASS = { 0: "sunny", diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index bc87b3cd64e..32b58964926 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -1,9 +1,9 @@ { - "domain": "ecobee", - "name": "Ecobee", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/ecobee", - "dependencies": [], - "requirements": ["python-ecobee-api==0.1.4"], - "codeowners": ["@marthoc"] + "domain": "ecobee", + "name": "Ecobee", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ecobee", + "dependencies": [], + "requirements": ["python-ecobee-api==0.1.4"], + "codeowners": ["@marthoc"] } diff --git a/homeassistant/components/ecobee/notify.py b/homeassistant/components/ecobee/notify.py index c7b3f47d29c..a8f53a027b3 100644 --- a/homeassistant/components/ecobee/notify.py +++ b/homeassistant/components/ecobee/notify.py @@ -1,8 +1,8 @@ """Support for Ecobee Send Message service.""" import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import BaseNotificationService, PLATFORM_SCHEMA from .const import CONF_INDEX, DOMAIN diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 76945080bfa..37201ec2121 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -8,7 +8,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER +from .const import _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], diff --git a/homeassistant/components/ecobee/util.py b/homeassistant/components/ecobee/util.py index 3acc3e5676d..2f5d194fec0 100644 --- a/homeassistant/components/ecobee/util.py +++ b/homeassistant/components/ecobee/util.py @@ -1,5 +1,6 @@ """Validation utility functions for ecobee services.""" from datetime import datetime + import voluptuous as vol diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 7b057f09a0c..a571e854f73 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -15,11 +15,11 @@ from homeassistant.components.weather import ( from homeassistant.const import TEMP_FAHRENHEIT from .const import ( + _LOGGER, DOMAIN, ECOBEE_MODEL_TO_NAME, ECOBEE_WEATHER_SYMBOL_TO_HASS, MANUFACTURER, - _LOGGER, ) diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 7ce52c021a1..d9ce5253e95 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -1,10 +1,8 @@ { "domain": "econet", - "name": "Econet", + "name": "Rheem EcoNET Water Products", "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": [ - "pyeconet==0.0.11" - ], + "requirements": ["pyeconet==0.0.11"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 5de2390dd30..637ee001ca3 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -2,11 +2,7 @@ "domain": "ecovacs", "name": "Ecovacs", "documentation": "https://www.home-assistant.io/integrations/ecovacs", - "requirements": [ - "sucks==0.9.4" - ], + "requirements": ["sucks==0.9.4"], "dependencies": [], - "codeowners": [ - "@OverloadUT" - ] + "codeowners": ["@OverloadUT"] } diff --git a/homeassistant/components/ecovacs/vacuum.py b/homeassistant/components/ecovacs/vacuum.py index 16a9d67bffc..a74fdaa21ba 100644 --- a/homeassistant/components/ecovacs/vacuum.py +++ b/homeassistant/components/ecovacs/vacuum.py @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): vacuums = [] for device in hass.data[ECOVACS_DEVICES]: vacuums.append(EcovacsVacuum(device)) - _LOGGER.debug("Adding Ecovacs Vacuums to Hass: %s", vacuums) + _LOGGER.debug("Adding Ecovacs Vacuums to Home Assistant: %s", vacuums) add_entities(vacuums, True) diff --git a/homeassistant/components/eddystone_temperature/manifest.json b/homeassistant/components/eddystone_temperature/manifest.json index 36918fa5ee5..7cc210c7053 100644 --- a/homeassistant/components/eddystone_temperature/manifest.json +++ b/homeassistant/components/eddystone_temperature/manifest.json @@ -1,11 +1,8 @@ { "domain": "eddystone_temperature", - "name": "Eddystone temperature", + "name": "Eddystone", "documentation": "https://www.home-assistant.io/integrations/eddystone_temperature", - "requirements": [ - "beacontools[scan]==1.2.3", - "construct==2.9.45" - ], + "requirements": ["beacontools[scan]==1.2.3", "construct==2.9.45"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/eddystone_temperature/sensor.py b/homeassistant/components/eddystone_temperature/sensor.py index 2084e307029..22d3533d32f 100644 --- a/homeassistant/components/eddystone_temperature/sensor.py +++ b/homeassistant/components/eddystone_temperature/sensor.py @@ -10,11 +10,7 @@ https://home-assistant.io/components/sensor.eddystone_temperature/ import logging # pylint: disable=import-error -from beacontools import ( - BeaconScanner, - EddystoneFilter, - EddystoneTLMFrame, -) +from beacontools import BeaconScanner, EddystoneFilter, EddystoneTLMFrame import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/edimax/manifest.json b/homeassistant/components/edimax/manifest.json index 6d1d444d2f4..20036311592 100644 --- a/homeassistant/components/edimax/manifest.json +++ b/homeassistant/components/edimax/manifest.json @@ -2,9 +2,7 @@ "domain": "edimax", "name": "Edimax", "documentation": "https://www.home-assistant.io/integrations/edimax", - "requirements": [ - "pyedimax==0.1" - ], + "requirements": ["pyedimax==0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ee_brightbox/manifest.json b/homeassistant/components/ee_brightbox/manifest.json index d4ae0c9d6df..b0a313a939f 100644 --- a/homeassistant/components/ee_brightbox/manifest.json +++ b/homeassistant/components/ee_brightbox/manifest.json @@ -1,10 +1,8 @@ { "domain": "ee_brightbox", - "name": "Ee brightbox", + "name": "EE Bright Box", "documentation": "https://www.home-assistant.io/integrations/ee_brightbox", - "requirements": [ - "eebrightbox==0.0.4" - ], + "requirements": ["eebrightbox==0.0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/efergy/sensor.py b/homeassistant/components/efergy/sensor.py index 43c3b67457a..3be962fea2f 100644 --- a/homeassistant/components/efergy/sensor.py +++ b/homeassistant/components/efergy/sensor.py @@ -5,7 +5,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_CURRENCY, POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CONF_CURRENCY, ENERGY_KILO_WATT_HOUR, POWER_WATT import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/egardia/__init__.py b/homeassistant/components/egardia/__init__.py index efe47736479..8b67d23d3cc 100644 --- a/homeassistant/components/egardia/__init__.py +++ b/homeassistant/components/egardia/__init__.py @@ -110,7 +110,7 @@ def setup(hass, config): bound = server.bind() if not bound: raise OSError( - "Binding error occurred while " + "starting EgardiaServer." + "Binding error occurred while starting EgardiaServer." ) hass.data[EGARDIA_SERVER] = server server.start() @@ -119,7 +119,7 @@ def setup(hass, config): """Handle Home Assistant stop event.""" server.stop() - # listen to home assistant stop event + # listen to Home Assistant stop event hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, handle_stop_event) except OSError: diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index 2c18be47a1f..7e5f88cff3e 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -139,7 +139,7 @@ class EgardiaAlarm(alarm.AlarmControlPanel): self._egardiasystem.alarm_disarm() except requests.exceptions.RequestException as err: _LOGGER.error( - "Egardia device exception occurred when " "sending disarm command: %s", + "Egardia device exception occurred when sending disarm command: %s", err, ) diff --git a/homeassistant/components/eight_sleep/manifest.json b/homeassistant/components/eight_sleep/manifest.json index 25961f15a0a..75998e71e5f 100644 --- a/homeassistant/components/eight_sleep/manifest.json +++ b/homeassistant/components/eight_sleep/manifest.json @@ -1,12 +1,8 @@ { "domain": "eight_sleep", - "name": "Eight sleep", + "name": "Eight Sleep", "documentation": "https://www.home-assistant.io/integrations/eight_sleep", - "requirements": [ - "pyeight==0.1.2" - ], + "requirements": ["pyeight==0.1.2"], "dependencies": [], - "codeowners": [ - "@mezz64" - ] + "codeowners": ["@mezz64"] } diff --git a/homeassistant/components/elgato/.translations/ca.json b/homeassistant/components/elgato/.translations/ca.json new file mode 100644 index 00000000000..b717a5abade --- /dev/null +++ b/homeassistant/components/elgato/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest dispositiu Elgato Key Light ja est\u00e0 configurat.", + "connection_error": "No s'ha pogut connectar amb el dispositiu Elgato Key Light." + }, + "error": { + "connection_error": "No s'ha pogut connectar amb el dispositiu Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP", + "port": "N\u00famero de port" + }, + "description": "Configura l'Elgato Key Light per integrar-lo amb Home Assistant.", + "title": "Enlla\u00e7a Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vols afegir l'Elgato Key Light amb n\u00famero de s\u00e8rie `{serial_number}` a Home Assistant?", + "title": "S'ha descobert un dispositiu Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/da.json b/homeassistant/components/elgato/.translations/da.json new file mode 100644 index 00000000000..a10e4d9e89f --- /dev/null +++ b/homeassistant/components/elgato/.translations/da.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Elgato Key Light-enhed er allerede konfigureret.", + "connection_error": "Kunne ikke oprette forbindelse til Elgato Key Light-enheden." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til Elgato Key Light-enheden." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse", + "port": "Portnummer" + }, + "description": "Indstil din Elgato Key Light til at integrere med Home Assistant.", + "title": "Forbind din Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vil du tilf\u00f8je Elgato Key Light med serienummer `{serial_number}` til Home Assistant?", + "title": "Fandt Elgato Key Light-enhed" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/de.json b/homeassistant/components/elgato/.translations/de.json new file mode 100644 index 00000000000..dd6344916de --- /dev/null +++ b/homeassistant/components/elgato/.translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert.", + "connection_error": "Verbindung zum Elgato Key Light-Ger\u00e4t fehlgeschlagen." + }, + "error": { + "connection_error": "Verbindung zum Elgato Key Light-Ger\u00e4t fehlgeschlagen." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host oder IP-Adresse", + "port": "Port-Nummer" + }, + "description": "Richten Sie Ihr Elgato Key Light f\u00fcr die Integration mit Home Assistant ein.", + "title": "Verkn\u00fcpfen Sie Ihr Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "M\u00f6chten Sie das Elgato Key Light mit der Seriennummer \"{serial_number} \" zu Home Assistant hinzuf\u00fcgen?", + "title": "Elgato Key Light Ger\u00e4t entdeckt" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/en.json b/homeassistant/components/elgato/.translations/en.json new file mode 100644 index 00000000000..d52003d10e1 --- /dev/null +++ b/homeassistant/components/elgato/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "This Elgato Key Light device is already configured.", + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "error": { + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host or IP address", + "port": "Port number" + }, + "description": "Set up your Elgato Key Light to integrate with Home Assistant.", + "title": "Link your Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Key Light device" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/es.json b/homeassistant/components/elgato/.translations/es.json new file mode 100644 index 00000000000..2e689b5e064 --- /dev/null +++ b/homeassistant/components/elgato/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo Elgato Key Light ya est\u00e1 configurado.", + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "error": { + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "port": "N\u00famero de puerto" + }, + "description": "Configura tu Elgato Key Light para integrarlo con Home Assistant.", + "title": "Conecte su Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "\u00bfDesea agregar Elgato Key Light con el n\u00famero de serie `{serial_number}` a Home Assistant?", + "title": "Descubierto dispositivo Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/fr.json b/homeassistant/components/elgato/.translations/fr.json new file mode 100644 index 00000000000..e8465a56728 --- /dev/null +++ b/homeassistant/components/elgato/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cet appareil Elgato Key Light est d\u00e9j\u00e0 configur\u00e9.", + "connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Elgato Key Light." + }, + "error": { + "connection_error": "Impossible de se connecter au p\u00e9riph\u00e9rique Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "H\u00f4te ou adresse IP", + "port": "Port" + }, + "description": "Configurez votre Elgato Key Light pour l'int\u00e9grer \u00e0 Home Assistant.", + "title": "Associez votre Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Voulez-vous ajouter l'Elgato Key Light avec le num\u00e9ro de s\u00e9rie `{serial_number}` \u00e0 Home Assistant?", + "title": "Appareil Elgato Key Light d\u00e9couvert" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/it.json b/homeassistant/components/elgato/.translations/it.json new file mode 100644 index 00000000000..81e363aa01b --- /dev/null +++ b/homeassistant/components/elgato/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Questo dispositivo Elgato Key Light \u00e8 gi\u00e0 configurato.", + "connection_error": "Impossibile connettersi al dispositivo Elgato Key Light." + }, + "error": { + "connection_error": "Impossibile connettersi al dispositivo Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Host o indirizzo IP", + "port": "Numero porta" + }, + "description": "Configura Elgato Key Light per l'integrazione con Home Assistant.", + "title": "Collega il tuo Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vuoi aggiungere il dispositivo Elgato Key Light con il numero di serie {serial_number} a Home Assistant?", + "title": "Dispositivo Elgato Key Light rilevato" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/ko.json b/homeassistant/components/elgato/.translations/ko.json new file mode 100644 index 00000000000..9d7ab4ef2b0 --- /dev/null +++ b/homeassistant/components/elgato/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Elgato Key Light \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "Elgato Key Light \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "Elgato Key Light \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8 \ubc88\ud638" + }, + "description": "Home Assistant \uc5d0 Elgato Key Light \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "Elgato Key Light \uc5f0\uacb0" + }, + "zeroconf_confirm": { + "description": "Elgato Key Light \uc2dc\ub9ac\uc5bc \ubc88\ud638 `{serial_number}` \uc744(\ub97c) Home Assistant \uc5d0 \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c Elgato Key Light \uae30\uae30" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/lb.json b/homeassistant/components/elgato/.translations/lb.json new file mode 100644 index 00000000000..d53eea87c4c --- /dev/null +++ b/homeassistant/components/elgato/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebsen Elgato Key Light Apparat ass scho konfigur\u00e9iert.", + "connection_error": "Feeler beim verbannen mam Elgato key Light Apparat." + }, + "error": { + "connection_error": "Feeler beim verbannen mam Elgato key Light Apparat." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Numm oder IP Adresse", + "port": "Port Nummer" + }, + "description": "\u00c4ren Elgator Key Light als Integratioun mam Home Assistant ariichten.", + "title": "\u00c4ren Elgato Key Light verbannen" + }, + "zeroconf_confirm": { + "description": "W\u00ebllt dir den Elgato Key Light mat der Seriennummer `{serial_number}` am 'Home Assistant dob\u00e4isetzen?", + "title": "Entdeckten Elgato Key Light Apparat" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/nl.json b/homeassistant/components/elgato/.translations/nl.json new file mode 100644 index 00000000000..ca05983eeb5 --- /dev/null +++ b/homeassistant/components/elgato/.translations/nl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Dit Elgato Key Light apparaat is al geconfigureerd.", + "connection_error": "Kan geen verbinding maken met het Elgato Key Light apparaat." + }, + "error": { + "connection_error": "Kan geen verbinding maken met het Elgato Key Light apparaat." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Hostnaam of IP-adres", + "port": "Poortnummer" + }, + "description": "Stel uw Elgato Key Light in om te integreren met Home Assistant.", + "title": "Koppel uw Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Wilt u de Elgato Key Light met serienummer ` {serial_number} ` toevoegen aan Home Assistant?", + "title": "Elgato Key Light apparaat ontdekt" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/no.json b/homeassistant/components/elgato/.translations/no.json new file mode 100644 index 00000000000..8642ae75025 --- /dev/null +++ b/homeassistant/components/elgato/.translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Elgato Key Light-enheten er allerede konfigurert.", + "connection_error": "Kunne ikke koble til Elgato Key Light-enheten." + }, + "error": { + "connection_error": "Kunne ikke koble til Elgato Key Light-enheten." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Vert eller IP-adresse", + "port": "Portnummer" + }, + "description": "Sett opp Elgato Key Light for \u00e5 integrere med Home Assistant.", + "title": "Linken ditt Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Vil du legge Elgato Key Light med serienummer ` {serial_number} til Home Assistant?", + "title": "Oppdaget Elgato Key Light-enheten" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/pl.json b/homeassistant/components/elgato/.translations/pl.json new file mode 100644 index 00000000000..97e10b451f0 --- /dev/null +++ b/homeassistant/components/elgato/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "To urz\u0105dzenie Elgato Key Light jest ju\u017c skonfigurowane.", + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light." + }, + "error": { + "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia z urz\u0105dzeniem Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port" + }, + "description": "Konfiguracja Elgato Key Light w celu integracji z Home Assistant'em.", + "title": "Po\u0142\u0105cz swoje Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Czy chcesz doda\u0107 urz\u0105dzenie Elgato Key Light o numerze seryjnym `{serial_number}` do Home Assistant'a?", + "title": "Wykryto urz\u0105dzenie Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/pt-BR.json b/homeassistant/components/elgato/.translations/pt-BR.json new file mode 100644 index 00000000000..d809647c99f --- /dev/null +++ b/homeassistant/components/elgato/.translations/pt-BR.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "zeroconf_confirm": { + "description": "Deseja adicionar o Elgato Key Light n\u00famero de s\u00e9rie ` {serial_number} ` ao Home Assistant?", + "title": "Dispositivo Elgato Key Light descoberto" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/ru.json b/homeassistant/components/elgato/.translations/ru.json new file mode 100644 index 00000000000..2b5fb72c507 --- /dev/null +++ b/homeassistant/components/elgato/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041d\u043e\u043c\u0435\u0440 \u043f\u043e\u0440\u0442\u0430" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Elgato Key Light \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "title": "Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c Elgato Key Light \u0441 \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", + "title": "\u041d\u0430\u0439\u0434\u0435\u043d\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Elgado Key Light" + } + }, + "title": "\u041e\u0441\u0432\u0435\u0442\u0438\u0442\u0435\u043b\u044c Elgado Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/sl.json b/homeassistant/components/elgato/.translations/sl.json new file mode 100644 index 00000000000..f05b0bcbd8f --- /dev/null +++ b/homeassistant/components/elgato/.translations/sl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ta naprava Elgato Key Light je \u017ee nastavljena.", + "connection_error": "Povezava z napravo Elgato Key Light ni uspela." + }, + "error": { + "connection_error": "Povezava z napravo Elgato Key Light ni uspela." + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "Gostitelj ali IP naslov", + "port": "\u0160tevilka vrat" + }, + "description": "Nastavite svojo Elgato Key Light tako, da se bo vklju\u010dila v Home Assistant.", + "title": "Pove\u017eite svojo Elgato Key Light" + }, + "zeroconf_confirm": { + "description": "Ali \u017eelite dodati Elgato Key Light s serijsko \u0161tevilko ' {serial_number} ' v Home Assistant-a?", + "title": "Odkrita naprava Elgato Key Light" + } + }, + "title": "Elgato Key Light" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/.translations/zh-Hant.json b/homeassistant/components/elgato/.translations/zh-Hant.json new file mode 100644 index 00000000000..b187abc5ccd --- /dev/null +++ b/homeassistant/components/elgato/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Elgato Key \u7167\u660e\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "connection_error": "Elgato Key \u7167\u660e\u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "error": { + "connection_error": "Elgato Key \u7167\u660e\u8a2d\u5099\u9023\u7dda\u5931\u6557\u3002" + }, + "flow_title": "Elgato Key \u7167\u660e\uff1a{serial_number}", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8a2d\u5b9a Elgato Key \u7167\u660e\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7d50 Elgato Key \u7167\u660e\u3002" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u5c07 Elgato Key \u7167\u660e\u5e8f\u865f `{serial_number}` \u65b0\u589e\u81f3 Home Assistant\uff1f", + "title": "\u767c\u73fe\u5230 Elgato Key \u7167\u660e\u8a2d\u5099" + } + }, + "title": "Elgato Key \u7167\u660e" + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/__init__.py b/homeassistant/components/elgato/__init__.py new file mode 100644 index 00000000000..993748033b5 --- /dev/null +++ b/homeassistant/components/elgato/__init__.py @@ -0,0 +1,55 @@ +"""Support for Elgato Key Lights.""" +import logging + +from elgato import Elgato, ElgatoConnectionError + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType + +from .const import DATA_ELGATO_CLIENT, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Elgato Key Light components.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Elgato Key Light from a config entry.""" + session = async_get_clientsession(hass) + elgato = Elgato(entry.data[CONF_HOST], port=entry.data[CONF_PORT], session=session,) + + # Ensure we can connect to it + try: + await elgato.info() + except ElgatoConnectionError as exception: + raise ConfigEntryNotReady from exception + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = {DATA_ELGATO_CLIENT: elgato} + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, LIGHT_DOMAIN) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload Elgato Key Light config entry.""" + # Unload entities for this entry/device. + await hass.config_entries.async_forward_entry_unload(entry, LIGHT_DOMAIN) + + # Cleanup + del hass.data[DOMAIN][entry.entry_id] + if not hass.data[DOMAIN]: + del hass.data[DOMAIN] + + return True diff --git a/homeassistant/components/elgato/config_flow.py b/homeassistant/components/elgato/config_flow.py new file mode 100644 index 00000000000..a8a81734999 --- /dev/null +++ b/homeassistant/components/elgato/config_flow.py @@ -0,0 +1,136 @@ +"""Config flow to configure the Elgato Key Light integration.""" +import logging +from typing import Any, Dict, Optional + +from elgato import Elgato, ElgatoError, Info +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers import ConfigType +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_SERIAL_NUMBER, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class ElgatoFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle a Elgato Key Light config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_setup_form() + + try: + info = await self._get_elgato_info( + user_input[CONF_HOST], user_input[CONF_PORT] + ) + except ElgatoError: + return self._show_setup_form({"base": "connection_error"}) + + # Check if already configured + await self.async_set_unique_id(info.serial_number) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=info.serial_number, + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input[CONF_PORT], + CONF_SERIAL_NUMBER: info.serial_number, + }, + ) + + async def async_step_zeroconf( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle zeroconf discovery.""" + if user_input is None: + return self.async_abort(reason="connection_error") + + # Hostname is format: my-ke.local. + host = user_input["hostname"].rstrip(".") + try: + info = await self._get_elgato_info(host, user_input[CONF_PORT]) + except ElgatoError: + return self.async_abort(reason="connection_error") + + # Check if already configured + await self.async_set_unique_id(info.serial_number) + self._abort_if_unique_id_configured() + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update( + { + CONF_HOST: host, + CONF_PORT: user_input[CONF_PORT], + CONF_SERIAL_NUMBER: info.serial_number, + "title_placeholders": {"serial_number": info.serial_number}, + } + ) + + # Prepare configuration flow + return self._show_confirm_dialog() + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + async def async_step_zeroconf_confirm( + self, user_input: ConfigType = None + ) -> Dict[str, Any]: + """Handle a flow initiated by zeroconf.""" + if user_input is None: + return self._show_confirm_dialog() + + try: + info = await self._get_elgato_info( + self.context.get(CONF_HOST), self.context.get(CONF_PORT) + ) + except ElgatoError: + return self.async_abort(reason="connection_error") + + # Check if already configured + await self.async_set_unique_id(info.serial_number) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self.context.get(CONF_SERIAL_NUMBER), + data={ + CONF_HOST: self.context.get(CONF_HOST), + CONF_PORT: self.context.get(CONF_PORT), + CONF_SERIAL_NUMBER: self.context.get(CONF_SERIAL_NUMBER), + }, + ) + + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=9123): int, + } + ), + errors=errors or {}, + ) + + def _show_confirm_dialog(self) -> Dict[str, Any]: + """Show the confirm dialog to the user.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + serial_number = self.context.get(CONF_SERIAL_NUMBER) + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"serial_number": serial_number}, + ) + + async def _get_elgato_info(self, host: str, port: int) -> Info: + """Get device information from an Elgato Key Light device.""" + session = async_get_clientsession(self.hass) + elgato = Elgato(host, port=port, session=session,) + return await elgato.info() diff --git a/homeassistant/components/elgato/const.py b/homeassistant/components/elgato/const.py new file mode 100644 index 00000000000..2b6caa37a8f --- /dev/null +++ b/homeassistant/components/elgato/const.py @@ -0,0 +1,17 @@ +"""Constants for the Elgato Key Light integration.""" + +# Integration domain +DOMAIN = "elgato" + +# Home Assistant data keys +DATA_ELGATO_CLIENT = "elgato_client" + +# Attributes +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MODEL = "model" +ATTR_ON = "on" +ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_TEMPERATURE = "temperature" + +CONF_SERIAL_NUMBER = "serial_number" diff --git a/homeassistant/components/elgato/light.py b/homeassistant/components/elgato/light.py new file mode 100644 index 00000000000..99bca1ba20e --- /dev/null +++ b/homeassistant/components/elgato/light.py @@ -0,0 +1,158 @@ +"""Support for LED lights.""" +from datetime import timedelta +import logging +from typing import Any, Callable, Dict, List, Optional + +from elgato import Elgato, ElgatoError, Info, State + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR_TEMP, + Light, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_ON, + ATTR_SOFTWARE_VERSION, + ATTR_TEMPERATURE, + DATA_ELGATO_CLIENT, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +PARALLEL_UPDATES = 1 +SCAN_INTERVAL = timedelta(seconds=10) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Elgato Key Light based on a config entry.""" + elgato: Elgato = hass.data[DOMAIN][entry.entry_id][DATA_ELGATO_CLIENT] + info = await elgato.info() + async_add_entities([ElgatoLight(entry.entry_id, elgato, info)], True) + + +class ElgatoLight(Light): + """Defines a Elgato Key Light.""" + + def __init__( + self, entry_id: str, elgato: Elgato, info: Info, + ): + """Initialize Elgato Key Light.""" + self._brightness: Optional[int] = None + self._info: Info = info + self._state: Optional[bool] = None + self._temperature: Optional[int] = None + self._available = True + self.elgato = elgato + + @property + def name(self) -> str: + """Return the name of the entity.""" + # Return the product name, if display name is not set + if not self._info.display_name: + return self._info.product_name + return self._info.display_name + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._available + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return self._info.serial_number + + @property + def brightness(self) -> Optional[int]: + """Return the brightness of this light between 1..255.""" + return self._brightness + + @property + def color_temp(self): + """Return the CT color value in mireds.""" + return self._temperature + + @property + def min_mireds(self): + """Return the coldest color_temp that this light supports.""" + return 143 + + @property + def max_mireds(self): + """Return the warmest color_temp that this light supports.""" + return 344 + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP + + @property + def is_on(self) -> bool: + """Return the state of the light.""" + return bool(self._state) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + await self.async_turn_on(on=False) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + data = {} + + data[ATTR_ON] = True + if ATTR_ON in kwargs: + data[ATTR_ON] = kwargs[ATTR_ON] + + if ATTR_COLOR_TEMP in kwargs: + data[ATTR_TEMPERATURE] = kwargs[ATTR_COLOR_TEMP] + + if ATTR_BRIGHTNESS in kwargs: + data[ATTR_BRIGHTNESS] = round((kwargs[ATTR_BRIGHTNESS] / 255) * 100) + + try: + await self.elgato.light(**data) + except ElgatoError: + _LOGGER.error("An error occurred while updating the Elgato Key Light") + self._available = False + + async def async_update(self) -> None: + """Update Elgato entity.""" + try: + state: State = await self.elgato.state() + except ElgatoError: + if self._available: + _LOGGER.error("An error occurred while updating the Elgato Key Light") + self._available = False + return + + self._available = True + self._brightness = round((state.brightness * 255) / 100) + self._state = state.on + self._temperature = state.temperature + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this Elgato Key Light.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._info.serial_number)}, + ATTR_NAME: self._info.product_name, + ATTR_MANUFACTURER: "Elgato", + ATTR_MODEL: self._info.product_name, + ATTR_SOFTWARE_VERSION: f"{self._info.firmware_version} ({self._info.firmware_build_number})", + } diff --git a/homeassistant/components/elgato/manifest.json b/homeassistant/components/elgato/manifest.json new file mode 100644 index 00000000000..039b125e988 --- /dev/null +++ b/homeassistant/components/elgato/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "elgato", + "name": "Elgato Key Light", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/elgato", + "requirements": ["elgato==0.2.0"], + "dependencies": [], + "zeroconf": ["_elg._tcp.local."], + "codeowners": ["@frenck"], + "quality_scale": "platinum" +} diff --git a/homeassistant/components/elgato/strings.json b/homeassistant/components/elgato/strings.json new file mode 100644 index 00000000000..03c46f02efc --- /dev/null +++ b/homeassistant/components/elgato/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Elgato Key Light", + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "title": "Link your Elgato Key Light", + "description": "Set up your Elgato Key Light to integrate with Home Assistant.", + "data": { + "host": "Host or IP address", + "port": "Port number" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the Elgato Key Light with serial number `{serial_number}` to Home Assistant?", + "title": "Discovered Elgato Key Light device" + } + }, + "error": { + "connection_error": "Failed to connect to Elgato Key Light device." + }, + "abort": { + "already_configured": "This Elgato Key Light device is already configured.", + "connection_error": "Failed to connect to Elgato Key Light device." + } + } +} diff --git a/homeassistant/components/eliqonline/manifest.json b/homeassistant/components/eliqonline/manifest.json index 0bc242509c3..1cbaa5fa156 100644 --- a/homeassistant/components/eliqonline/manifest.json +++ b/homeassistant/components/eliqonline/manifest.json @@ -2,9 +2,7 @@ "domain": "eliqonline", "name": "Eliqonline", "documentation": "https://www.home-assistant.io/integrations/eliqonline", - "requirements": [ - "eliqonline==1.2.2" - ], + "requirements": ["eliqonline==1.2.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 1e1a8eba9e0..de1cb62234c 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -3,8 +3,8 @@ from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, FORMAT_NUMBER, + AlarmControlPanel, ) from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -29,13 +29,13 @@ from homeassistant.helpers.dispatcher import ( ) from . import ( - create_elk_entities, DOMAIN, - ElkEntity, SERVICE_ALARM_ARM_HOME_INSTANT, SERVICE_ALARM_ARM_NIGHT_INSTANT, SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISPLAY_MESSAGE, + ElkEntity, + create_elk_entities, ) SIGNAL_ARM_ENTITY = "elkm1_arm" diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 75acab5860d..c75da1ef039 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -1,10 +1,8 @@ { "domain": "elkm1", - "name": "Elkm1", + "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": [ - "elkm1-lib==0.7.15" - ], + "requirements": ["elkm1-lib==0.7.15"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/elv/__init__.py b/homeassistant/components/elv/__init__.py index b6097737414..b776c7f5453 100644 --- a/homeassistant/components/elv/__init__.py +++ b/homeassistant/components/elv/__init__.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol -from homeassistant.helpers import discovery from homeassistant.const import CONF_DEVICE +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/elv/manifest.json b/homeassistant/components/elv/manifest.json index 8390fc597f0..d5fb1eb251b 100644 --- a/homeassistant/components/elv/manifest.json +++ b/homeassistant/components/elv/manifest.json @@ -1,8 +1,8 @@ { - "domain": "elv", - "name": "ELV PCA", - "documentation": "https://www.home-assistant.io/integrations/pca", - "dependencies": [], - "codeowners": ["@majuss"], - "requirements": ["pypca==0.0.7"] - } + "domain": "elv", + "name": "ELV PCA", + "documentation": "https://www.home-assistant.io/integrations/pca", + "dependencies": [], + "codeowners": ["@majuss"], + "requirements": ["pypca==0.0.7"] +} diff --git a/homeassistant/components/elv/switch.py b/homeassistant/components/elv/switch.py index 362424c7fac..a77d21cf173 100644 --- a/homeassistant/components/elv/switch.py +++ b/homeassistant/components/elv/switch.py @@ -4,7 +4,7 @@ import logging import pypca from serial import SerialException -from homeassistant.components.switch import SwitchDevice, ATTR_CURRENT_POWER_W +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice from homeassistant.const import EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/emby/manifest.json b/homeassistant/components/emby/manifest.json index 12dbb152206..ec50b663c01 100644 --- a/homeassistant/components/emby/manifest.json +++ b/homeassistant/components/emby/manifest.json @@ -2,11 +2,7 @@ "domain": "emby", "name": "Emby", "documentation": "https://www.home-assistant.io/integrations/emby", - "requirements": [ - "pyemby==1.6" - ], + "requirements": ["pyemby==1.6"], "dependencies": [], - "codeowners": [ - "@mezz64" - ] + "codeowners": ["@mezz64"] } diff --git a/homeassistant/components/emoncms/sensor.py b/homeassistant/components/emoncms/sensor.py index 5f9d31697b8..34063e4c253 100644 --- a/homeassistant/components/emoncms/sensor.py +++ b/homeassistant/components/emoncms/sensor.py @@ -2,23 +2,23 @@ from datetime import timedelta import logging -import voluptuous as vol import requests +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, - CONF_URL, - CONF_VALUE_TEMPLATE, - CONF_UNIT_OF_MEASUREMENT, CONF_ID, CONF_SCAN_INTERVAL, - STATE_UNKNOWN, + CONF_UNIT_OF_MEASUREMENT, + CONF_URL, + CONF_VALUE_TEMPLATE, POWER_WATT, + STATE_UNKNOWN, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers import template +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 3b30a29960b..fd38da1cac1 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,20 +1,20 @@ """Support for sending data to Emoncms.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_API_KEY, - CONF_WHITELIST, - CONF_URL, - STATE_UNKNOWN, - STATE_UNAVAILABLE, CONF_SCAN_INTERVAL, + CONF_URL, + CONF_WHITELIST, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time from homeassistant.util import dt as dt_util diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index 80d946f4868..34270b6e209 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -1,6 +1,6 @@ { "domain": "emoncms_history", - "name": "Emoncms history", + "name": "Emoncms History", "documentation": "https://www.home-assistant.io/integrations/emoncms_history", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 791085b46f3..0a358c6e894 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -5,20 +5,22 @@ from aiohttp import web import voluptuous as vol from homeassistant import util +from homeassistant.components.http import real_ip from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.deprecation import get_deprecated import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.deprecation import get_deprecated from homeassistant.util.json import load_json, save_json -from homeassistant.components.http import real_ip from .hue_api import ( - HueUsernameView, - HueAllLightsStateView, - HueOneLightStateView, - HueOneLightChangeView, - HueGroupView, HueAllGroupsStateView, + HueAllLightsStateView, + HueFullStateView, + HueGroupView, + HueOneLightChangeView, + HueOneLightStateView, + HueUnauthorizedUser, + HueUsernameView, ) from .upnp import DescriptionXmlView, UPNPResponderThread @@ -113,11 +115,13 @@ async def async_setup(hass, yaml_config): DescriptionXmlView(config).register(app, app.router) HueUsernameView().register(app, app.router) + HueUnauthorizedUser().register(app, app.router) HueAllLightsStateView(config).register(app, app.router) HueOneLightStateView(config).register(app, app.router) HueOneLightChangeView(config).register(app, app.router) HueAllGroupsStateView(config).register(app, app.router) HueGroupView(config).register(app, app.router) + HueFullStateView(config).register(app, app.router) upnp_listener = UPNPResponderThread( config.host_ip_addr, @@ -308,7 +312,7 @@ class Config: def _load_json(filename): - """Wrapper, because we actually want to handle invalid json.""" + """Load JSON, handling invalid syntax.""" try: return load_json(filename) except HomeAssistantError: diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index e7f15e7fc53..b054d69e7a4 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -1,6 +1,6 @@ """Support for a Hue API to control Home Assistant.""" -import logging import hashlib +import logging from homeassistant import core from homeassistant.components import ( @@ -34,8 +34,8 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, @@ -49,8 +49,8 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, HTTP_BAD_REQUEST, - HTTP_UNAUTHORIZED, HTTP_NOT_FOUND, + HTTP_UNAUTHORIZED, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_TURN_OFF, @@ -89,6 +89,24 @@ HUE_API_STATE_SAT_MAX = 254 HUE_API_STATE_CT_MIN = 153 # Color temp HUE_API_STATE_CT_MAX = 500 +HUE_API_USERNAME = "12345678901234567890" +UNAUTHORIZED_USER = [ + {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} +] + + +class HueUnauthorizedUser(HomeAssistantView): + """Handle requests to find the emulated hue bridge.""" + + url = "/api" + name = "emulated_hue:api:unauthorized_user" + extra_urls = ["/api/"] + requires_auth = False + + async def get(self, request): + """Handle a GET request.""" + return self.json(UNAUTHORIZED_USER) + class HueUsernameView(HomeAssistantView): """Handle requests to create a username for the emulated hue bridge.""" @@ -111,7 +129,7 @@ class HueUsernameView(HomeAssistantView): if "devicetype" not in data: return self.json_message("devicetype not specified", HTTP_BAD_REQUEST) - return self.json([{"success": {"username": "12345678901234567890"}}]) + return self.json([{"success": {"username": HUE_API_USERNAME}}]) class HueAllGroupsStateView(HomeAssistantView): @@ -181,13 +199,37 @@ class HueAllLightsStateView(HomeAssistantView): if not is_local(request[KEY_REAL_IP]): return self.json_message("Only local IPs allowed", HTTP_UNAUTHORIZED) - hass = request.app["hass"] - json_response = {} + return self.json(create_list_of_entities(self.config, request)) - for entity in hass.states.async_all(): - if self.config.is_entity_exposed(entity): - number = self.config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(self.config, entity) + +class HueFullStateView(HomeAssistantView): + """Return full state view of emulated hue.""" + + url = "/api/{username}" + name = "emulated_hue:username:state" + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def get(self, request, username): + """Process a request to get the list of available lights.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) + if username != HUE_API_USERNAME: + return self.json(UNAUTHORIZED_USER) + + json_response = { + "lights": create_list_of_entities(self.config, request), + "config": { + "mac": "00:00:00:00:00:00", + "swversion": "01003542", + "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, + "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + }, + } return self.json(json_response) @@ -604,7 +646,7 @@ def entity_to_json(config, entity): and (entity_features & SUPPORT_COLOR) and (entity_features & SUPPORT_COLOR_TEMP) ): - # Extended Color light (ZigBee Device ID: 0x0210) + # Extended Color light (Zigbee Device ID: 0x0210) # Same as Color light, but which supports additional setting of color temperature retval["type"] = "Extended color light" retval["modelid"] = "HASS231" @@ -622,7 +664,7 @@ def entity_to_json(config, entity): else: retval["state"][HUE_API_STATE_COLORMODE] = "ct" elif (entity_features & SUPPORT_BRIGHTNESS) and (entity_features & SUPPORT_COLOR): - # Color light (ZigBee Device ID: 0x0200) + # Color light (Zigbee Device ID: 0x0200) # Supports on/off, dimming and color control (hue/saturation, enhanced hue, color loop and XY) retval["type"] = "Color light" retval["modelid"] = "HASS213" @@ -638,7 +680,7 @@ def entity_to_json(config, entity): elif (entity_features & SUPPORT_BRIGHTNESS) and ( entity_features & SUPPORT_COLOR_TEMP ): - # Color temperature light (ZigBee Device ID: 0x0220) + # Color temperature light (Zigbee Device ID: 0x0220) # Supports groups, scenes, on/off, dimming, and setting of a color temperature retval["type"] = "Color temperature light" retval["modelid"] = "HASS312" @@ -655,13 +697,13 @@ def entity_to_json(config, entity): | SUPPORT_TARGET_TEMPERATURE ) ) or entity.domain == script.DOMAIN: - # Dimmable light (ZigBee Device ID: 0x0100) + # Dimmable light (Zigbee Device ID: 0x0100) # Supports groups, scenes, on/off and dimming retval["type"] = "Dimmable light" retval["modelid"] = "HASS123" retval["state"].update({HUE_API_STATE_BRI: state[STATE_BRIGHTNESS]}) else: - # On/off light (ZigBee Device ID: 0x0000) + # On/off light (Zigbee Device ID: 0x0000) # Supports groups, scenes and on/off control retval["type"] = "On/off light" retval["modelid"] = "HASS321" @@ -673,3 +715,16 @@ def create_hue_success_response(entity_id, attr, value): """Create a success response for an attribute set on a light.""" success_key = f"/lights/{entity_id}/state/{attr}" return {"success": {success_key: value}} + + +def create_list_of_entities(config, request): + """Create a list of all entites.""" + hass = request.app["hass"] + json_response = {} + + for entity in hass.states.async_all(): + if config.is_entity_exposed(entity): + number = config.entity_id_to_number(entity.entity_id) + json_response[number] = entity_to_json(config, entity) + + return json_response diff --git a/homeassistant/components/emulated_hue/manifest.json b/homeassistant/components/emulated_hue/manifest.json index ddd39443886..fff85572477 100644 --- a/homeassistant/components/emulated_hue/manifest.json +++ b/homeassistant/components/emulated_hue/manifest.json @@ -1,12 +1,9 @@ { "domain": "emulated_hue", - "name": "Emulated hue", + "name": "Emulated Hue", "documentation": "https://www.home-assistant.io/integrations/emulated_hue", - "requirements": [ - "aiohttp_cors==0.7.0" - ], + "requirements": ["aiohttp_cors==0.7.0"], "dependencies": [], - "codeowners": [ - "@NobleKangaroo" - ] + "codeowners": ["@NobleKangaroo"], + "quality_scale": "internal" } diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 412dfdd673e..da9b4e23fe2 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -1,8 +1,8 @@ """Support UPNP discovery method that mimics Hue hubs.""" -import threading -import socket import logging import select +import socket +import threading from aiohttp import web @@ -35,7 +35,7 @@ class DescriptionXmlView(HomeAssistantView): http://{0}:{1}/ urn:schemas-upnp-org:device:Basic:1 -HASS Bridge ({0}) +Home Assistant Bridge ({0}) Royal Philips Electronics http://www.philips.com Philips hue Personal Wireless Lighting diff --git a/homeassistant/components/emulated_roku/.translations/da.json b/homeassistant/components/emulated_roku/.translations/da.json index 0479dee437d..0da64fac623 100644 --- a/homeassistant/components/emulated_roku/.translations/da.json +++ b/homeassistant/components/emulated_roku/.translations/da.json @@ -6,14 +6,14 @@ "step": { "user": { "data": { - "advertise_ip": "Adviserings IP", - "advertise_port": "Adviserings port", - "host_ip": "V\u00e6rt IP", - "listen_port": "Lytte port", + "advertise_ip": "Adviserings-IP", + "advertise_port": "Adviseringsport", + "host_ip": "V\u00e6rts-IP", + "listen_port": "Lytte-port", "name": "Navn", "upnp_bind_multicast": "Bind multicast (sand/falsk)" }, - "title": "Angiv server konfiguration" + "title": "Angiv server-konfiguration" } }, "title": "EmulatedRoku" diff --git a/homeassistant/components/emulated_roku/manifest.json b/homeassistant/components/emulated_roku/manifest.json index 824e5bef7c8..05cf72019d8 100644 --- a/homeassistant/components/emulated_roku/manifest.json +++ b/homeassistant/components/emulated_roku/manifest.json @@ -1,11 +1,9 @@ { "domain": "emulated_roku", - "name": "Emulated roku", + "name": "Emulated Roku", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/emulated_roku", - "requirements": [ - "emulated_roku==0.1.8" - ], + "requirements": ["emulated_roku==0.1.8"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/enigma2/manifest.json b/homeassistant/components/enigma2/manifest.json index 870681fd4a5..a49f2aa0190 100644 --- a/homeassistant/components/enigma2/manifest.json +++ b/homeassistant/components/enigma2/manifest.json @@ -1,10 +1,8 @@ { "domain": "enigma2", - "name": "Enigma2", + "name": "Enigma2 (OpenWebif)", "documentation": "https://www.home-assistant.io/integrations/enigma2", - "requirements": [ - "openwebifpy==3.1.1" - ], + "requirements": ["openwebifpy==3.1.1"], "dependencies": [], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index 4dffbabd219..a1d2c4a9260 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -1,10 +1,8 @@ { "domain": "enocean", - "name": "Enocean", + "name": "EnOcean", "documentation": "https://www.home-assistant.io/integrations/enocean", - "requirements": [ - "enocean==0.50" - ], + "requirements": ["enocean==0.50"], "dependencies": [], "codeowners": ["@bdurrer"] } diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index cfab52b3665..59ca10da791 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -10,8 +10,11 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, POWER_WATT, + STATE_CLOSED, + STATE_OPEN, TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv @@ -25,34 +28,44 @@ CONF_RANGE_TO = "range_to" DEFAULT_NAME = "EnOcean sensor" -DEVICE_CLASS_POWER = "powersensor" +SENSOR_TYPE_HUMIDITY = "humidity" +SENSOR_TYPE_POWER = "powersensor" +SENSOR_TYPE_TEMPERATURE = "temperature" +SENSOR_TYPE_WINDOWHANDLE = "windowhandle" SENSOR_TYPES = { - DEVICE_CLASS_HUMIDITY: { + SENSOR_TYPE_HUMIDITY: { "name": "Humidity", "unit": "%", "icon": "mdi:water-percent", "class": DEVICE_CLASS_HUMIDITY, }, - DEVICE_CLASS_POWER: { + SENSOR_TYPE_POWER: { "name": "Power", "unit": POWER_WATT, "icon": "mdi:power-plug", "class": DEVICE_CLASS_POWER, }, - DEVICE_CLASS_TEMPERATURE: { + SENSOR_TYPE_TEMPERATURE: { "name": "Temperature", "unit": TEMP_CELSIUS, "icon": "mdi:thermometer", "class": DEVICE_CLASS_TEMPERATURE, }, + SENSOR_TYPE_WINDOWHANDLE: { + "name": "WindowHandle", + "unit": None, + "icon": "mdi:window", + "class": None, + }, } + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS, default=DEVICE_CLASS_POWER): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=SENSOR_TYPE_POWER): cv.string, vol.Optional(CONF_MAX_TEMP, default=40): vol.Coerce(int), vol.Optional(CONF_MIN_TEMP, default=0): vol.Coerce(int), vol.Optional(CONF_RANGE_FROM, default=255): cv.positive_int, @@ -65,9 +78,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up an EnOcean sensor device.""" dev_id = config.get(CONF_ID) dev_name = config.get(CONF_NAME) - dev_class = config.get(CONF_DEVICE_CLASS) + sensor_type = config.get(CONF_DEVICE_CLASS) - if dev_class == DEVICE_CLASS_TEMPERATURE: + if sensor_type == SENSOR_TYPE_TEMPERATURE: temp_min = config.get(CONF_MIN_TEMP) temp_max = config.get(CONF_MAX_TEMP) range_from = config.get(CONF_RANGE_FROM) @@ -80,12 +93,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] ) - elif dev_class == DEVICE_CLASS_HUMIDITY: + elif sensor_type == SENSOR_TYPE_HUMIDITY: add_entities([EnOceanHumiditySensor(dev_id, dev_name)]) - elif dev_class == DEVICE_CLASS_POWER: + elif sensor_type == SENSOR_TYPE_POWER: add_entities([EnOceanPowerSensor(dev_id, dev_name)]) + elif sensor_type == SENSOR_TYPE_WINDOWHANDLE: + add_entities([EnOceanWindowHandle(dev_id, dev_name)]) + class EnOceanSensor(enocean.EnOceanDevice): """Representation of an EnOcean sensor device such as a power meter.""" @@ -140,7 +156,7 @@ class EnOceanPowerSensor(EnOceanSensor): def __init__(self, dev_id, dev_name): """Initialize the EnOcean power sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_POWER) + super().__init__(dev_id, dev_name, SENSOR_TYPE_POWER) def value_changed(self, packet): """Update the internal state of the sensor.""" @@ -175,7 +191,7 @@ class EnOceanTemperatureSensor(EnOceanSensor): def __init__(self, dev_id, dev_name, scale_min, scale_max, range_from, range_to): """Initialize the EnOcean temperature sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_TEMPERATURE) + super().__init__(dev_id, dev_name, SENSOR_TYPE_TEMPERATURE) self._scale_min = scale_min self._scale_max = scale_max self.range_from = range_from @@ -205,7 +221,7 @@ class EnOceanHumiditySensor(EnOceanSensor): def __init__(self, dev_id, dev_name): """Initialize the EnOcean humidity sensor device.""" - super().__init__(dev_id, dev_name, DEVICE_CLASS_HUMIDITY) + super().__init__(dev_id, dev_name, SENSOR_TYPE_HUMIDITY) def value_changed(self, packet): """Update the internal state of the sensor.""" @@ -214,3 +230,29 @@ class EnOceanHumiditySensor(EnOceanSensor): humidity = packet.data[2] * 100 / 250 self._state = round(humidity, 1) self.schedule_update_ha_state() + + +class EnOceanWindowHandle(EnOceanSensor): + """Representation of an EnOcean window handle device. + + EEPs (EnOcean Equipment Profiles): + - F6-10-00 (Mechanical handle / Hoppe AG) + """ + + def __init__(self, dev_id, dev_name): + """Initialize the EnOcean window handle sensor device.""" + super().__init__(dev_id, dev_name, SENSOR_TYPE_WINDOWHANDLE) + + def value_changed(self, packet): + """Update the internal state of the sensor.""" + + action = (packet.data[1] & 0x70) >> 4 + + if action == 0x07: + self._state = STATE_CLOSED + if action in (0x04, 0x06): + self._state = STATE_OPEN + if action == 0x05: + self._state = "tilt" + + self.schedule_update_ha_state() diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 5b5f94f7c8c..68f584c053e 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -1,10 +1,8 @@ { "domain": "enphase_envoy", - "name": "Enphase envoy", + "name": "Enphase Envoy", "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", - "requirements": [ - "envoy_reader==0.8.6" - ], + "requirements": ["envoy_reader==0.11.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/enphase_envoy/sensor.py b/homeassistant/components/enphase_envoy/sensor.py index 3977326c06d..a2b50f20eb6 100644 --- a/homeassistant/components/enphase_envoy/sensor.py +++ b/homeassistant/components/enphase_envoy/sensor.py @@ -2,6 +2,7 @@ import logging from envoy_reader.envoy_reader import EnvoyReader +import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -9,6 +10,8 @@ from homeassistant.const import ( CONF_IP_ADDRESS, CONF_MONITORED_CONDITIONS, CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, ENERGY_WATT_HOUR, POWER_WATT, ) @@ -42,6 +45,8 @@ CONST_DEFAULT_HOST = "envoy" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_IP_ADDRESS, default=CONST_DEFAULT_HOST): cv.string, + vol.Optional(CONF_USERNAME, default="envoy"): cv.string, + vol.Optional(CONF_PASSWORD, default=""): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): vol.All( cv.ensure_list, [vol.In(list(SENSORS))] ), @@ -52,30 +57,42 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Enphase Envoy sensor.""" - ip_address = config[CONF_IP_ADDRESS] monitored_conditions = config[CONF_MONITORED_CONDITIONS] name = config[CONF_NAME] + username = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + + envoy_reader = EnvoyReader(ip_address, username, password) entities = [] # Iterate through the list of sensors for condition in monitored_conditions: if condition == "inverters": - inverters = await EnvoyReader(ip_address).inverters_production() + try: + inverters = await envoy_reader.inverters_production() + except requests.exceptions.HTTPError: + _LOGGER.warning( + "Authentication for Inverter data failed during setup: %s", + ip_address, + ) + continue + if isinstance(inverters, dict): for inverter in inverters: entities.append( Envoy( - ip_address, + envoy_reader, condition, f"{name}{SENSORS[condition][0]} {inverter}", SENSORS[condition][1], ) ) + else: entities.append( Envoy( - ip_address, + envoy_reader, condition, f"{name}{SENSORS[condition][0]}", SENSORS[condition][1], @@ -87,13 +104,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class Envoy(Entity): """Implementation of the Enphase Envoy sensors.""" - def __init__(self, ip_address, sensor_type, name, unit): + def __init__(self, envoy_reader, sensor_type, name, unit): """Initialize the sensor.""" - self._ip_address = ip_address + self._envoy_reader = envoy_reader + self._type = sensor_type self._name = name self._unit_of_measurement = unit - self._type = sensor_type self._state = None + self._last_reported = None @property def name(self): @@ -115,11 +133,18 @@ class Envoy(Entity): """Icon to use in the frontend, if any.""" return ICON + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._type == "inverters": + return {"last_reported": self._last_reported} + + return None + async def async_update(self): """Get the energy production data from the Enphase Envoy.""" - if self._type != "inverters": - _state = await getattr(EnvoyReader(self._ip_address), self._type)() + _state = await getattr(self._envoy_reader, self._type)() if isinstance(_state, int): self._state = _state else: @@ -127,9 +152,17 @@ class Envoy(Entity): self._state = None elif self._type == "inverters": - inverters = await (EnvoyReader(self._ip_address).inverters_production()) + try: + inverters = await (self._envoy_reader.inverters_production()) + except requests.exceptions.HTTPError: + _LOGGER.warning( + "Authentication for Inverter data failed during update: %s", + self._envoy_reader.host, + ) + if isinstance(inverters, dict): serial_number = self._name.split(" ")[2] - self._state = inverters[serial_number] + self._state = inverters[serial_number][0] + self._last_reported = inverters[serial_number][1] else: self._state = None diff --git a/homeassistant/components/entur_public_transport/manifest.json b/homeassistant/components/entur_public_transport/manifest.json index 6396ff8e678..0d5f3e24f83 100644 --- a/homeassistant/components/entur_public_transport/manifest.json +++ b/homeassistant/components/entur_public_transport/manifest.json @@ -1,12 +1,8 @@ { "domain": "entur_public_transport", - "name": "Entur public transport", + "name": "Entur", "documentation": "https://www.home-assistant.io/integrations/entur_public_transport", - "requirements": [ - "enturclient==0.2.1" - ], + "requirements": ["enturclient==0.2.1"], "dependencies": [], - "codeowners": [ - "@hfurubotten" - ] -} \ No newline at end of file + "codeowners": ["@hfurubotten"] +} diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index bfe0aa5d2cb..fa243f09bbb 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,11 +2,7 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": [ - "env_canada==0.0.31" - ], + "requirements": ["env_canada==0.0.31"], "dependencies": [], - "codeowners": [ - "@michaeldavie" - ] + "codeowners": ["@michaeldavie"] } diff --git a/homeassistant/components/envirophat/manifest.json b/homeassistant/components/envirophat/manifest.json index ddf69d0d417..4cf443f4de6 100644 --- a/homeassistant/components/envirophat/manifest.json +++ b/homeassistant/components/envirophat/manifest.json @@ -1,11 +1,8 @@ { "domain": "envirophat", - "name": "Envirophat", + "name": "Enviro pHAT", "documentation": "https://www.home-assistant.io/integrations/envirophat", - "requirements": [ - "envirophat==0.0.6", - "smbus-cffi==0.5.1" - ], + "requirements": ["envirophat==0.0.6", "smbus-cffi==0.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 2aaeefa48cf..ce1f154f911 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -1,12 +1,12 @@ """Support for Enviro pHAT sensors.""" +from datetime import timedelta import importlib import logging -from datetime import timedelta import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_DISPLAY_OPTIONS, CONF_NAME +from homeassistant.const import CONF_DISPLAY_OPTIONS, CONF_NAME, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 52303c18413..1bc9d38e998 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -2,9 +2,7 @@ "domain": "envisalink", "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", - "requirements": [ - "pyenvisalink==4.0" - ], + "requirements": ["pyenvisalink==4.0"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/ephember/climate.py b/homeassistant/components/ephember/climate.py index c189b2d62b8..d743f3e82ba 100644 --- a/homeassistant/components/ephember/climate.py +++ b/homeassistant/components/ephember/climate.py @@ -199,10 +199,10 @@ class EphEmberThermostat(ClimateDevice): @staticmethod def map_mode_hass_eph(operation_mode): - """Map from home assistant mode to eph mode.""" + """Map from Home Assistant mode to eph mode.""" return getattr(ZoneMode, HA_STATE_TO_EPH.get(operation_mode), None) @staticmethod def map_mode_eph_hass(operation_mode): - """Map from eph mode to home assistant mode.""" + """Map from eph mode to Home Assistant mode.""" return EPH_TO_HA_STATE.get(operation_mode.name, HVAC_MODE_HEAT_COOL) diff --git a/homeassistant/components/ephember/manifest.json b/homeassistant/components/ephember/manifest.json index e05d21c0c02..4df302ac2dd 100644 --- a/homeassistant/components/ephember/manifest.json +++ b/homeassistant/components/ephember/manifest.json @@ -1,12 +1,8 @@ { "domain": "ephember", - "name": "Ephember", + "name": "EPH Controls", "documentation": "https://www.home-assistant.io/integrations/ephember", - "requirements": [ - "pyephember==0.3.1" - ], + "requirements": ["pyephember==0.3.1"], "dependencies": [], - "codeowners": [ - "@ttroy50" - ] + "codeowners": ["@ttroy50"] } diff --git a/homeassistant/components/epson/manifest.json b/homeassistant/components/epson/manifest.json index 22055e347af..81d08d76dfb 100644 --- a/homeassistant/components/epson/manifest.json +++ b/homeassistant/components/epson/manifest.json @@ -2,9 +2,7 @@ "domain": "epson", "name": "Epson", "documentation": "https://www.home-assistant.io/integrations/epson", - "requirements": [ - "epson-projector==0.1.3" - ], + "requirements": ["epson-projector==0.1.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index f3428602fad..b39722c39f3 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -1,8 +1,7 @@ """Support for Epson projector.""" import logging -import voluptuous as vol - +import epson_projector as epson from epson_projector.const import ( BACK, BUSY, @@ -19,15 +18,15 @@ from epson_projector.const import ( POWER, SOURCE, SOURCE_LIST, - TURN_ON, TURN_OFF, - VOLUME, + TURN_ON, VOL_DOWN, VOL_UP, + VOLUME, ) -import epson_projector as epson +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PREVIOUS_TRACK, @@ -48,6 +47,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv + from .const import ( ATTR_CMODE, DATA_EPSON, diff --git a/homeassistant/components/epsonworkforce/manifest.json b/homeassistant/components/epsonworkforce/manifest.json index d73b331d5f0..37620b66e7c 100644 --- a/homeassistant/components/epsonworkforce/manifest.json +++ b/homeassistant/components/epsonworkforce/manifest.json @@ -6,4 +6,3 @@ "codeowners": ["@ThaStealth"], "requirements": ["epsonprinter==0.0.9"] } - diff --git a/homeassistant/components/eq3btsmart/manifest.json b/homeassistant/components/eq3btsmart/manifest.json index e168752d83d..a7d9ee11f6f 100644 --- a/homeassistant/components/eq3btsmart/manifest.json +++ b/homeassistant/components/eq3btsmart/manifest.json @@ -1,13 +1,8 @@ { "domain": "eq3btsmart", - "name": "Eq3btsmart", + "name": "EQ3 Bluetooth Smart Thermostats", "documentation": "https://www.home-assistant.io/integrations/eq3btsmart", - "requirements": [ - "construct==2.9.45", - "python-eq3bt==0.1.11" - ], + "requirements": ["construct==2.9.45", "python-eq3bt==0.1.11"], "dependencies": [], - "codeowners": [ - "@rytilahti" - ] + "codeowners": ["@rytilahti"] } diff --git a/homeassistant/components/esphome/.translations/da.json b/homeassistant/components/esphome/.translations/da.json index ba84ab40301..db4b4362a5e 100644 --- a/homeassistant/components/esphome/.translations/da.json +++ b/homeassistant/components/esphome/.translations/da.json @@ -18,8 +18,8 @@ "title": "Indtast adgangskode" }, "discovery_confirm": { - "description": "Vil du tilf\u00f8je ESPHome node `{name}` til Home Assistant?", - "title": "Fandt ESPHome node" + "description": "Vil du tilf\u00f8je ESPHome-knudepunkt `{name}` til Home Assistant?", + "title": "Fandt ESPHome-knudepunkt" }, "user": { "data": { diff --git a/homeassistant/components/esphome/.translations/ko.json b/homeassistant/components/esphome/.translations/ko.json index b6bcf3cd1b3..4d8068c801b 100644 --- a/homeassistant/components/esphome/.translations/ko.json +++ b/homeassistant/components/esphome/.translations/ko.json @@ -18,8 +18,8 @@ "title": "\ube44\ubc00\ubc88\ud638 \uc785\ub825" }, "discovery_confirm": { - "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\ubc1c\uacac \ub41c ESPHome node" + "description": "Home Assistant \uc5d0 ESPHome node `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c ESPHome node" }, "user": { "data": { diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 2ad24e6f75e..cabba95ea7e 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -130,7 +130,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool # ESPHome uses servicecall packet for both events and service calls # Ensure the user can only send events of form 'esphome.xyz' if domain != "esphome": - _LOGGER.error("Can only generate events under esphome " "domain!") + _LOGGER.error("Can only generate events under esphome domain!") return hass.bus.async_fire(service.service, service_data) else: diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 549a063528f..c3d87bf836d 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,12 +3,8 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": [ - "aioesphomeapi==2.6.1" - ], + "requirements": ["aioesphomeapi==2.6.1"], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], - "codeowners": [ - "@OttoWinter" - ] + "codeowners": ["@OttoWinter"] } diff --git a/homeassistant/components/etherscan/manifest.json b/homeassistant/components/etherscan/manifest.json index f0abf8c7de0..106ec6f1f96 100644 --- a/homeassistant/components/etherscan/manifest.json +++ b/homeassistant/components/etherscan/manifest.json @@ -2,9 +2,7 @@ "domain": "etherscan", "name": "Etherscan", "documentation": "https://www.home-assistant.io/integrations/etherscan", - "requirements": [ - "python-etherscan-api==0.0.3" - ], + "requirements": ["python-etherscan-api==0.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index 191d6ab5315..eca637ec371 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,7 +1,7 @@ """Support for Eufy devices.""" import logging -import lakeside +import lakeside import voluptuous as vol from homeassistant.const import ( diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index 21c26606bdd..570f690307f 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,5 +1,6 @@ """Support for Eufy lights.""" import logging + import lakeside from homeassistant.components.light import ( @@ -7,16 +8,14 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, Light, ) - import homeassistant.util.color as color_util - from homeassistant.util.color import ( - color_temperature_mired_to_kelvin as mired_to_kelvin, color_temperature_kelvin_to_mired as kelvin_to_mired, + color_temperature_mired_to_kelvin as mired_to_kelvin, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/eufy/manifest.json b/homeassistant/components/eufy/manifest.json index 92a91976500..dc9176db7b0 100644 --- a/homeassistant/components/eufy/manifest.json +++ b/homeassistant/components/eufy/manifest.json @@ -1,10 +1,8 @@ { "domain": "eufy", - "name": "Eufy", + "name": "eufy", "documentation": "https://www.home-assistant.io/integrations/eufy", - "requirements": [ - "lakeside==0.12" - ], + "requirements": ["lakeside==0.12"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index 2e13886dd2a..cbc09f4101c 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,5 +1,6 @@ """Support for Eufy switches.""" import logging + import lakeside from homeassistant.components.switch import SwitchDevice diff --git a/homeassistant/components/everlights/manifest.json b/homeassistant/components/everlights/manifest.json index 53e2dbf2cb4..7ee6378af01 100644 --- a/homeassistant/components/everlights/manifest.json +++ b/homeassistant/components/everlights/manifest.json @@ -1,10 +1,8 @@ { "domain": "everlights", - "name": "Everlights", + "name": "EverLights", "documentation": "https://www.home-assistant.io/integrations/everlights", - "requirements": [ - "pyeverlights==0.1.0" - ], + "requirements": ["pyeverlights==0.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 82a7001539d..3da11bc8087 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -81,8 +81,8 @@ async def async_setup_platform( broker.params[CONF_LOCATION_IDX], ) - # special case of RoundModulation/RoundWireless (is a single zone system) - if broker.config["zones"][0]["zoneType"] == "Thermostat": + # special case of RoundModulation/RoundWireless as a single zone system + if len(broker.tcs.zones) == 1 and list(broker.tcs.zones.keys())[0] == "Thermostat": zone = list(broker.tcs.zones.values())[0] _LOGGER.debug( "Found the Thermostat (%s), id=%s, name=%s", @@ -121,9 +121,7 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): async def _set_tcs_mode(self, op_mode: str) -> None: """Set a Controller to any of its native EVO_* operating modes.""" - await self._call_client_api( - self._evo_tcs._set_status(op_mode) # pylint: disable=protected-access - ) + await self._call_client_api(self._evo_tcs.set_status(op_mode)) @property def hvac_modes(self) -> List[str]: diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 0b112df42bb..16b27452c7c 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -1,10 +1,8 @@ { "domain": "evohome", - "name": "Evohome", + "name": "Honeywell Total Connect Comfort (Europe)", "documentation": "https://www.home-assistant.io/integrations/evohome", - "requirements": [ - "evohome-async==0.3.4b1" - ], + "requirements": ["evohome-async==0.3.5.post1"], "dependencies": [], "codeowners": ["@zxdavb"] } diff --git a/homeassistant/components/facebook/manifest.json b/homeassistant/components/facebook/manifest.json index 930047065c5..dfdda34d39f 100644 --- a/homeassistant/components/facebook/manifest.json +++ b/homeassistant/components/facebook/manifest.json @@ -1,6 +1,6 @@ { "domain": "facebook", - "name": "Facebook", + "name": "Facebook Messenger", "documentation": "https://www.home-assistant.io/integrations/facebook", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/facebook/notify.py b/homeassistant/components/facebook/notify.py index 452b81c0f16..b75f2628033 100644 --- a/homeassistant/components/facebook/notify.py +++ b/homeassistant/components/facebook/notify.py @@ -6,15 +6,14 @@ from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol -from homeassistant.const import CONTENT_TYPE_JSON -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONTENT_TYPE_JSON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/facebox/image_processing.py b/homeassistant/components/facebox/image_processing.py index ba53ac1ac7d..ee6e4d8a6fa 100644 --- a/homeassistant/components/facebox/image_processing.py +++ b/homeassistant/components/facebox/image_processing.py @@ -5,26 +5,27 @@ import logging import requests import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME -from homeassistant.core import split_entity_id -import homeassistant.helpers.config_validation as cv from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingFaceEntity, ATTR_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingFaceEntity, ) from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_NAME, CONF_IP_ADDRESS, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, HTTP_BAD_REQUEST, HTTP_OK, HTTP_UNAUTHORIZED, ) +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv from .const import DOMAIN, SERVICE_TEACH_FACE diff --git a/homeassistant/components/fail2ban/manifest.json b/homeassistant/components/fail2ban/manifest.json index 6ff0c7be0e4..01afbb12b6f 100644 --- a/homeassistant/components/fail2ban/manifest.json +++ b/homeassistant/components/fail2ban/manifest.json @@ -1,6 +1,6 @@ { "domain": "fail2ban", - "name": "Fail2ban", + "name": "Fail2Ban", "documentation": "https://www.home-assistant.io/integrations/fail2ban", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/fail2ban/sensor.py b/homeassistant/components/fail2ban/sensor.py index 2dc528b2cff..692b48d9db5 100644 --- a/homeassistant/components/fail2ban/sensor.py +++ b/homeassistant/components/fail2ban/sensor.py @@ -5,17 +5,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fail2ban/ """ -import os -import logging - from datetime import timedelta - +import logging +import os import re + import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_FILE_PATH +from homeassistant.const import CONF_FILE_PATH, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/familyhub/manifest.json b/homeassistant/components/familyhub/manifest.json index e8aa3ab51b3..f0181ba79ed 100644 --- a/homeassistant/components/familyhub/manifest.json +++ b/homeassistant/components/familyhub/manifest.json @@ -1,10 +1,8 @@ { "domain": "familyhub", - "name": "Familyhub", + "name": "Samsung Family Hub", "documentation": "https://www.home-assistant.io/integrations/familyhub", - "requirements": [ - "python-family-hub-local==0.0.2" - ], + "requirements": ["python-family-hub-local==0.0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fan/.translations/bg.json b/homeassistant/components/fan/.translations/bg.json index 62452e67179..f678c870968 100644 --- a/homeassistant/components/fan/.translations/bg.json +++ b/homeassistant/components/fan/.translations/bg.json @@ -4,7 +4,7 @@ "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0438 {entity_name}", "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" }, diff --git a/homeassistant/components/fan/.translations/ca.json b/homeassistant/components/fan/.translations/ca.json index 0530ccf5a85..e2f3ce2b0a4 100644 --- a/homeassistant/components/fan/.translations/ca.json +++ b/homeassistant/components/fan/.translations/ca.json @@ -4,7 +4,7 @@ "turn_off": "Apaga {entity_name}", "turn_on": "Enc\u00e9n {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e0 apagat", "is_on": "{entity_name} est\u00e0 enc\u00e8s" }, diff --git a/homeassistant/components/fan/.translations/da.json b/homeassistant/components/fan/.translations/da.json new file mode 100644 index 00000000000..0c9556bfedb --- /dev/null +++ b/homeassistant/components/fan/.translations/da.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er slukket", + "is_on": "{entity_name} er t\u00e6ndt" + }, + "trigger_type": { + "turned_off": "{entity_name} blev slukket", + "turned_on": "{entity_name} blev t\u00e6ndt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/de.json b/homeassistant/components/fan/.translations/de.json index 9ac3d999370..9c3559b7cfc 100644 --- a/homeassistant/components/fan/.translations/de.json +++ b/homeassistant/components/fan/.translations/de.json @@ -4,7 +4,7 @@ "turn_off": "Schalte {entity_name} aus.", "turn_on": "Schalte {entity_name} ein." }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} ist ausgeschaltet", "is_on": "{entity_name} ist eingeschaltet" }, diff --git a/homeassistant/components/fan/.translations/en.json b/homeassistant/components/fan/.translations/en.json index b085e7baa45..c27d983ca2e 100644 --- a/homeassistant/components/fan/.translations/en.json +++ b/homeassistant/components/fan/.translations/en.json @@ -4,7 +4,7 @@ "turn_off": "Turn off {entity_name}", "turn_on": "Turn on {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} is off", "is_on": "{entity_name} is on" }, diff --git a/homeassistant/components/fan/.translations/es.json b/homeassistant/components/fan/.translations/es.json index d92153a6302..4ceefe9c721 100644 --- a/homeassistant/components/fan/.translations/es.json +++ b/homeassistant/components/fan/.translations/es.json @@ -4,7 +4,7 @@ "turn_off": "Desactivar {entity_name}", "turn_on": "Activar {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e1 desactivado", "is_on": "{entity_name} est\u00e1 activado" }, diff --git a/homeassistant/components/fan/.translations/fr.json b/homeassistant/components/fan/.translations/fr.json index 5c5a65b6bcd..e6944dab781 100644 --- a/homeassistant/components/fan/.translations/fr.json +++ b/homeassistant/components/fan/.translations/fr.json @@ -4,7 +4,7 @@ "turn_off": "\u00c9teindre {entity_name}", "turn_on": "Allumer {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est d\u00e9sactiv\u00e9", "is_on": "{entity_name} est activ\u00e9" }, diff --git a/homeassistant/components/fan/.translations/hu.json b/homeassistant/components/fan/.translations/hu.json new file mode 100644 index 00000000000..b559f29c581 --- /dev/null +++ b/homeassistant/components/fan/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kikapcsol\u00e1sa", + "turn_on": "{entity_name} bekapcsol\u00e1sa" + }, + "condition_type": { + "is_off": "{entity_name} ki van kapcsolva", + "is_on": "{entity_name} be van kapcsolva" + }, + "trigger_type": { + "turned_off": "{entity_name} ki lett kapcsolva", + "turned_on": "{entity_name} be lett kapcsolva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/it.json b/homeassistant/components/fan/.translations/it.json index b62d80c793b..4fab847f1cb 100644 --- a/homeassistant/components/fan/.translations/it.json +++ b/homeassistant/components/fan/.translations/it.json @@ -4,7 +4,7 @@ "turn_off": "Spegnere {entity_name}", "turn_on": "Accendere {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u00e8 spento", "is_on": "{entity_name} \u00e8 acceso" }, diff --git a/homeassistant/components/fan/.translations/ko.json b/homeassistant/components/fan/.translations/ko.json new file mode 100644 index 00000000000..dec2a711e57 --- /dev/null +++ b/homeassistant/components/fan/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" + }, + "condition_type": { + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" + }, + "trigger_type": { + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/lb.json b/homeassistant/components/fan/.translations/lb.json index 316a77d471d..f5170949bad 100644 --- a/homeassistant/components/fan/.translations/lb.json +++ b/homeassistant/components/fan/.translations/lb.json @@ -4,7 +4,7 @@ "turn_off": "{entity_name} ausschalten", "turn_on": "{entity_name} uschalten" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} ass aus", "is_on": "{entity_name} ass un" }, diff --git a/homeassistant/components/fan/.translations/nl.json b/homeassistant/components/fan/.translations/nl.json index 706c2c92b19..4837b301ea7 100644 --- a/homeassistant/components/fan/.translations/nl.json +++ b/homeassistant/components/fan/.translations/nl.json @@ -4,7 +4,7 @@ "turn_off": "Schakel {entity_name} uit", "turn_on": "Schakel {entity_name} in" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} is uitgeschakeld", "is_on": "{entity_name} is ingeschakeld" }, diff --git a/homeassistant/components/fan/.translations/no.json b/homeassistant/components/fan/.translations/no.json index 73917ac45c4..aa6320f0a65 100644 --- a/homeassistant/components/fan/.translations/no.json +++ b/homeassistant/components/fan/.translations/no.json @@ -4,7 +4,7 @@ "turn_off": "Sl\u00e5 av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} er av", "is_on": "{entity_name} er p\u00e5" }, diff --git a/homeassistant/components/fan/.translations/pl.json b/homeassistant/components/fan/.translations/pl.json index 424794a5b64..709a63c2389 100644 --- a/homeassistant/components/fan/.translations/pl.json +++ b/homeassistant/components/fan/.translations/pl.json @@ -4,7 +4,7 @@ "turn_off": "wy\u0142\u0105cz {entity_name}", "turn_on": "w\u0142\u0105cz {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "wentylator (entity_name} jest wy\u0142\u0105czony", "is_on": "wentylator (entity_name} jest w\u0142\u0105czony" }, diff --git a/homeassistant/components/fan/.translations/pt-BR.json b/homeassistant/components/fan/.translations/pt-BR.json new file mode 100644 index 00000000000..6b95464bdbc --- /dev/null +++ b/homeassistant/components/fan/.translations/pt-BR.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fan/.translations/pt.json b/homeassistant/components/fan/.translations/pt.json index a76550cbedd..ab78bc776bd 100644 --- a/homeassistant/components/fan/.translations/pt.json +++ b/homeassistant/components/fan/.translations/pt.json @@ -4,7 +4,7 @@ "turn_off": "Desligar {entity_name}", "turn_on": "Ligar {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} est\u00e1 desligada", "is_on": "{entity_name} est\u00e1 ligada" }, diff --git a/homeassistant/components/fan/.translations/ru.json b/homeassistant/components/fan/.translations/ru.json index 4fd5ebe28c5..157c78975cb 100644 --- a/homeassistant/components/fan/.translations/ru.json +++ b/homeassistant/components/fan/.translations/ru.json @@ -4,7 +4,7 @@ "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}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" }, diff --git a/homeassistant/components/fan/.translations/sl.json b/homeassistant/components/fan/.translations/sl.json index a5de109f764..a2bca3352ab 100644 --- a/homeassistant/components/fan/.translations/sl.json +++ b/homeassistant/components/fan/.translations/sl.json @@ -4,7 +4,7 @@ "turn_off": "Izklopite {entity_name}", "turn_on": "Vklopite {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} je izklopljen", "is_on": "{entity_name} je vklopljen" }, diff --git a/homeassistant/components/fan/.translations/zh-Hant.json b/homeassistant/components/fan/.translations/zh-Hant.json index 4b34f6e0165..78c0d991125 100644 --- a/homeassistant/components/fan/.translations/zh-Hant.json +++ b/homeassistant/components/fan/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "turn_off": "\u95dc\u9589 {entity_name}", "turn_on": "\u958b\u555f {entity_name}" }, - "condtion_type": { + "condition_type": { "is_off": "{entity_name} \u95dc\u9589", "is_on": "{entity_name} \u958b\u555f" }, diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 51aecc3e7c2..44b33af0c6e 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -6,25 +6,21 @@ from typing import Optional import voluptuous as vol -from homeassistant.components import group -from homeassistant.const import SERVICE_TURN_ON, SERVICE_TOGGLE, SERVICE_TURN_OFF -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.const import SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) DOMAIN = "fan" SCAN_INTERVAL = timedelta(seconds=30) -GROUP_NAME_ALL_FANS = "all fans" -ENTITY_ID_ALL_FANS = group.ENTITY_ID_FORMAT.format(GROUP_NAME_ALL_FANS) - ENTITY_ID_FORMAT = DOMAIN + ".{}" # Bitfield of features supported by the fan entity @@ -51,16 +47,14 @@ ATTR_DIRECTION = "direction" PROP_TO_ATTR = { "speed": ATTR_SPEED, - "speed_list": ATTR_SPEED_LIST, "oscillating": ATTR_OSCILLATING, "current_direction": ATTR_DIRECTION, } @bind_hass -def is_on(hass, entity_id: Optional[str] = None) -> bool: +def is_on(hass, entity_id: str) -> bool: """Return if the fans are on based on the statemachine.""" - entity_id = entity_id or ENTITY_ID_ALL_FANS state = hass.states.get(entity_id) return state.attributes[ATTR_SPEED] not in [SPEED_OFF, None] @@ -68,7 +62,7 @@ def is_on(hass, entity_id: Optional[str] = None) -> bool: async def async_setup(hass, config: dict): """Expose fan control via statemachine and services.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_FANS + _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -79,17 +73,22 @@ async def async_setup(hass, config: dict): component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off") component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") component.async_register_entity_service( - SERVICE_SET_SPEED, {vol.Required(ATTR_SPEED): cv.string}, "async_set_speed" + SERVICE_SET_SPEED, + {vol.Required(ATTR_SPEED): cv.string}, + "async_set_speed", + [SUPPORT_SET_SPEED], ) component.async_register_entity_service( SERVICE_OSCILLATE, {vol.Required(ATTR_OSCILLATING): cv.boolean}, "async_oscillate", + [SUPPORT_OSCILLATE], ) component.async_register_entity_service( SERVICE_SET_DIRECTION, {vol.Optional(ATTR_DIRECTION): cv.string}, "async_set_direction", + [SUPPORT_DIRECTION], ) return True @@ -178,6 +177,11 @@ class FanEntity(ToggleEntity): """Return the current direction of the fan.""" return None + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + return {ATTR_SPEED_LIST: self.speed_list} + @property def state_attributes(self) -> dict: """Return optional state attributes.""" diff --git a/homeassistant/components/fan/device_action.py b/homeassistant/components/fan/device_action.py index b26f632a775..a5d35d741b6 100644 --- a/homeassistant/components/fan/device_action.py +++ b/homeassistant/components/fan/device_action.py @@ -1,19 +1,21 @@ """Provides device automations for Fan.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - SERVICE_TURN_ON, + CONF_TYPE, SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN ACTION_TYPES = {"turn_on", "turn_off"} diff --git a/homeassistant/components/fan/device_condition.py b/homeassistant/components/fan/device_condition.py index 8b567fcd4c9..c69f28c10e9 100644 --- a/homeassistant/components/fan/device_condition.py +++ b/homeassistant/components/fan/device_condition.py @@ -1,21 +1,23 @@ """Provide the device automations for Fan.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_on", "is_off"} diff --git a/homeassistant/components/fan/device_trigger.py b/homeassistant/components/fan/device_trigger.py index 3e917e0ae79..3bfeb5ee36b 100644 --- a/homeassistant/components/fan/device_trigger.py +++ b/homeassistant/components/fan/device_trigger.py @@ -1,21 +1,23 @@ """Provides device automations for Fan.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, - STATE_ON, + CONF_PLATFORM, + CONF_TYPE, STATE_OFF, + STATE_ON, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN TRIGGER_TYPES = {"turned_on", "turned_off"} diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index 0df3b7a850e..02ed368feac 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -3,8 +3,7 @@ "name": "Fan", "documentation": "https://www.home-assistant.io/integrations/fan", "requirements": [], - "dependencies": [ - "group" - ], - "codeowners": [] + "dependencies": ["group"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py index 1053861e2bf..2692ac7ee5c 100644 --- a/homeassistant/components/fan/reproduce_state.py +++ b/homeassistant/components/fan/reproduce_state.py @@ -6,19 +6,19 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from . import ( - DOMAIN, ATTR_DIRECTION, ATTR_OSCILLATING, ATTR_SPEED, + DOMAIN, SERVICE_OSCILLATE, SERVICE_SET_DIRECTION, SERVICE_SET_SPEED, diff --git a/homeassistant/components/fan/strings.json b/homeassistant/components/fan/strings.json index 134119f41ff..98c3012c123 100644 --- a/homeassistant/components/fan/strings.json +++ b/homeassistant/components/fan/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_on": "{entity_name} is on", "is_off": "{entity_name} is off" }, diff --git a/homeassistant/components/fastdotcom/manifest.json b/homeassistant/components/fastdotcom/manifest.json index 2e47248d778..d6fe4a07c59 100644 --- a/homeassistant/components/fastdotcom/manifest.json +++ b/homeassistant/components/fastdotcom/manifest.json @@ -1,12 +1,8 @@ { "domain": "fastdotcom", - "name": "Fastdotcom", + "name": "Fast.com", "documentation": "https://www.home-assistant.io/integrations/fastdotcom", - "requirements": [ - "fastdotcom==0.0.3" - ], + "requirements": ["fastdotcom==0.0.3"], "dependencies": [], - "codeowners": [ - "@rohankapoorcom" - ] + "codeowners": ["@rohankapoorcom"] } diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 27b164e4edf..2643607c3a8 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -2,15 +2,15 @@ from datetime import datetime, timedelta from logging import getLogger from os.path import exists -from threading import Lock import pickle +from threading import Lock -import voluptuous as vol import feedparser +import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_interval _LOGGER = getLogger(__name__) @@ -131,7 +131,7 @@ class FeedManager: """Filter the entries provided and return the ones to keep.""" if len(self._feed.entries) > self._max_entries: _LOGGER.debug( - "Processing only the first %s entries " "in feed %s", + "Processing only the first %s entries in feed %s", self._max_entries, self._url, ) diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index 6ecc9efffb8..16c32bdd089 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -2,9 +2,7 @@ "domain": "feedreader", "name": "Feedreader", "documentation": "https://www.home-assistant.io/integrations/feedreader", - "requirements": [ - "feedparser-homeassistant==5.2.2.dev1" - ], + "requirements": ["feedparser-homeassistant==5.2.2.dev1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 673a34230fc..bc402b46fb2 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -2,20 +2,20 @@ import logging import re -import voluptuous as vol from haffmpeg.tools import FFVersion +import voluptuous as vol -from homeassistant.core import callback from homeassistant.const import ( ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, - async_dispatcher_connect, -) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity DOMAIN = "ffmpeg" diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 0f500176933..db3eb5621ff 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -2,11 +2,11 @@ import asyncio import logging -import voluptuous as vol from haffmpeg.camera import CameraMjpeg -from haffmpeg.tools import ImageFrame, IMAGE_JPEG +from haffmpeg.tools import IMAGE_JPEG, ImageFrame +import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/ffmpeg/manifest.json b/homeassistant/components/ffmpeg/manifest.json index 438891eca92..bacfa498fe1 100644 --- a/homeassistant/components/ffmpeg/manifest.json +++ b/homeassistant/components/ffmpeg/manifest.json @@ -1,10 +1,8 @@ { "domain": "ffmpeg", - "name": "Ffmpeg", + "name": "FFmpeg", "documentation": "https://www.home-assistant.io/integrations/ffmpeg", - "requirements": [ - "ha-ffmpeg==2.0" - ], + "requirements": ["ha-ffmpeg==2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ffmpeg_motion/binary_sensor.py b/homeassistant/components/ffmpeg_motion/binary_sensor.py index 54f3981f48a..294fcc2518f 100644 --- a/homeassistant/components/ffmpeg_motion/binary_sensor.py +++ b/homeassistant/components/ffmpeg_motion/binary_sensor.py @@ -4,17 +4,17 @@ import logging import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.components.ffmpeg import ( - FFmpegBase, - DATA_FFMPEG, - CONF_INPUT, CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, + CONF_INPUT, + DATA_FFMPEG, + FFmpegBase, ) from homeassistant.const import CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ffmpeg_motion/manifest.json b/homeassistant/components/ffmpeg_motion/manifest.json index 5b445dd3094..c1ae41e0f2b 100644 --- a/homeassistant/components/ffmpeg_motion/manifest.json +++ b/homeassistant/components/ffmpeg_motion/manifest.json @@ -1,6 +1,6 @@ { "domain": "ffmpeg_motion", - "name": "Ffmpeg motion", + "name": "FFmpeg Motion", "documentation": "https://www.home-assistant.io/integrations/ffmpeg_motion", "requirements": [], "dependencies": ["ffmpeg"], diff --git a/homeassistant/components/ffmpeg_noise/binary_sensor.py b/homeassistant/components/ffmpeg_noise/binary_sensor.py index 7c5f8656410..6ada2bb2748 100644 --- a/homeassistant/components/ffmpeg_noise/binary_sensor.py +++ b/homeassistant/components/ffmpeg_noise/binary_sensor.py @@ -4,17 +4,17 @@ import logging import haffmpeg.sensor as ffmpeg_sensor import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.binary_sensor import PLATFORM_SCHEMA -from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor from homeassistant.components.ffmpeg import ( - DATA_FFMPEG, - CONF_INPUT, - CONF_OUTPUT, CONF_EXTRA_ARGUMENTS, CONF_INITIAL_STATE, + CONF_INPUT, + CONF_OUTPUT, + DATA_FFMPEG, ) +from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ffmpeg_noise/manifest.json b/homeassistant/components/ffmpeg_noise/manifest.json index 1bb8e7353dc..ca7043c51a5 100644 --- a/homeassistant/components/ffmpeg_noise/manifest.json +++ b/homeassistant/components/ffmpeg_noise/manifest.json @@ -1,6 +1,6 @@ { "domain": "ffmpeg_noise", - "name": "Ffmpeg noise", + "name": "FFmpeg Noise", "documentation": "https://www.home-assistant.io/integrations/ffmpeg_noise", "requirements": [], "dependencies": ["ffmpeg"], diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index d44819e758b..32d8f328ef8 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -134,11 +134,11 @@ class FibaroController: info = self._client.info.get() self.hub_serial = slugify(info.serialNumber) except AssertionError: - _LOGGER.error("Can't connect to Fibaro HC. " "Please check URL.") + _LOGGER.error("Can't connect to Fibaro HC. Please check URL.") return False if login is None or login.status is False: _LOGGER.error( - "Invalid login for Fibaro HC. " "Please check username and password" + "Invalid login for Fibaro HC. Please check username and password" ) return False @@ -268,7 +268,7 @@ class FibaroController: else: room_name = self._room_map[device.roomID].name device.room_name = room_name - device.friendly_name = room_name + " " + device.name + device.friendly_name = f"{room_name} {device.name}" device.ha_id = "{}_{}_{}".format( slugify(room_name), slugify(device.name), device.id ) @@ -380,7 +380,7 @@ class FibaroDevice(Entity): def dont_know_message(self, action): """Make a warning in case we don't know how to perform an action.""" _LOGGER.warning( - "Not sure how to setValue: %s " "(available actions: %s)", + "Not sure how to setValue: %s (available actions: %s)", str(self.ha_id), str(self.fibaro_device.actions), ) diff --git a/homeassistant/components/fibaro/manifest.json b/homeassistant/components/fibaro/manifest.json index 0a5d1316561..b4288afee71 100644 --- a/homeassistant/components/fibaro/manifest.json +++ b/homeassistant/components/fibaro/manifest.json @@ -2,9 +2,7 @@ "domain": "fibaro", "name": "Fibaro", "documentation": "https://www.home-assistant.io/integrations/fibaro", - "requirements": [ - "fiblary3==0.1.7" - ], + "requirements": ["fiblary3==0.1.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fido/manifest.json b/homeassistant/components/fido/manifest.json index 638505d5dd9..690fc3ed777 100644 --- a/homeassistant/components/fido/manifest.json +++ b/homeassistant/components/fido/manifest.json @@ -2,9 +2,7 @@ "domain": "fido", "name": "Fido", "documentation": "https://www.home-assistant.io/integrations/fido", - "requirements": [ - "pyfido==2.1.1" - ], + "requirements": ["pyfido==2.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 8814a2406c5..086ae87a529 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -7,21 +7,23 @@ https://www.fido.ca/pages/#/my-account/wireless For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.fido/ """ -import logging from datetime import timedelta +import logging +from pyfido import FidoClient +from pyfido.client import PyFidoError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -147,7 +149,6 @@ class FidoData: def __init__(self, username, password, httpsession): """Initialize the data object.""" - from pyfido import FidoClient self.client = FidoClient(username, password, REQUESTS_TIMEOUT, httpsession) self.data = {} @@ -155,7 +156,6 @@ class FidoData: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest data from Fido.""" - from pyfido.client import PyFidoError try: await self.client.fetch_data() diff --git a/homeassistant/components/file/manifest.json b/homeassistant/components/file/manifest.json index 07a9cde900f..b0340eb271e 100644 --- a/homeassistant/components/file/manifest.json +++ b/homeassistant/components/file/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/file", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/file/notify.py b/homeassistant/components/file/notify.py index b190bf5d121..4cd83e64a83 100644 --- a/homeassistant/components/file/notify.py +++ b/homeassistant/components/file/notify.py @@ -4,16 +4,15 @@ import os import voluptuous as vol -from homeassistant.const import CONF_FILENAME -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_FILENAME +import homeassistant.helpers.config_validation as cv +import homeassistant.util.dt as dt_util CONF_TIMESTAMP = "timestamp" diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 60f04b18f24..96ae885ca77 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -1,12 +1,12 @@ """Support for sensor value(s) stored in local files.""" -import os import logging +import os import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_NAME, CONF_UNIT_OF_MEASUREMENT +from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/filesize/manifest.json b/homeassistant/components/filesize/manifest.json index 14e0a6a487c..4687e074547 100644 --- a/homeassistant/components/filesize/manifest.json +++ b/homeassistant/components/filesize/manifest.json @@ -1,6 +1,6 @@ { "domain": "filesize", - "name": "Filesize", + "name": "File Size", "documentation": "https://www.home-assistant.io/integrations/filesize", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/filesize/sensor.py b/homeassistant/components/filesize/sensor.py index af9375aad05..8c6cd30b118 100644 --- a/homeassistant/components/filesize/sensor.py +++ b/homeassistant/components/filesize/sensor.py @@ -5,9 +5,9 @@ import os import voluptuous as vol -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/filter/manifest.json b/homeassistant/components/filter/manifest.json index f28007ba552..d1933507f4d 100644 --- a/homeassistant/components/filter/manifest.json +++ b/homeassistant/components/filter/manifest.json @@ -3,8 +3,7 @@ "name": "Filter", "documentation": "https://www.home-assistant.io/integrations/filter", "requirements": [], - "dependencies": [], - "codeowners": [ - "@dgomes" - ] + "dependencies": ["history"], + "codeowners": ["@dgomes"], + "quality_scale": "internal" } diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 81c4623c53f..baa4f90af3f 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -1,31 +1,31 @@ """Allows the creation of a sensor that filters state property.""" -import logging -import statistics -from collections import deque, Counter -from numbers import Number -from functools import partial +from collections import Counter, deque from copy import copy from datetime import timedelta +from functools import partial +import logging +from numbers import Number +import statistics from typing import Optional import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components import history from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_ENTITY_ID, - ATTR_UNIT_OF_MEASUREMENT, ATTR_ENTITY_ID, ATTR_ICON, - STATE_UNKNOWN, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.util.decorator import Registry from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change -from homeassistant.components import history +from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -324,7 +324,8 @@ class FilterState: def set_precision(self, precision): """Set precision of Number based states.""" if isinstance(self.state, Number): - self.state = round(float(self.state), precision) + value = round(float(self.state), precision) + self.state = int(value) if precision == 0 else value def __str__(self): """Return state as the string representation of FilterState.""" diff --git a/homeassistant/components/fints/manifest.json b/homeassistant/components/fints/manifest.json index 1d0879c291a..8644124fde2 100644 --- a/homeassistant/components/fints/manifest.json +++ b/homeassistant/components/fints/manifest.json @@ -1,10 +1,8 @@ { "domain": "fints", - "name": "Fints", + "name": "FinTS", "documentation": "https://www.home-assistant.io/integrations/fints", - "requirements": [ - "fints==1.0.1" - ], + "requirements": ["fints==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index f550cb75c5d..88620785e14 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -2,14 +2,7 @@ "domain": "fitbit", "name": "Fitbit", "documentation": "https://www.home-assistant.io/integrations/fitbit", - "requirements": [ - "fitbit==0.3.1" - ], - "dependencies": [ - "configurator", - "http" - ], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["fitbit==0.3.1"], + "dependencies": ["configurator", "http"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 0d4b8d61e7a..5ddb63ef899 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -1,7 +1,7 @@ """Support for the Fitbit API.""" -import os -import logging import datetime +import logging +import os import time from fitbit import Fitbit @@ -9,17 +9,15 @@ from fitbit.api import FitbitOauth2Client from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ATTRIBUTION -from homeassistant.const import CONF_UNIT_SYSTEM +from homeassistant.const import ATTR_ATTRIBUTION, CONF_UNIT_SYSTEM +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json - _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fixer/manifest.json b/homeassistant/components/fixer/manifest.json index e6661ca6ce4..4bb0b7ba1b7 100644 --- a/homeassistant/components/fixer/manifest.json +++ b/homeassistant/components/fixer/manifest.json @@ -2,11 +2,7 @@ "domain": "fixer", "name": "Fixer", "documentation": "https://www.home-assistant.io/integrations/fixer", - "requirements": [ - "fixerio==1.0.0a0" - ], + "requirements": ["fixerio==1.0.0a0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/fleetgo/manifest.json b/homeassistant/components/fleetgo/manifest.json index eece29e167c..142d6ba00ed 100644 --- a/homeassistant/components/fleetgo/manifest.json +++ b/homeassistant/components/fleetgo/manifest.json @@ -2,9 +2,7 @@ "domain": "fleetgo", "name": "FleetGO", "documentation": "https://www.home-assistant.io/integrations/fleetgo", - "requirements": [ - "ritassist==0.9.2" - ], + "requirements": ["ritassist==0.9.2"], "dependencies": [], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/flexit/manifest.json b/homeassistant/components/flexit/manifest.json index 311904166d5..6c98925abab 100644 --- a/homeassistant/components/flexit/manifest.json +++ b/homeassistant/components/flexit/manifest.json @@ -2,11 +2,7 @@ "domain": "flexit", "name": "Flexit", "documentation": "https://www.home-assistant.io/integrations/flexit", - "requirements": [ - "pyflexit==0.3" - ], - "dependencies": [ - "modbus" - ], + "requirements": ["pyflexit==0.3"], + "dependencies": ["modbus"], "codeowners": [] } diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 416d39e5332..4f2f229977f 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -3,24 +3,24 @@ import logging import threading from pyflic import ( - FlicClient, ButtonConnectionChannel, ClickType, ConnectionStatus, + FlicClient, ScanWizard, ScanWizardResult, ) import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( + CONF_DISCOVERY, CONF_HOST, CONF_PORT, - CONF_DISCOVERY, CONF_TIMEOUT, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/flic/manifest.json b/homeassistant/components/flic/manifest.json index d0651c7fbe9..24170b34acf 100644 --- a/homeassistant/components/flic/manifest.json +++ b/homeassistant/components/flic/manifest.json @@ -2,9 +2,7 @@ "domain": "flic", "name": "Flic", "documentation": "https://www.home-assistant.io/integrations/flic", - "requirements": [ - "pyflic-homeassistant==0.4.dev0" - ], + "requirements": ["pyflic-homeassistant==0.4.dev0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/flock/manifest.json b/homeassistant/components/flock/manifest.json index ba6f4b1f43f..6bb3eaf9e69 100644 --- a/homeassistant/components/flock/manifest.json +++ b/homeassistant/components/flock/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/flock", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/flock/notify.py b/homeassistant/components/flock/notify.py index ba52d3b4beb..a71601ea2c4 100644 --- a/homeassistant/components/flock/notify.py +++ b/homeassistant/components/flock/notify.py @@ -5,12 +5,11 @@ import logging import async_timeout import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.flock.com/hooks/sendMessage/" diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index 6a9fb7a1fd8..d03c6330f20 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -1,11 +1,8 @@ { - "domain": "flume", - "name": "Flume", - "documentation": "https://www.home-assistant.io/integrations/flume/", - "requirements": [ - "pyflume==0.2.4" - ], - "dependencies": [], - "codeowners": ["@ChrisMandich"] - } - + "domain": "flume", + "name": "flume", + "documentation": "https://www.home-assistant.io/integrations/flume/", + "requirements": ["pyflume==0.2.4"], + "dependencies": [], + "codeowners": ["@ChrisMandich"] +} diff --git a/homeassistant/components/flunearyou/manifest.json b/homeassistant/components/flunearyou/manifest.json index a5dfaf4027f..e7394356c64 100644 --- a/homeassistant/components/flunearyou/manifest.json +++ b/homeassistant/components/flunearyou/manifest.json @@ -1,12 +1,8 @@ { "domain": "flunearyou", - "name": "Flunearyou", + "name": "Flu Near You", "documentation": "https://www.home-assistant.io/integrations/flunearyou", - "requirements": [ - "pyflunearyou==1.0.3" - ], + "requirements": ["pyflunearyou==1.0.3"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/flunearyou/sensor.py b/homeassistant/components/flunearyou/sensor.py index 86a97cce8c7..e06eb3a8ef4 100644 --- a/homeassistant/components/flunearyou/sensor.py +++ b/homeassistant/components/flunearyou/sensor.py @@ -142,7 +142,7 @@ class FluNearYouSensor(Entity): @property def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return f"{self.fny.latitude},{self.fny.longitude}_{self._kind}" @property diff --git a/homeassistant/components/flux/manifest.json b/homeassistant/components/flux/manifest.json index e7b0387698d..5195ed06bb3 100644 --- a/homeassistant/components/flux/manifest.json +++ b/homeassistant/components/flux/manifest.json @@ -5,5 +5,6 @@ "requirements": [], "dependencies": [], "after_dependencies": ["light"], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 404067d4107..f22b6335911 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -11,9 +11,7 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - is_on, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, @@ -22,29 +20,31 @@ from homeassistant.components.light import ( ATTR_XY_COLOR, DOMAIN as LIGHT_DOMAIN, VALID_TRANSITION, + is_on, ) from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_NAME, - CONF_PLATFORM, CONF_LIGHTS, CONF_MODE, + CONF_NAME, + CONF_PLATFORM, SERVICE_TURN_ON, STATE_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( - color_temperature_to_rgb, color_RGB_to_xy_brightness, color_temperature_kelvin_to_mired, + color_temperature_to_rgb, ) -from homeassistant.util.dt import utcnow as dt_utcnow, as_local +from homeassistant.util.dt import as_local, utcnow as dt_utcnow _LOGGER = logging.getLogger(__name__) @@ -323,7 +323,7 @@ class FluxSwitch(SwitchDevice, RestoreEntity): elif self._mode == MODE_RGB: await async_set_lights_rgb(self.hass, self._lights, rgb, self._transition) _LOGGER.debug( - "Lights updated to rgb:%s, %s%% " "of %s cycle complete at %s", + "Lights updated to rgb:%s, %s%% of %s cycle complete at %s", rgb, round(percentage_complete * 100), time_state, diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 7973956848a..16db60abbc0 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -1,28 +1,28 @@ """Support for Flux lights.""" import logging -import socket import random +import socket from flux_led import BulbScanner, WifiLedBulb import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_EFFECT, - ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, EFFECT_COLORLOOP, EFFECT_RANDOM, - SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, - SUPPORT_COLOR_TEMP, - Light, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_WHITE_VALUE, + Light, ) +from homeassistant.const import ATTR_MODE, CONF_DEVICES, CONF_NAME, CONF_PROTOCOL import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util diff --git a/homeassistant/components/flux_led/manifest.json b/homeassistant/components/flux_led/manifest.json index 4e5531ab4e3..20699139179 100644 --- a/homeassistant/components/flux_led/manifest.json +++ b/homeassistant/components/flux_led/manifest.json @@ -1,10 +1,8 @@ { "domain": "flux_led", - "name": "Flux led", + "name": "Flux LED/MagicLight", "documentation": "https://www.home-assistant.io/integrations/flux_led", - "requirements": [ - "flux_led==0.22" - ], + "requirements": ["flux_led==0.22"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/folder/sensor.py b/homeassistant/components/folder/sensor.py index e9e4ea680c4..a706ab2a0b5 100644 --- a/homeassistant/components/folder/sensor.py +++ b/homeassistant/components/folder/sensor.py @@ -6,9 +6,9 @@ import os import voluptuous as vol -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/folder_watcher/manifest.json b/homeassistant/components/folder_watcher/manifest.json index 17c54a39763..47edc4dccc0 100644 --- a/homeassistant/components/folder_watcher/manifest.json +++ b/homeassistant/components/folder_watcher/manifest.json @@ -1,10 +1,9 @@ { "domain": "folder_watcher", - "name": "Folder watcher", + "name": "Folder Watcher", "documentation": "https://www.home-assistant.io/integrations/folder_watcher", - "requirements": [ - "watchdog==0.8.3" - ], + "requirements": ["watchdog==0.8.3"], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index b02149d2bcd..c30985225f4 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -2,9 +2,7 @@ "domain": "foobot", "name": "Foobot", "documentation": "https://www.home-assistant.io/integrations/foobot", - "requirements": [ - "foobot_async==0.3.1" - ], + "requirements": ["foobot_async==0.3.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fortigate/manifest.json b/homeassistant/components/fortigate/manifest.json index ff063d6b7e2..1fdd28e256d 100644 --- a/homeassistant/components/fortigate/manifest.json +++ b/homeassistant/components/fortigate/manifest.json @@ -1,12 +1,8 @@ { - "domain": "fortigate", - "name": "Fortigate", - "documentation": "https://www.home-assistant.io/integrations/fortigate", - "dependencies": [], - "codeowners": [ - "@kifeo" - ], - "requirements": [ - "pyfgt==0.5.1" - ] + "domain": "fortigate", + "name": "FortiGate", + "documentation": "https://www.home-assistant.io/integrations/fortigate", + "dependencies": [], + "codeowners": ["@kifeo"], + "requirements": ["pyfgt==0.5.1"] } diff --git a/homeassistant/components/fortios/device_tracker.py b/homeassistant/components/fortios/device_tracker.py index 51bce5429f6..2b2d14f60e0 100644 --- a/homeassistant/components/fortios/device_tracker.py +++ b/homeassistant/components/fortios/device_tracker.py @@ -5,17 +5,16 @@ This component is part of the device_tracker platform. """ import logging -import voluptuous as vol from fortiosapi import FortiOSAPI +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_TOKEN -from homeassistant.const import CONF_VERIFY_SSL +from homeassistant.const import CONF_HOST, CONF_TOKEN, CONF_VERIFY_SSL +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_VERIFY_SSL = False diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index 4ec5a4fcb2a..dd0fdae9619 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -2,9 +2,7 @@ "domain": "fortios", "name": "Home Assistant Device Tracker to support FortiOS", "documentation": "https://www.home-assistant.io/integrations/fortios/", - "requirements": [ - "fortiosapi==0.10.8" - ], + "requirements": ["fortiosapi==0.10.8"], "dependencies": [], "codeowners": ["@kimfrellsen"] } diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 0e2ca4073bf..f4ec6556894 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,26 +1,26 @@ """This component provides basic support for Foscam IP cameras.""" -import logging import asyncio +import logging from libpyfoscam import FoscamCamera - import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_NAME, - CONF_USERNAME, CONF_PASSWORD, CONF_PORT, - ATTR_ENTITY_ID, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids -from .const import DOMAIN as FOSCAM_DOMAIN -from .const import DATA as FOSCAM_DATA -from .const import ENTITIES as FOSCAM_ENTITIES - +from .const import ( + DATA as FOSCAM_DATA, + DOMAIN as FOSCAM_DOMAIN, + ENTITIES as FOSCAM_ENTITIES, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index 6a47012ef84..63d44fc04e9 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -2,9 +2,7 @@ "domain": "foscam", "name": "Foscam", "documentation": "https://www.home-assistant.io/integrations/foscam", - "requirements": [ - "libpyfoscam==1.0" - ], + "requirements": ["libpyfoscam==1.0"], "dependencies": [], "codeowners": ["@skgsergio"] } diff --git a/homeassistant/components/foursquare/__init__.py b/homeassistant/components/foursquare/__init__.py index 7efb989e142..af15c4e5fa8 100644 --- a/homeassistant/components/foursquare/__init__.py +++ b/homeassistant/components/foursquare/__init__.py @@ -4,9 +4,9 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST from homeassistant.components.http import HomeAssistantView +from homeassistant.const import CONF_ACCESS_TOKEN, HTTP_BAD_REQUEST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -103,7 +103,7 @@ class FoursquarePushReceiver(HomeAssistantView): if self.push_secret != secret: _LOGGER.error( - "Received Foursquare push with invalid" "push secret: %s", secret + "Received Foursquare push with invalid push secret: %s", secret ) return self.json_message("Incorrect secret", HTTP_BAD_REQUEST) diff --git a/homeassistant/components/foursquare/manifest.json b/homeassistant/components/foursquare/manifest.json index c488bf790d5..450759a5922 100644 --- a/homeassistant/components/foursquare/manifest.json +++ b/homeassistant/components/foursquare/manifest.json @@ -3,10 +3,6 @@ "name": "Foursquare", "documentation": "https://www.home-assistant.io/integrations/foursquare", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@robbiet480" - ] + "dependencies": ["http"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index 19d1ce0aff7..2bba216242f 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -1,10 +1,8 @@ { "domain": "free_mobile", - "name": "Free mobile", + "name": "Free Mobile", "documentation": "https://www.home-assistant.io/integrations/free_mobile", - "requirements": [ - "freesms==0.1.2" - ], + "requirements": ["freesms==0.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 64c59c3ef2a..58426334dea 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -60,7 +60,7 @@ async def async_setup_freebox(hass, config, host, port): } token_file = hass.config.path(FREEBOX_CONFIG_FILE) - api_version = "v4" + api_version = "v6" fbx = Freepybox(app_desc=app_desc, token_file=token_file, api_version=api_version) @@ -71,6 +71,12 @@ async def async_setup_freebox(hass, config, host, port): else: hass.data[DATA_FREEBOX] = fbx + async def async_freebox_reboot(call): + """Handle reboot service call.""" + await fbx.system.reboot() + + hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot) + hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) hass.async_create_task( async_load_platform(hass, "device_tracker", DOMAIN, {}, config) diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index ac507f59c7f..5a29a619a33 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -2,11 +2,7 @@ "domain": "freebox", "name": "Freebox", "documentation": "https://www.home-assistant.io/integrations/freebox", - "requirements": [ - "aiofreepybox==0.0.8" - ], + "requirements": ["aiofreepybox==0.0.8"], "dependencies": [], - "codeowners": [ - "@snoof85" - ] + "codeowners": ["@snoof85"] } diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index e8f96586300..61ec670d217 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -18,6 +18,8 @@ class FbxSensor(Entity): """Representation of a freebox sensor.""" _name = "generic" + _unit = None + _icon = None def __init__(self, fbx): """Initialize the sensor.""" @@ -30,6 +32,16 @@ class FbxSensor(Entity): """Return the name of the sensor.""" return self._name + @property + def unit_of_measurement(self): + """Return the unit of the sensor.""" + return self._unit + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def state(self): """Return the state of the sensor.""" @@ -45,11 +57,7 @@ class FbxRXSensor(FbxSensor): _name = "Freebox download speed" _unit = "KB/s" - - @property - def unit_of_measurement(self): - """Define the unit.""" - return self._unit + _icon = "mdi:download-network" async def async_update(self): """Get the value from fetched datas.""" @@ -63,11 +71,7 @@ class FbxTXSensor(FbxSensor): _name = "Freebox upload speed" _unit = "KB/s" - - @property - def unit_of_measurement(self): - """Define the unit.""" - return self._unit + _icon = "mdi:upload-network" async def async_update(self): """Get the value from fetched datas.""" diff --git a/homeassistant/components/freebox/services.yaml b/homeassistant/components/freebox/services.yaml new file mode 100644 index 00000000000..be7afa60562 --- /dev/null +++ b/homeassistant/components/freebox/services.yaml @@ -0,0 +1,5 @@ +# Freebox service entries description. + +reboot: + # Description of the service + description: Reboots the Freebox. diff --git a/homeassistant/components/freedns/__init__.py b/homeassistant/components/freedns/__init__.py index 30f80280c1f..7aa34c8780e 100644 --- a/homeassistant/components/freedns/__init__.py +++ b/homeassistant/components/freedns/__init__.py @@ -1,14 +1,14 @@ """Integrate with FreeDNS Dynamic DNS service at freedns.afraid.org.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL, CONF_URL +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/freedns/manifest.json b/homeassistant/components/freedns/manifest.json index 02332c9fb10..ff4f9ec9202 100644 --- a/homeassistant/components/freedns/manifest.json +++ b/homeassistant/components/freedns/manifest.json @@ -1,6 +1,6 @@ { "domain": "freedns", - "name": "Freedns", + "name": "FreeDNS", "documentation": "https://www.home-assistant.io/integrations/freedns", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index ab4deec96f7..f27e409a28d 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -60,7 +60,7 @@ class FritzBoxScanner(DeviceScanner): self._update_info() else: _LOGGER.error( - "Failed to establish connection to FRITZ!Box " "with IP: %s", self.host + "Failed to establish connection to FRITZ!Box with IP: %s", self.host ) def scan_devices(self): @@ -79,6 +79,14 @@ class FritzBoxScanner(DeviceScanner): return None return ret + def get_extra_attributes(self, device): + """Return the attributes (ip, mac) of the given device or None if is not known.""" + ip_device = self.fritz_box.get_specific_host_entry(device).get("NewIPAddress") + + if not ip_device: + return {} + return {"ip": ip_device, "mac": device} + def _update_info(self): """Retrieve latest information from the FRITZ!Box.""" if not self.success_init: diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index 15a3406891f..80709db0437 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -1,10 +1,8 @@ { "domain": "fritz", - "name": "Fritz", + "name": "AVM Fritzbox", "documentation": "https://www.home-assistant.io/integrations/fritz", - "requirements": [ - "fritzconnection==0.8.4" - ], + "requirements": ["fritzconnection==0.8.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 40c16930206..115f7f8e644 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -7,11 +7,11 @@ from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, HVAC_MODE_HEAT, - PRESET_ECO, - PRESET_COMFORT, - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_OFF, + PRESET_COMFORT, + PRESET_ECO, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_BATTERY_LEVEL, diff --git a/homeassistant/components/fritzbox/manifest.json b/homeassistant/components/fritzbox/manifest.json index a1e28966568..494e70e8bcc 100644 --- a/homeassistant/components/fritzbox/manifest.json +++ b/homeassistant/components/fritzbox/manifest.json @@ -1,10 +1,8 @@ { "domain": "fritzbox", - "name": "Fritzbox", + "name": "AVM FRITZ!Box", "documentation": "https://www.home-assistant.io/integrations/fritzbox", - "requirements": [ - "pyfritzhome==0.4.0" - ], + "requirements": ["pyfritzhome==0.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index f85f16d6c0d..f05bcec846a 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -1,10 +1,8 @@ { "domain": "fritzbox_callmonitor", - "name": "Fritzbox callmonitor", + "name": "AVM FRITZ!Box Call Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", - "requirements": [ - "fritzconnection==0.8.4" - ], + "requirements": ["fritzconnection==0.8.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index 9afaa71e699..4dbb978842c 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -1,10 +1,8 @@ { "domain": "fritzbox_netmonitor", - "name": "Fritzbox netmonitor", + "name": "AVM FRITZ!Box Net Monitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", - "requirements": [ - "fritzconnection==0.8.4" - ], + "requirements": ["fritzconnection==0.8.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fritzdect/manifest.json b/homeassistant/components/fritzdect/manifest.json index e7b59b07138..9fc91293608 100644 --- a/homeassistant/components/fritzdect/manifest.json +++ b/homeassistant/components/fritzdect/manifest.json @@ -1,10 +1,8 @@ { "domain": "fritzdect", - "name": "Fritzdect", + "name": "AVM FRITZ!DECT", "documentation": "https://www.home-assistant.io/integrations/fritzdect", - "requirements": [ - "fritzhome==1.0.4" - ], + "requirements": ["fritzhome==1.0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py index cc629c54dc3..f67c84ae552 100644 --- a/homeassistant/components/fritzdect/switch.py +++ b/homeassistant/components/fritzdect/switch.py @@ -209,7 +209,7 @@ class FritzDectSwitchData: try: self.state = actor.get_state() self.current_consumption = (actor.get_power() or 0.0) / 1000 - self.total_consumption = (actor.get_energy() or 0.0) / 100000 + self.total_consumption = (actor.get_energy() or 0.0) / 1000 except (RequestException, HTTPError): _LOGGER.error("Request to actor failed") self.state = None diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ff0694afaab..27e2531c9f9 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -2,24 +2,23 @@ import copy from datetime import timedelta import logging -import voluptuous as vol from pyfronius import Fronius +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_RESOURCE, - CONF_SENSOR_TYPE, CONF_DEVICE, CONF_MONITORED_CONDITIONS, + CONF_RESOURCE, CONF_SCAN_INTERVAL, + CONF_SENSOR_TYPE, ) 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 - _LOGGER = logging.getLogger(__name__) CONF_SCOPE = "scope" diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 7ef2bd38644..efb1c34653b 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -242,6 +242,7 @@ def _frontend_root(dev_repo_path): if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" # Keep import here so that we can import frontend without installing reqs + # pylint: disable=import-outside-toplevel import hass_frontend return hass_frontend.where() diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 75d02baaeeb..1bc1900ee94 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/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191204.1" + "home-assistant-frontend==20200108.0" ], "dependencies": [ "api", @@ -16,5 +16,6 @@ ], "codeowners": [ "@home-assistant/frontend" - ] + ], + "quality_scale": "internal" } \ No newline at end of file diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 75b7b356ef9..2f68c5f8e01 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -1,10 +1,10 @@ """API for persistent storage for the frontend.""" from functools import wraps + import voluptuous as vol from homeassistant.components import websocket_api - # mypy: allow-untyped-calls, allow-untyped-defs DATA_STORAGE = "frontend_storage" diff --git a/homeassistant/components/frontier_silicon/manifest.json b/homeassistant/components/frontier_silicon/manifest.json index 17e9f973fd6..d8ca3148acc 100644 --- a/homeassistant/components/frontier_silicon/manifest.json +++ b/homeassistant/components/frontier_silicon/manifest.json @@ -1,10 +1,8 @@ { "domain": "frontier_silicon", - "name": "Frontier silicon", + "name": "Frontier Silicon", "documentation": "https://www.home-assistant.io/integrations/frontier_silicon", - "requirements": [ - "afsapi==0.0.4" - ], + "requirements": ["afsapi==0.0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index 7b9e79dbb3e..4da3bfd5bc3 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -55,12 +55,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def to_futurenow_level(level): - """Convert the given HASS light level (0-255) to FutureNow (0-100).""" + """Convert the given Home Assistant light level (0-255) to FutureNow (0-100).""" return int((level * 100) / 255) def to_hass_level(level): - """Convert the given FutureNow (0-100) light level to HASS (0-255).""" + """Convert the given FutureNow (0-100) light level to Home Assistant (0-255).""" return int((level * 255) / 100) diff --git a/homeassistant/components/futurenow/manifest.json b/homeassistant/components/futurenow/manifest.json index c1b0cd2c0ff..6a4599ea942 100644 --- a/homeassistant/components/futurenow/manifest.json +++ b/homeassistant/components/futurenow/manifest.json @@ -1,10 +1,8 @@ { "domain": "futurenow", - "name": "Futurenow", + "name": "P5 FutureNow", "documentation": "https://www.home-assistant.io/integrations/futurenow", - "requirements": [ - "pyfnip==0.2" - ], + "requirements": ["pyfnip==0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/garadget/cover.py b/homeassistant/components/garadget/cover.py index d487c39db6b..0eeb5f2b8f9 100644 --- a/homeassistant/components/garadget/cover.py +++ b/homeassistant/components/garadget/cover.py @@ -4,19 +4,19 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA -from homeassistant.helpers.event import track_utc_time_change +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice from homeassistant.const import ( - CONF_DEVICE, - CONF_USERNAME, - CONF_PASSWORD, CONF_ACCESS_TOKEN, + CONF_COVERS, + CONF_DEVICE, CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, STATE_CLOSED, STATE_OPEN, - CONF_COVERS, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_utc_time_change _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gc100/manifest.json b/homeassistant/components/gc100/manifest.json index 5ea7cb2fb41..e566643a4f8 100644 --- a/homeassistant/components/gc100/manifest.json +++ b/homeassistant/components/gc100/manifest.json @@ -1,10 +1,8 @@ { "domain": "gc100", - "name": "Gc100", + "name": "Global Caché GC-100", "documentation": "https://www.home-assistant.io/integrations/gc100", - "requirements": [ - "python-gc100==1.0.3a" - ], + "requirements": ["python-gc100==1.0.3a"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/gearbest/manifest.json b/homeassistant/components/gearbest/manifest.json index c8bb89c71a9..03f2b83cf3a 100644 --- a/homeassistant/components/gearbest/manifest.json +++ b/homeassistant/components/gearbest/manifest.json @@ -2,11 +2,7 @@ "domain": "gearbest", "name": "Gearbest", "documentation": "https://www.home-assistant.io/integrations/gearbest", - "requirements": [ - "gearbest_parser==1.0.7" - ], + "requirements": ["gearbest_parser==1.0.7"], "dependencies": [], - "codeowners": [ - "@HerrHofrat" - ] + "codeowners": ["@HerrHofrat"] } diff --git a/homeassistant/components/geizhals/manifest.json b/homeassistant/components/geizhals/manifest.json index 0f81ecbf1be..12ff4209820 100644 --- a/homeassistant/components/geizhals/manifest.json +++ b/homeassistant/components/geizhals/manifest.json @@ -2,9 +2,7 @@ "domain": "geizhals", "name": "Geizhals", "documentation": "https://www.home-assistant.io/integrations/geizhals", - "requirements": [ - "geizhals==0.0.9" - ], + "requirements": ["geizhals==0.0.9"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py index f04e943964c..9d5605cc404 100644 --- a/homeassistant/components/geizhals/sensor.py +++ b/homeassistant/components/geizhals/sensor.py @@ -1,15 +1,15 @@ """Parse prices of a device from geizhals.""" -import logging from datetime import timedelta +import logging from geizhals import Device, Geizhals import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle -from homeassistant.helpers.entity import Entity from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 01d2fb948ed..3d39d75ff4a 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -8,24 +8,24 @@ import requests from requests.auth import HTTPDigestAuth import voluptuous as vol -from homeassistant.const import ( - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, - CONF_AUTHENTICATION, - HTTP_BASIC_AUTHENTICATION, - HTTP_DIGEST_AUTHENTICATION, - CONF_VERIFY_SSL, -) -from homeassistant.exceptions import TemplateError from homeassistant.components.camera import ( - PLATFORM_SCHEMA, DEFAULT_CONTENT_TYPE, + PLATFORM_SCHEMA, SUPPORT_STREAM, Camera, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, +) +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 5cb4c21c577..58514934fc7 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -15,9 +15,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -214,7 +214,7 @@ class GenericThermostat(ClimateDevice, RestoreEntity): else: self._target_temp = self.min_temp _LOGGER.warning( - "Undefined target temperature," "falling back to %s", + "Undefined target temperature, falling back to %s", self._target_temp, ) else: diff --git a/homeassistant/components/generic_thermostat/manifest.json b/homeassistant/components/generic_thermostat/manifest.json index 283ea3c45fa..c601c1297d5 100644 --- a/homeassistant/components/generic_thermostat/manifest.json +++ b/homeassistant/components/generic_thermostat/manifest.json @@ -1,11 +1,8 @@ { "domain": "generic_thermostat", - "name": "Generic thermostat", + "name": "Generic Thermostat", "documentation": "https://www.home-assistant.io/integrations/generic_thermostat", "requirements": [], - "dependencies": [ - "sensor", - "switch" - ], + "dependencies": ["sensor", "switch"], "codeowners": [] } diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 6aa0d792c77..ab9349d1472 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -2,9 +2,7 @@ "domain": "geniushub", "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", - "requirements": [ - "geniushub-client==0.6.30" - ], + "requirements": ["geniushub-client==0.6.30"], "dependencies": [], "codeowners": ["@zxdavb"] } diff --git a/homeassistant/components/geniushub/switch.py b/homeassistant/components/geniushub/switch.py index 79d14417dd4..b73c9a89041 100644 --- a/homeassistant/components/geniushub/switch.py +++ b/homeassistant/components/geniushub/switch.py @@ -1,5 +1,5 @@ """Support for Genius Hub switch/outlet devices.""" -from homeassistant.components.switch import SwitchDevice, DEVICE_CLASS_OUTLET +from homeassistant.components.switch import DEVICE_CLASS_OUTLET, SwitchDevice from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import DOMAIN, GeniusZone diff --git a/homeassistant/components/geo_json_events/manifest.json b/homeassistant/components/geo_json_events/manifest.json index c4c892e88f4..bb1e8f942ad 100644 --- a/homeassistant/components/geo_json_events/manifest.json +++ b/homeassistant/components/geo_json_events/manifest.json @@ -1,10 +1,8 @@ { "domain": "geo_json_events", - "name": "Geo json events", + "name": "GeoJSON", "documentation": "https://www.home-assistant.io/integrations/geo_json_events", - "requirements": [ - "geojson_client==0.4" - ], + "requirements": ["geojson_client==0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index e5c587f93ed..6142fa22209 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -11,7 +11,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geo_location/manifest.json b/homeassistant/components/geo_location/manifest.json index 74dd7cbbf87..80067b2b5b6 100644 --- a/homeassistant/components/geo_location/manifest.json +++ b/homeassistant/components/geo_location/manifest.json @@ -1,6 +1,6 @@ { "domain": "geo_location", - "name": "Geo location", + "name": "Geolocation", "documentation": "https://www.home-assistant.io/integrations/geo_location", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index b8949286dea..33903246463 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -1,12 +1,8 @@ { "domain": "geo_rss_events", - "name": "Geo RSS events", + "name": "GeoRSS", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", - "requirements": [ - "georss_generic_client==0.3" - ], + "requirements": ["georss_generic_client==0.3"], "dependencies": [], - "codeowners": [ - "@exxamalte" - ] + "codeowners": ["@exxamalte"] } diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 39e6c5c7e82..b8891cdef0d 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -8,23 +8,23 @@ and grouped by category. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.geo_rss_events/ """ -import logging from datetime import timedelta +import logging -import voluptuous as vol from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA from georss_client.generic_feed import GenericFeed +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_UNIT_OF_MEASUREMENT, - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, + CONF_UNIT_OF_MEASUREMENT, CONF_URL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geofency/.translations/da.json b/homeassistant/components/geofency/.translations/da.json index 1390dfb504a..6e9443af5e8 100644 --- a/homeassistant/components/geofency/.translations/da.json +++ b/homeassistant/components/geofency/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Geofency meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Geofency-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 Geofency.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Geofency.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/geofency/.translations/ko.json b/homeassistant/components/geofency/.translations/ko.json index 42ff061a151..37f5ef0e76a 100644 --- a/homeassistant/components/geofency/.translations/ko.json +++ b/homeassistant/components/geofency/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Geofency Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Geofency Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Geofency Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/geofency/__init__.py b/homeassistant/components/geofency/__init__.py index 9d8e0b29f5d..9afc9a8bfac 100644 --- a/homeassistant/components/geofency/__init__.py +++ b/homeassistant/components/geofency/__init__.py @@ -18,8 +18,8 @@ from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import slugify -from .const import DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geofency/config_flow.py b/homeassistant/components/geofency/config_flow.py index 1a87502df2a..2d8bce86d74 100644 --- a/homeassistant/components/geofency/config_flow.py +++ b/homeassistant/components/geofency/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Geofency.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py index 09e9d46ce6d..49bd70192ef 100644 --- a/homeassistant/components/geofency/device_tracker.py +++ b/homeassistant/components/geofency/device_tracker.py @@ -1,13 +1,13 @@ """Support for the Geofency device tracker platform.""" import logging -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE -from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import callback +from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers import device_registry from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE diff --git a/homeassistant/components/geofency/manifest.json b/homeassistant/components/geofency/manifest.json index f7939e2b025..c48474a2927 100644 --- a/homeassistant/components/geofency/manifest.json +++ b/homeassistant/components/geofency/manifest.json @@ -4,8 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geofency", "requirements": [], - "dependencies": [ - "webhook" - ], + "dependencies": ["webhook"], "codeowners": [] } diff --git a/homeassistant/components/geonetnz_quakes/.translations/hu.json b/homeassistant/components/geonetnz_quakes/.translations/hu.json index 42de5a13142..4a163d24b75 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/hu.json +++ b/homeassistant/components/geonetnz_quakes/.translations/hu.json @@ -5,7 +5,7 @@ "data": { "radius": "Sug\u00e1r" }, - "title": "T\u00f6ltse ki a sz\u0171r\u0151 adatait." + "title": "T\u00f6ltsd ki a sz\u0171r\u0151 adatait." } } } diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py index 069c9ab7daa..141d0506847 100644 --- a/homeassistant/components/geonetnz_quakes/__init__.py +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -1,30 +1,29 @@ """The GeoNet NZ Quakes integration.""" import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from aio_geojson_geonetnz_quakes import GeonetnzQuakesFeedManager +import voluptuous as vol -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, + CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_MILES, ) -from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import METRIC_SYSTEM from .config_flow import configured_instances from .const import ( - PLATFORMS, CONF_MINIMUM_MAGNITUDE, CONF_MMI, DEFAULT_FILTER_TIME_INTERVAL, @@ -34,6 +33,7 @@ from .const import ( DEFAULT_SCAN_INTERVAL, DOMAIN, FEED, + PLATFORMS, SIGNAL_DELETE_ENTITY, SIGNAL_NEW_GEOLOCATION, SIGNAL_STATUS, diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py index bd93f08c72b..cc40f31f1fb 100644 --- a/homeassistant/components/geonetnz_quakes/config_flow.py +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -17,13 +17,13 @@ from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from .const import ( + CONF_MINIMUM_MAGNITUDE, CONF_MMI, + DEFAULT_MINIMUM_MAGNITUDE, DEFAULT_MMI, DEFAULT_RADIUS, DEFAULT_SCAN_INTERVAL, DOMAIN, - DEFAULT_MINIMUM_MAGNITUDE, - CONF_MINIMUM_MAGNITUDE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 1ee7c287c61..ae8b8fef48d 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -5,10 +5,10 @@ from typing import Optional from homeassistant.components.geo_location import GeolocationEvent from homeassistant.const import ( ATTR_ATTRIBUTION, + ATTR_TIME, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, LENGTH_MILES, - ATTR_TIME, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json index 9996e1d1cb3..775ca8760bc 100644 --- a/homeassistant/components/geonetnz_quakes/manifest.json +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -3,11 +3,7 @@ "name": "GeoNet NZ Quakes", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/geonetnz_quakes", - "requirements": [ - "aio_geojson_geonetnz_quakes==0.11" - ], + "requirements": ["aio_geojson_geonetnz_quakes==0.11"], "dependencies": [], - "codeowners": [ - "@exxamalte" - ] -} \ No newline at end of file + "codeowners": ["@exxamalte"] +} diff --git a/homeassistant/components/geonetnz_volcano/.translations/da.json b/homeassistant/components/geonetnz_volcano/.translations/da.json new file mode 100644 index 00000000000..a8c238a60b0 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokalitet allerede registreret" + }, + "step": { + "user": { + "data": { + "radius": "Radius" + }, + "title": "Udfyld dine filteroplysninger." + } + }, + "title": "GeoNet NZ vulkan" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/de.json b/homeassistant/components/geonetnz_volcano/.translations/de.json index 1a51f1fb490..fa87d24811c 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/de.json +++ b/homeassistant/components/geonetnz_volcano/.translations/de.json @@ -7,8 +7,10 @@ "user": { "data": { "radius": "Radius" - } + }, + "title": "F\u00fcllen Sie Ihre Filterangaben aus." } - } + }, + "title": "GeoNet NZ Volcano" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/ko.json b/homeassistant/components/geonetnz_volcano/.translations/ko.json new file mode 100644 index 00000000000..5d393fef4c4 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "radius": "\ubc18\uacbd" + }, + "title": "\ud544\ud130 \uc138\ubd80 \uc0ac\ud56d\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "GeoNet NZ Volcano" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/nl.json b/homeassistant/components/geonetnz_volcano/.translations/nl.json index 73c7c1eaab3..44d814b9db2 100644 --- a/homeassistant/components/geonetnz_volcano/.translations/nl.json +++ b/homeassistant/components/geonetnz_volcano/.translations/nl.json @@ -10,6 +10,7 @@ }, "title": "Vul uw filtergegevens in." } - } + }, + "title": "GeoNet NZ Volcano" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json b/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json new file mode 100644 index 00000000000..b1629599926 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/.translations/pt-BR.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "identifier_exists": "Local j\u00e1 registrado" + }, + "step": { + "user": { + "data": { + "radius": "Raio" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/__init__.py b/homeassistant/components/geonetnz_volcano/__init__.py index f0887da9c06..e24de7fdc5d 100644 --- a/homeassistant/components/geonetnz_volcano/__init__.py +++ b/homeassistant/components/geonetnz_volcano/__init__.py @@ -1,27 +1,27 @@ """The GeoNet NZ Volcano integration.""" import asyncio +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime from typing import Optional -import voluptuous as vol from aio_geojson_geonetnz_volcano import GeonetnzVolcanoFeedManager +import voluptuous as vol -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, + CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_MILES, ) -from homeassistant.helpers import config_validation as cv, aiohttp_client +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import METRIC_SYSTEM from .config_flow import configured_instances from .const import ( diff --git a/homeassistant/components/geonetnz_volcano/manifest.json b/homeassistant/components/geonetnz_volcano/manifest.json index a80ebdcff65..2fa10812d37 100644 --- a/homeassistant/components/geonetnz_volcano/manifest.json +++ b/homeassistant/components/geonetnz_volcano/manifest.json @@ -3,11 +3,7 @@ "name": "GeoNet NZ Volcano", "config_flow": true, "documentation": "https://www.home-assistant.io/components/geonetnz_volcano", - "requirements": [ - "aio_geojson_geonetnz_volcano==0.5" - ], + "requirements": ["aio_geojson_geonetnz_volcano==0.5"], "dependencies": [], - "codeowners": [ - "@exxamalte" - ] -} \ No newline at end of file + "codeowners": ["@exxamalte"] +} diff --git a/homeassistant/components/geonetnz_volcano/sensor.py b/homeassistant/components/geonetnz_volcano/sensor.py index 364ee416be4..f87ea88fc1c 100644 --- a/homeassistant/components/geonetnz_volcano/sensor.py +++ b/homeassistant/components/geonetnz_volcano/sensor.py @@ -3,11 +3,11 @@ import logging from typing import Optional from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_LATITUDE, + ATTR_LONGITUDE, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, - ATTR_ATTRIBUTION, - ATTR_LONGITUDE, - ATTR_LATITUDE, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/gios/.translations/ca.json b/homeassistant/components/gios/.translations/ca.json new file mode 100644 index 00000000000..80fedcafdd9 --- /dev/null +++ b/homeassistant/components/gios/.translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "No s'ha pogut connectar al servidor de GIO\u015a.", + "invalid_sensors_data": "Les dades dels sensors d'aquesta estaci\u00f3 de mesura s\u00f3n inv\u00e0lides.", + "wrong_station_id": "L'ID de l'estaci\u00f3 de mesura \u00e9s incorrecte." + }, + "step": { + "user": { + "data": { + "name": "Nom de la integraci\u00f3", + "station_id": "ID de l'estaci\u00f3 de mesura" + }, + "description": "Integraci\u00f3 de mesura de qualitat de l\u2019aire GIO\u015a (Polish Chief Inspectorate Of Environmental Protection). Si necessites ajuda amb la configuraci\u00f3, fes un cop d'ull a: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/da.json b/homeassistant/components/gios/.translations/da.json new file mode 100644 index 00000000000..bd0e947f1dc --- /dev/null +++ b/homeassistant/components/gios/.translations/da.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "GIO\u015a-integration for denne m\u00e5lestation er allerede konfigureret." + }, + "error": { + "cannot_connect": "Kan ikke oprette forbindelse til GIO\u015a-serveren.", + "invalid_sensors_data": "Ugyldige sensordata for denne m\u00e5lestation.", + "wrong_station_id": "M\u00e5lestationens ID er ikke korrekt." + }, + "step": { + "user": { + "data": { + "name": "Navn p\u00e5 integrationen", + "station_id": "ID for m\u00e5lestationen" + }, + "description": "Ops\u00e6t GIO\u015a (polsk inspektorat for milj\u00f8beskyttelse) luftkvalitet-integration. Hvis du har brug for hj\u00e6lp med konfigurationen, kig her: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/de.json b/homeassistant/components/gios/.translations/de.json new file mode 100644 index 00000000000..36813d71d76 --- /dev/null +++ b/homeassistant/components/gios/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Es kann keine Verbindung zum GIO\u015a-Server hergestellt werden.", + "invalid_sensors_data": "Ung\u00fcltige Sensordaten f\u00fcr diese Messstation.", + "wrong_station_id": "ID der Messstation ist nicht korrekt." + }, + "step": { + "user": { + "data": { + "name": "Name der Integration", + "station_id": "ID der Messstation" + } + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/en.json b/homeassistant/components/gios/.translations/en.json new file mode 100644 index 00000000000..0a85aaa9d15 --- /dev/null +++ b/homeassistant/components/gios/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "GIO\u015a integration for this measuring station is already configured." + }, + "error": { + "cannot_connect": "Cannot connect to the GIO\u015a server.", + "invalid_sensors_data": "Invalid sensors' data for this measuring station.", + "wrong_station_id": "ID of the measuring station is not correct." + }, + "step": { + "user": { + "data": { + "name": "Name of the integration", + "station_id": "ID of the measuring station" + }, + "description": "Set up GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/es.json b/homeassistant/components/gios/.translations/es.json new file mode 100644 index 00000000000..fb9eead7d2c --- /dev/null +++ b/homeassistant/components/gios/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3n de GIO\u015a para esta estaci\u00f3n de medici\u00f3n ya est\u00e1 configurada." + }, + "error": { + "cannot_connect": "No se puede conectar al servidor GIO\u015a.", + "invalid_sensors_data": "Datos de sensores no v\u00e1lidos para esta estaci\u00f3n de medici\u00f3n.", + "wrong_station_id": "El ID de la estaci\u00f3n de medici\u00f3n no es correcta." + }, + "step": { + "user": { + "data": { + "name": "Nombre de la integraci\u00f3n", + "station_id": "ID de la estaci\u00f3n de medici\u00f3n" + }, + "description": "Configurar la integraci\u00f3n de la calidad del aire GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n Ambiental de Polonia). Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n del Medio Ambiente de Polonia)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/fr.json b/homeassistant/components/gios/.translations/fr.json new file mode 100644 index 00000000000..2a9136bab4f --- /dev/null +++ b/homeassistant/components/gios/.translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossible de se connecter au serveur GIOS", + "invalid_sensors_data": "Donn\u00e9es des capteurs non valides pour cette station de mesure.", + "wrong_station_id": "L'identifiant de la station de mesure n'est pas correct." + }, + "step": { + "user": { + "data": { + "name": "Nom de l'int\u00e9gration", + "station_id": "Identifiant de la station de mesure" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/it.json b/homeassistant/components/gios/.translations/it.json new file mode 100644 index 00000000000..b3d1b9a71cf --- /dev/null +++ b/homeassistant/components/gios/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione GIO\u015a per questa stazione di misurazione \u00e8 gi\u00e0 configurata." + }, + "error": { + "cannot_connect": "Impossibile connettersi al server GIO\u015a.", + "invalid_sensors_data": "Dati dei sensori non validi per questa stazione di misura.", + "wrong_station_id": "L'ID della stazione di misura non \u00e8 corretto." + }, + "step": { + "user": { + "data": { + "name": "Nome dell'integrazione", + "station_id": "ID della stazione di misura" + }, + "description": "Impostare l'integrazione della qualit\u00e0 dell'aria GIO\u015a (Ispettorato capo polacco di protezione ambientale). Se hai bisogno di aiuto con la configurazione dai un'occhiata qui: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Ispettorato capo polacco di protezione ambientale)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ko.json b/homeassistant/components/gios/.translations/ko.json new file mode 100644 index 00000000000..2a92f935794 --- /dev/null +++ b/homeassistant/components/gios/.translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "GIO\u015a \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "invalid_sensors_data": "\uc774 \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc5d0 \ub300\ud55c \uc13c\uc11c \ub370\uc774\ud130\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "wrong_station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID \uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984", + "station_id": "\uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc758 ID" + }, + "description": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a) \ub300\uae30\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. \uad6c\uc131\uc5d0 \ub3c4\uc6c0\uc774 \ud544\uc694\ud55c \uacbd\uc6b0 https://www.home-assistant.io/integrations/gios \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694", + "title": "\ud3f4\ub780\ub4dc \ud658\uacbd\uccad (GIO\u015a)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/lb.json b/homeassistant/components/gios/.translations/lb.json new file mode 100644 index 00000000000..3b23ba5eee5 --- /dev/null +++ b/homeassistant/components/gios/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "GIO\u015a Integratioun fir d\u00ebs Miess Statioun ass scho konfigur\u00e9iert." + }, + "error": { + "cannot_connect": "Konnt sech net mam GIO\u015a Server verbannen.", + "invalid_sensors_data": "Ong\u00eblteg Sensor Donn\u00e9e\u00eb fir d\u00ebs Miess Statioun", + "wrong_station_id": "ID vun der Miess Statioun ass net korrekt." + }, + "step": { + "user": { + "data": { + "name": "Numm vun der Integratioun", + "station_id": "ID vun der Miess Statioun" + }, + "description": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz) Loft Qualit\u00e9it Integratioun ariichten. Fir w\u00e9ider H\u00ebllef mat der Konfiuratioun kuckt hei: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/no.json b/homeassistant/components/gios/.translations/no.json new file mode 100644 index 00000000000..b045c51e563 --- /dev/null +++ b/homeassistant/components/gios/.translations/no.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "GIO\u015a-integrasjon for denne m\u00e5lestasjonen er allerede konfigurert." + }, + "error": { + "cannot_connect": "Kan ikke koble til GIO\u015a-tjeneren", + "invalid_sensors_data": "Ugyldig sensordata for denne m\u00e5lestasjonen", + "wrong_station_id": "ID for m\u00e5lestasjon er ikke korrekt" + }, + "step": { + "user": { + "data": { + "name": "Navn p\u00e5 integrasjon", + "station_id": "ID til m\u00e5lestasjon" + }, + "description": "Sett opp GIO\u015a (Polish Chief Inspectorate Of Environmental Protection) luftkvalitet integrering. Hvis du trenger hjelp med konfigurasjonen ta en titt her: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/pl.json b/homeassistant/components/gios/.translations/pl.json new file mode 100644 index 00000000000..677762c2930 --- /dev/null +++ b/homeassistant/components/gios/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Integracja GIO\u015a dla tej stacji pomiarowej jest ju\u017c skonfigurowana." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z serwerem GIO\u015a.", + "invalid_sensors_data": "Nieprawid\u0142owe dane sensor\u00f3w dla tej stacji pomiarowej.", + "wrong_station_id": "Identyfikator stacji pomiarowej nie jest prawid\u0142owy." + }, + "step": { + "user": { + "data": { + "name": "Nazwa integracji", + "station_id": "Identyfikator stacji pomiarowej" + }, + "description": "Konfiguracja integracji jako\u015bci powietrza GIO\u015a (G\u0142\u00f3wny Inspektorat Ochrony \u015arodowiska). Je\u015bli potrzebujesz pomocy z konfiguracj\u0105, przejd\u017a na stron\u0119: https://www.home-assistant.io/integrations/gios", + "title": "G\u0142\u00f3wny Inspektorat Ochrony \u015arodowiska (GIO\u015a)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/pt-BR.json b/homeassistant/components/gios/.translations/pt-BR.json new file mode 100644 index 00000000000..83add749e47 --- /dev/null +++ b/homeassistant/components/gios/.translations/pt-BR.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "GIO\u015a (Inspetor-Chefe Polon\u00eas de Prote\u00e7\u00e3o Ambiental)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/ru.json b/homeassistant/components/gios/.translations/ru.json new file mode 100644 index 00000000000..69ffff98517 --- /dev/null +++ b/homeassistant/components/gios/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 GIO\u015a.", + "invalid_sensors_data": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438.", + "wrong_station_id": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438", + "station_id": "ID \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0442\u0430\u043d\u0446\u0438\u0438" + }, + "description": "\u0418\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u043e\u0442 \u041f\u043e\u043b\u044c\u0441\u043a\u043e\u0439 \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u0438 \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b (GIO\u015a). \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043f\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438: https://www.home-assistant.io/integrations/gios.", + "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u043a\u0430\u044f \u0438\u043d\u0441\u043f\u0435\u043a\u0446\u0438\u044f \u043f\u043e \u043e\u0445\u0440\u0430\u043d\u0435 \u043e\u043a\u0440\u0443\u0436\u0430\u044e\u0449\u0435\u0439 \u0441\u0440\u0435\u0434\u044b)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/sl.json b/homeassistant/components/gios/.translations/sl.json new file mode 100644 index 00000000000..da3995dd0b3 --- /dev/null +++ b/homeassistant/components/gios/.translations/sl.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "cannot_connect": "Ne morem se povezati s stre\u017enikom GIO\u015a.", + "invalid_sensors_data": "Neveljavni podatki senzorjev za to merilno postajo.", + "wrong_station_id": "ID merilne postaje ni pravilen." + }, + "step": { + "user": { + "data": { + "name": "Ime integracije", + "station_id": "ID merilne postaje" + }, + "description": "Nastavite GIO\u015a (poljski glavni in\u0161pektorat za varstvo okolja) integracijo kakovosti zraka. \u010ce potrebujete pomo\u010d pri konfiguraciji si oglejte tukaj: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (glavni poljski in\u0161pektorat za varstvo okolja)" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/zh-Hant.json b/homeassistant/components/gios/.translations/zh-Hant.json new file mode 100644 index 00000000000..3f10f2eb37b --- /dev/null +++ b/homeassistant/components/gios/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64 GIO\u015a \u76e3\u6e2c\u7ad9\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 GIO\u015a \u4f3a\u670d\u5668\u3002", + "invalid_sensors_data": "\u6b64\u76e3\u6e2c\u7ad9\u50b3\u611f\u5668\u8cc7\u6599\u7121\u6548\u3002", + "wrong_station_id": "\u76e3\u6e2c\u7ad9 ID \u4e0d\u6b63\u78ba\u3002" + }, + "step": { + "user": { + "data": { + "name": "\u6574\u5408\u540d\u7a31", + "station_id": "\u76e3\u6e2c\u7ad9 ID" + }, + "description": "\u8a2d\u5b9a GIO\u015a\uff08\u6ce2\u862d\u7e3d\u74b0\u5883\u4fdd\u8b77\u7763\u5bdf\u8655\uff09\u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u5047\u5982\u9700\u8981\u5354\u52a9\uff0c\u8acb\u53c3\u8003\uff1ahttps://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a\uff08\u6ce2\u862d\u7e3d\u74b0\u5883\u4fdd\u8b77\u7763\u5bdf\u8655\uff09" + } + }, + "title": "GIO\u015a" + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py new file mode 100644 index 00000000000..981de6395de --- /dev/null +++ b/homeassistant/components/gios/__init__.py @@ -0,0 +1,78 @@ +"""The GIOS component.""" +import asyncio +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from gios import ApiError, Gios, NoStationError + +from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle + +from .const import CONF_STATION_ID, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured GIOS.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + return True + + +async def async_setup_entry(hass, config_entry): + """Set up GIOS as config entry.""" + station_id = config_entry.data[CONF_STATION_ID] + _LOGGER.debug("Using station_id: %s", station_id) + + websession = async_get_clientsession(hass) + + gios = GiosData(websession, station_id) + + await gios.async_update() + + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = gios + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") + return True + + +class GiosData: + """Define an object to hold GIOS data.""" + + def __init__(self, session, station_id): + """Initialize.""" + self._gios = Gios(station_id, session) + self.station_id = station_id + self.sensors = {} + self.latitude = None + self.longitude = None + self.station_name = None + self.available = True + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update GIOS data.""" + try: + with timeout(30): + await self._gios.update() + except asyncio.TimeoutError: + _LOGGER.error("Asyncio Timeout Error") + except (ApiError, NoStationError, ClientConnectorError) as error: + _LOGGER.error("GIOS data update failed: %s", error) + self.available = self._gios.available + self.latitude = self._gios.latitude + self.longitude = self._gios.longitude + self.station_name = self._gios.station_name + self.sensors = self._gios.data diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py new file mode 100644 index 00000000000..f7285c8cc5a --- /dev/null +++ b/homeassistant/components/gios/air_quality.py @@ -0,0 +1,158 @@ +"""Support for the GIOS service.""" +from homeassistant.components.air_quality import ( + ATTR_CO, + ATTR_NO2, + ATTR_OZONE, + ATTR_PM_2_5, + ATTR_PM_10, + ATTR_SO2, + AirQualityEntity, +) +from homeassistant.const import CONF_NAME + +from .const import ATTR_STATION, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, ICONS_MAP + +ATTRIBUTION = "Data provided by GIOŚ" +SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a GIOS entities from a config_entry.""" + name = config_entry.data[CONF_NAME] + + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + + async_add_entities([GiosAirQuality(data, name)], True) + + +def round_state(func): + """Round state.""" + + def _decorator(self): + res = func(self) + if isinstance(res, float): + return round(res) + return res + + return _decorator + + +class GiosAirQuality(AirQualityEntity): + """Define an GIOS sensor.""" + + def __init__(self, gios, name): + """Initialize.""" + self.gios = gios + self._name = name + self._aqi = None + self._co = None + self._no2 = None + self._o3 = None + self._pm_2_5 = None + self._pm_10 = None + self._so2 = None + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def icon(self): + """Return the icon.""" + if self._aqi in ICONS_MAP: + return ICONS_MAP[self._aqi] + return "mdi:blur" + + @property + def air_quality_index(self): + """Return the air quality index.""" + return self._aqi + + @property + @round_state + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._pm_2_5 + + @property + @round_state + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._pm_10 + + @property + @round_state + def ozone(self): + """Return the O3 (ozone) level.""" + return self._o3 + + @property + @round_state + def carbon_monoxide(self): + """Return the CO (carbon monoxide) level.""" + return self._co + + @property + @round_state + def sulphur_dioxide(self): + """Return the SO2 (sulphur dioxide) level.""" + return self._so2 + + @property + @round_state + def nitrogen_dioxide(self): + """Return the NO2 (nitrogen dioxide) level.""" + return self._no2 + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self.gios.station_id + + @property + def available(self): + """Return True if entity is available.""" + return self.gios.available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + self._attrs[ATTR_STATION] = self.gios.station_name + return self._attrs + + async def async_update(self): + """Get the data from GIOS.""" + await self.gios.async_update() + + if self.gios.available: + # Different measuring stations have different sets of sensors. We don't know + # what data we will get. + if "AQI" in self.gios.sensors: + self._aqi = self.gios.sensors["AQI"]["value"] + if "CO" in self.gios.sensors: + self._co = self.gios.sensors["CO"]["value"] + self._attrs[f"{ATTR_CO}_index"] = self.gios.sensors["CO"]["index"] + if "NO2" in self.gios.sensors: + self._no2 = self.gios.sensors["NO2"]["value"] + self._attrs[f"{ATTR_NO2}_index"] = self.gios.sensors["NO2"]["index"] + if "O3" in self.gios.sensors: + self._o3 = self.gios.sensors["O3"]["value"] + self._attrs[f"{ATTR_OZONE}_index"] = self.gios.sensors["O3"]["index"] + if "PM2.5" in self.gios.sensors: + self._pm_2_5 = self.gios.sensors["PM2.5"]["value"] + self._attrs[f"{ATTR_PM_2_5}_index"] = self.gios.sensors["PM2.5"][ + "index" + ] + if "PM10" in self.gios.sensors: + self._pm_10 = self.gios.sensors["PM10"]["value"] + self._attrs[f"{ATTR_PM_10}_index"] = self.gios.sensors["PM10"]["index"] + if "SO2" in self.gios.sensors: + self._so2 = self.gios.sensors["SO2"]["value"] + self._attrs[f"{ATTR_SO2}_index"] = self.gios.sensors["SO2"]["index"] diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py new file mode 100644 index 00000000000..368d610c226 --- /dev/null +++ b/homeassistant/components/gios/config_flow.py @@ -0,0 +1,65 @@ +"""Adds config flow for GIOS.""" +import asyncio + +from aiohttp.client_exceptions import ClientConnectorError +from async_timeout import timeout +from gios import ApiError, Gios, NoStationError +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import CONF_NAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_STATION_ID, DEFAULT_NAME, DOMAIN # pylint:disable=unused-import + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_STATION_ID): int, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + } +) + + +class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for GIOS.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + try: + await self.async_set_unique_id( + user_input[CONF_STATION_ID], raise_on_progress=False + ) + self._abort_if_unique_id_configured() + + websession = async_get_clientsession(self.hass) + + with timeout(30): + gios = Gios(user_input[CONF_STATION_ID], websession) + await gios.update() + + if not gios.available: + raise InvalidSensorsData() + + return self.async_create_entry( + title=user_input[CONF_STATION_ID], data=user_input, + ) + except (ApiError, ClientConnectorError, asyncio.TimeoutError): + errors["base"] = "cannot_connect" + except NoStationError: + errors[CONF_STATION_ID] = "wrong_station_id" + except InvalidSensorsData: + errors[CONF_STATION_ID] = "invalid_sensors_data" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class InvalidSensorsData(exceptions.HomeAssistantError): + """Error to indicate invalid sensors data.""" diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py new file mode 100644 index 00000000000..3588b5e8dfc --- /dev/null +++ b/homeassistant/components/gios/const.py @@ -0,0 +1,25 @@ +"""Constants for GIOS integration.""" +from datetime import timedelta + +ATTR_NAME = "name" +ATTR_STATION = "station" +CONF_STATION_ID = "station_id" +DATA_CLIENT = "client" +DEFAULT_NAME = "GIOŚ" +# Term of service GIOŚ allow downloading data no more than twice an hour. +DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) +DOMAIN = "gios" + +AQI_GOOD = "dobry" +AQI_MODERATE = "umiarkowany" +AQI_POOR = "dostateczny" +AQI_VERY_GOOD = "bardzo dobry" +AQI_VERY_POOR = "zły" + +ICONS_MAP = { + AQI_VERY_GOOD: "mdi:emoticon-excited", + AQI_GOOD: "mdi:emoticon-happy", + AQI_MODERATE: "mdi:emoticon-neutral", + AQI_POOR: "mdi:emoticon-sad", + AQI_VERY_POOR: "mdi:emoticon-dead", +} diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json new file mode 100644 index 00000000000..b3d125d8ab6 --- /dev/null +++ b/homeassistant/components/gios/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "gios", + "name": "GIOŚ", + "documentation": "https://www.home-assistant.io/integrations/gios", + "dependencies": [], + "codeowners": ["@bieniu"], + "requirements": ["gios==0.0.3"], + "config_flow": true +} diff --git a/homeassistant/components/gios/strings.json b/homeassistant/components/gios/strings.json new file mode 100644 index 00000000000..2442fa61a91 --- /dev/null +++ b/homeassistant/components/gios/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "GIOŚ", + "step": { + "user": { + "title": "GIOŚ (Polish Chief Inspectorate Of Environmental Protection)", + "description": "Set up GIOŚ (Polish Chief Inspectorate Of Environmental Protection) air quality integration. If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/gios", + "data": { + "name": "Name of the integration", + "station_id": "ID of the measuring station" + } + } + }, + "error": { + "wrong_station_id": "ID of the measuring station is not correct.", + "invalid_sensors_data": "Invalid sensors' data for this measuring station.", + "cannot_connect": "Cannot connect to the GIOŚ server." + }, + "abort": { + "already_configured": "GIOŚ integration for this measuring station is already configured." + } + } +} diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 02593bf603d..c2686346e5b 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -1,10 +1,8 @@ { "domain": "github", - "name": "Github", + "name": "GitHub", "documentation": "https://www.home-assistant.io/integrations/github", - "requirements": [ - "PyGithub==1.43.8" - ], + "requirements": ["PyGithub==1.43.8"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index 5e8200b41ab..c77cf7930b8 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,6 +1,7 @@ """Support for GitHub.""" from datetime import timedelta import logging + import github import voluptuous as vol diff --git a/homeassistant/components/gitlab_ci/manifest.json b/homeassistant/components/gitlab_ci/manifest.json index e439e8d7eda..ba29f56cfba 100644 --- a/homeassistant/components/gitlab_ci/manifest.json +++ b/homeassistant/components/gitlab_ci/manifest.json @@ -1,10 +1,8 @@ { "domain": "gitlab_ci", - "name": "Gitlab ci", + "name": "GitLab-CI", "documentation": "https://www.home-assistant.io/integrations/gitlab_ci", - "requirements": [ - "python-gitlab==1.6.0" - ], + "requirements": ["python-gitlab==1.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/gitter/manifest.json b/homeassistant/components/gitter/manifest.json index 96df8c4e083..35904b3a57b 100644 --- a/homeassistant/components/gitter/manifest.json +++ b/homeassistant/components/gitter/manifest.json @@ -2,11 +2,7 @@ "domain": "gitter", "name": "Gitter", "documentation": "https://www.home-assistant.io/integrations/gitter", - "requirements": [ - "gitterpy==0.1.7" - ], + "requirements": ["gitterpy==0.1.7"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/glances/.translations/de.json b/homeassistant/components/glances/.translations/de.json index 04fed0fdc49..8330745f4b4 100644 --- a/homeassistant/components/glances/.translations/de.json +++ b/homeassistant/components/glances/.translations/de.json @@ -14,17 +14,23 @@ "name": "Name", "password": "Passwort", "port": "Port", - "username": "Benutzername" - } + "ssl": "Verwenden Sie SSL / TLS, um eine Verbindung zum Glances-System herzustellen", + "username": "Benutzername", + "verify_ssl": "\u00dcberpr\u00fcfen Sie die Zertifizierung des Systems", + "version": "Glances API-Version (2 oder 3)" + }, + "title": "Glances einrichten" } - } + }, + "title": "Glances" }, "options": { "step": { "init": { "data": { "scan_interval": "Aktualisierungsfrequenz" - } + }, + "description": "Konfigurieren Sie die Optionen f\u00fcr Glances" } } } diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 6067b1a9868..761f77510b6 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -3,12 +3,7 @@ "name": "Glances", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", - "requirements": [ - "glances_api==0.2.0" - ], + "requirements": ["glances_api==0.2.0"], "dependencies": [], - "codeowners": [ - "@fabaff", - "@engrbm87" - ] -} \ No newline at end of file + "codeowners": ["@fabaff", "@engrbm87"] +} diff --git a/homeassistant/components/gntp/manifest.json b/homeassistant/components/gntp/manifest.json index f1c030125ac..3433b369456 100644 --- a/homeassistant/components/gntp/manifest.json +++ b/homeassistant/components/gntp/manifest.json @@ -1,12 +1,8 @@ { "domain": "gntp", - "name": "Gntp", + "name": "Growl (GnGNTP)", "documentation": "https://www.home-assistant.io/integrations/gntp", - "requirements": [ - "gntp==1.0.3" - ], + "requirements": ["gntp==1.0.3"], "dependencies": [], - "codeowners": [ - "@robbiet480" - ] + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/goalfeed/manifest.json b/homeassistant/components/goalfeed/manifest.json index a4d7cd50686..f0202dbb4f3 100644 --- a/homeassistant/components/goalfeed/manifest.json +++ b/homeassistant/components/goalfeed/manifest.json @@ -2,9 +2,7 @@ "domain": "goalfeed", "name": "Goalfeed", "documentation": "https://www.home-assistant.io/integrations/goalfeed", - "requirements": [ - "pysher==1.0.1" - ], + "requirements": ["pysher==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index d8878c8b351..690f2098cac 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -2,9 +2,7 @@ "domain": "gogogate2", "name": "Gogogate2", "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": [ - "pygogogate2==0.1.1" - ], + "requirements": ["pygogogate2==0.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 9cb9be0fa4f..0e7ccd33b33 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -1,23 +1,22 @@ """Support for Google - Calendar Event Devices.""" -from datetime import timedelta, datetime +from datetime import datetime, timedelta import logging import os -import yaml +from googleapiclient import discovery as google_discovery import httplib2 from oauth2client.client import ( - OAuth2WebServerFlow, - OAuth2DeviceCodeError, FlowExchangeError, + OAuth2DeviceCodeError, + OAuth2WebServerFlow, ) from oauth2client.file import Storage -from googleapiclient import discovery as google_discovery - import voluptuous as vol from voluptuous.error import Error as VoluptuousError +import yaml -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_time_change from homeassistant.util import convert, dt diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index d72cc992f6d..5c1be98bb56 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -1,6 +1,6 @@ { "domain": "google", - "name": "Google", + "name": "Google Calendars", "documentation": "https://www.home-assistant.io/integrations/google", "requirements": [ "google-api-python-client==1.6.4", diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index ecb6d767817..2d848101def 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -1,39 +1,39 @@ """Support for Actions on Google Assistant Smart Home Control.""" import logging -from typing import Dict, Any +from typing import Any, Dict import voluptuous as vol # Typing imports -from homeassistant.core import HomeAssistant, ServiceCall - from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import config_validation as cv from .const import ( - DOMAIN, - CONF_PROJECT_ID, - CONF_EXPOSE_BY_DEFAULT, - DEFAULT_EXPOSE_BY_DEFAULT, - CONF_EXPOSED_DOMAINS, - DEFAULT_EXPOSED_DOMAINS, + CONF_ALIASES, + CONF_ALLOW_UNLOCK, CONF_API_KEY, - SERVICE_REQUEST_SYNC, + CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, - CONF_ALIASES, + CONF_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, + CONF_PRIVATE_KEY, + CONF_PROJECT_ID, CONF_REPORT_STATE, CONF_ROOM_HINT, - CONF_ALLOW_UNLOCK, CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, - CONF_CLIENT_EMAIL, - CONF_PRIVATE_KEY, + DEFAULT_EXPOSE_BY_DEFAULT, + DEFAULT_EXPOSED_DOMAINS, + DOMAIN, + SERVICE_REQUEST_SYNC, ) -from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 from .http import GoogleAssistantView, GoogleConfig +from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401, isort:skip + _LOGGER = logging.getLogger(__name__) ENTITY_SCHEMA = vol.Schema( @@ -65,6 +65,7 @@ def _check_report_state(data): GOOGLE_ASSISTANT_SCHEMA = vol.All( cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), + cv.deprecated(CONF_API_KEY, invalidation_version="0.105"), vol.Schema( { vol.Required(CONF_PROJECT_ID): cv.string, diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 35a04e0e08e..dcb87d1d93d 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -1,5 +1,6 @@ """Constants for Google Assistant.""" from homeassistant.components import ( + alarm_control_panel, binary_sensor, camera, climate, @@ -15,7 +16,6 @@ from homeassistant.components import ( sensor, switch, vacuum, - alarm_control_panel, ) DOMAIN = "google_assistant" diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 64375beaf0d..6493d759880 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -8,26 +8,26 @@ from typing import List, Optional from aiohttp.web import json_response -from homeassistant.core import Context, callback, HomeAssistant, State -from homeassistant.helpers.event import async_call_later from homeassistant.components import webhook -from homeassistant.helpers.storage import Store from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + CLOUD_NEVER_EXPOSED_ENTITIES, CONF_NAME, STATE_UNAVAILABLE, - ATTR_SUPPORTED_FEATURES, - ATTR_DEVICE_CLASS, - CLOUD_NEVER_EXPOSED_ENTITIES, ) +from homeassistant.core import Context, HomeAssistant, State, callback +from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.storage import Store from . import trait from .const import ( + CONF_ALIASES, + CONF_ROOM_HINT, + DEVICE_CLASS_TO_GOOGLE_TYPES, DOMAIN, DOMAIN_TO_GOOGLE_TYPES, - CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, - DEVICE_CLASS_TO_GOOGLE_TYPES, - CONF_ROOM_HINT, STORE_AGENT_USER_IDS, ) from .error import SmartHomeError @@ -124,6 +124,7 @@ class AbstractConfig(ABC): def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep + # pylint: disable=import-outside-toplevel from .report_state import async_enable_report_state if self._unsub_report_state is None: @@ -218,6 +219,8 @@ class AbstractConfig(ABC): async def _handle_local_webhook(self, hass, webhook_id, request): """Handle an incoming local SDK message.""" + # Circular dep + # pylint: disable=import-outside-toplevel from . import smart_home payload = await request.json() diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index 3b3c5f1a2c1..f8fa51da8d7 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -3,10 +3,10 @@ import asyncio from datetime import timedelta import logging from uuid import uuid4 -import jwt -from aiohttp import ClientResponseError, ClientError +from aiohttp import ClientError, ClientResponseError from aiohttp.web import Request, Response +import jwt # Typing imports from homeassistant.components.http import HomeAssistantView @@ -15,24 +15,24 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import dt as dt_util from .const import ( - GOOGLE_ASSISTANT_API_ENDPOINT, CONF_API_KEY, - CONF_EXPOSE_BY_DEFAULT, - CONF_EXPOSED_DOMAINS, + CONF_CLIENT_EMAIL, CONF_ENTITY_CONFIG, CONF_EXPOSE, + CONF_EXPOSE_BY_DEFAULT, + CONF_EXPOSED_DOMAINS, + CONF_PRIVATE_KEY, CONF_REPORT_STATE, CONF_SECURE_DEVICES_PIN, CONF_SERVICE_ACCOUNT, - CONF_CLIENT_EMAIL, - CONF_PRIVATE_KEY, - HOMEGRAPH_TOKEN_URL, + GOOGLE_ASSISTANT_API_ENDPOINT, HOMEGRAPH_SCOPE, + HOMEGRAPH_TOKEN_URL, REPORT_STATE_BASE_URL, REQUEST_SYNC_BASE_URL, ) -from .smart_home import async_handle_message from .helpers import AbstractConfig +from .smart_home import async_handle_message _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_assistant/manifest.json b/homeassistant/components/google_assistant/manifest.json index 94dd3b7f079..0f266801343 100644 --- a/homeassistant/components/google_assistant/manifest.json +++ b/homeassistant/components/google_assistant/manifest.json @@ -1,6 +1,6 @@ { "domain": "google_assistant", - "name": "Google assistant", + "name": "Google Assistant", "documentation": "https://www.home-assistant.io/integrations/google_assistant", "requirements": [], "dependencies": ["http"], diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 78a0f50e277..1e8b6c020de 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,12 +1,12 @@ """Google Report State implementation.""" import logging -from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later -from .helpers import AbstractConfig, GoogleEntity, async_get_entities from .error import SmartHomeError +from .helpers import AbstractConfig, GoogleEntity, async_get_entities # Time to wait until the homegraph updates # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 680a6f7ecf1..8033bcec865 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -3,20 +3,19 @@ import asyncio from itertools import product import logging +from homeassistant.const import ATTR_ENTITY_ID, __version__ from homeassistant.util.decorator import Registry -from homeassistant.const import ATTR_ENTITY_ID, __version__ - from .const import ( - ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, + ERR_PROTOCOL_ERROR, ERR_UNKNOWN_ERROR, EVENT_COMMAND_RECEIVED, - EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED, + EVENT_SYNC_RECEIVED, ) -from .helpers import RequestData, GoogleEntity, async_get_entities from .error import SmartHomeError +from .helpers import GoogleEntity, RequestData, async_get_entities HANDLERS = Registry() _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 5b089459d83..14839066ebe 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2,66 +2,68 @@ import logging from homeassistant.components import ( + alarm_control_panel, binary_sensor, camera, cover, - group, fan, + group, input_boolean, - media_player, light, lock, + media_player, scene, script, sensor, switch, vacuum, - alarm_control_panel, ) from homeassistant.components.climate import const as climate from homeassistant.const import ( - ATTR_ENTITY_ID, + ATTR_ASSUMED_STATE, + ATTR_CODE, ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + ATTR_TEMPERATURE, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_CUSTOM_BYPASS, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, STATE_LOCKED, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_SUPPORTED_FEATURES, - ATTR_TEMPERATURE, - ATTR_ASSUMED_STATE, - SERVICE_ALARM_DISARM, - SERVICE_ALARM_ARM_HOME, - SERVICE_ALARM_ARM_AWAY, - SERVICE_ALARM_ARM_NIGHT, - SERVICE_ALARM_ARM_CUSTOM_BYPASS, - SERVICE_ALARM_TRIGGER, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_ARMED_CUSTOM_BYPASS, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, - STATE_ALARM_PENDING, - ATTR_CODE, - STATE_UNKNOWN, ) from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.util import color as color_util, temperature as temp_util + from .const import ( - ERR_VALUE_OUT_OF_RANGE, - ERR_NOT_SUPPORTED, - ERR_FUNCTION_NOT_SUPPORTED, - ERR_CHALLENGE_NOT_SETUP, CHALLENGE_ACK_NEEDED, - CHALLENGE_PIN_NEEDED, CHALLENGE_FAILED_PIN_NEEDED, - ERR_ALREADY_DISARMED, + CHALLENGE_PIN_NEEDED, ERR_ALREADY_ARMED, + ERR_ALREADY_DISARMED, + ERR_CHALLENGE_NOT_SETUP, + ERR_FUNCTION_NOT_SUPPORTED, + ERR_NOT_SUPPORTED, + ERR_VALUE_OUT_OF_RANGE, ) -from .error import SmartHomeError, ChallengeNeeded +from .error import ChallengeNeeded, SmartHomeError _LOGGER = logging.getLogger(__name__) @@ -665,7 +667,7 @@ class TemperatureSettingTrait(_Trait): device_class = attrs.get(ATTR_DEVICE_CLASS) if device_class == sensor.DEVICE_CLASS_TEMPERATURE: current_temp = self.state.state - if current_temp is not None: + if current_temp not in (STATE_UNKNOWN, STATE_UNAVAILABLE): response["thermostatTemperatureAmbient"] = round( temp_util.convert(float(current_temp), unit, TEMP_CELSIUS), 1 ) @@ -886,7 +888,7 @@ class HumiditySettingTrait(_Trait): device_class = attrs.get(ATTR_DEVICE_CLASS) if device_class == sensor.DEVICE_CLASS_HUMIDITY: current_humidity = self.state.state - if current_humidity is not None: + if current_humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): response["humidityAmbientPercent"] = round(float(current_humidity)) return response @@ -1119,98 +1121,9 @@ class ModesTrait(_Trait): name = TRAIT_MODES commands = [COMMAND_MODES] - # Google requires specific mode names and settings. Here is the full list. - # https://developers.google.com/actions/reference/smarthome/traits/modes - # All settings are mapped here as of 2018-11-28 and can be used for other - # entity types. - - HA_TO_GOOGLE = {media_player.ATTR_INPUT_SOURCE: "input source"} - SUPPORTED_MODE_SETTINGS = { - "xsmall": ["xsmall", "extra small", "min", "minimum", "tiny", "xs"], - "small": ["small", "half"], - "large": ["large", "big", "full"], - "xlarge": ["extra large", "xlarge", "xl"], - "Cool": ["cool", "rapid cool", "rapid cooling"], - "Heat": ["heat"], - "Low": ["low"], - "Medium": ["medium", "med", "mid", "half"], - "High": ["high"], - "Auto": ["auto", "automatic"], - "Bake": ["bake"], - "Roast": ["roast"], - "Convection Bake": ["convection bake", "convect bake"], - "Convection Roast": ["convection roast", "convect roast"], - "Favorite": ["favorite"], - "Broil": ["broil"], - "Warm": ["warm"], - "Off": ["off"], - "On": ["on"], - "Normal": [ - "normal", - "normal mode", - "normal setting", - "standard", - "schedule", - "original", - "default", - "old settings", - ], - "None": ["none"], - "Tap Cold": ["tap cold"], - "Cold Warm": ["cold warm"], - "Hot": ["hot"], - "Extra Hot": ["extra hot"], - "Eco": ["eco"], - "Wool": ["wool", "fleece"], - "Turbo": ["turbo"], - "Rinse": ["rinse", "rinsing", "rinse wash"], - "Away": ["away", "holiday"], - "maximum": ["maximum"], - "media player": ["media player"], - "chromecast": ["chromecast"], - "tv": [ - "tv", - "television", - "tv position", - "television position", - "watching tv", - "watching tv position", - "entertainment", - "entertainment position", - ], - "am fm": ["am fm", "am radio", "fm radio"], - "internet radio": ["internet radio"], - "satellite": ["satellite"], - "game console": ["game console"], - "antifrost": ["antifrost", "anti-frost"], - "boost": ["boost"], - "Clock": ["clock"], - "Message": ["message"], - "Messages": ["messages"], - "News": ["news"], - "Disco": ["disco"], - "antifreeze": ["antifreeze", "anti-freeze", "anti freeze"], - "balanced": ["balanced", "normal"], - "swing": ["swing"], - "media": ["media", "media mode"], - "panic": ["panic"], - "ring": ["ring"], - "frozen": ["frozen", "rapid frozen", "rapid freeze"], - "cotton": ["cotton", "cottons"], - "blend": ["blend", "mix"], - "baby wash": ["baby wash"], - "synthetics": ["synthetic", "synthetics", "compose"], - "hygiene": ["hygiene", "sterilization"], - "smart": ["smart", "intelligent", "intelligence"], - "comfortable": ["comfortable", "comfort"], - "manual": ["manual"], - "energy saving": ["energy saving"], - "sleep": ["sleep"], - "quick wash": ["quick wash", "fast wash"], - "cold": ["cold"], - "airsupply": ["airsupply", "air supply"], - "dehumidification": ["dehumidication", "dehumidify"], - "game": ["game", "game mode"], + SYNONYMS = { + "input source": ["input source", "input", "source"], + "sound mode": ["sound mode", "effects"], } @staticmethod @@ -1219,42 +1132,51 @@ class ModesTrait(_Trait): if domain != media_player.DOMAIN: return False - return features & media_player.SUPPORT_SELECT_SOURCE + return ( + features & media_player.SUPPORT_SELECT_SOURCE + or features & media_player.SUPPORT_SELECT_SOUND_MODE + ) def sync_attributes(self): """Return mode attributes for a sync request.""" - sources_list = self.state.attributes.get( - media_player.ATTR_INPUT_SOURCE_LIST, [] - ) - modes = [] - sources = {} - if sources_list: - sources = { - "name": self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE), - "name_values": [{"name_synonym": ["input source"], "lang": "en"}], + def _generate(name, settings): + mode = { + "name": name, + "name_values": [ + {"name_synonym": self.SYNONYMS.get(name, [name]), "lang": "en"} + ], "settings": [], "ordered": False, } - for source in sources_list: - if source in self.SUPPORTED_MODE_SETTINGS: - src = source - synonyms = self.SUPPORTED_MODE_SETTINGS.get(src) - elif source.lower() in self.SUPPORTED_MODE_SETTINGS: - src = source.lower() - synonyms = self.SUPPORTED_MODE_SETTINGS.get(src) - - else: - continue - - sources["settings"].append( + for setting in settings: + mode["settings"].append( { - "setting_name": src, - "setting_values": [{"setting_synonym": synonyms, "lang": "en"}], + "setting_name": setting, + "setting_values": [ + { + "setting_synonym": self.SYNONYMS.get( + setting, [setting] + ), + "lang": "en", + } + ], } ) - if sources: - modes.append(sources) + return mode + + attrs = self.state.attributes + modes = [] + if media_player.ATTR_INPUT_SOURCE_LIST in attrs: + modes.append( + _generate("input source", attrs[media_player.ATTR_INPUT_SOURCE_LIST]) + ) + + if media_player.ATTR_SOUND_MODE_LIST in attrs: + modes.append( + _generate("sound mode", attrs[media_player.ATTR_SOUND_MODE_LIST]) + ) + payload = {"availableModes": modes} return payload @@ -1265,14 +1187,12 @@ class ModesTrait(_Trait): response = {} mode_settings = {} - if attrs.get(media_player.ATTR_INPUT_SOURCE_LIST): - mode_settings.update( - { - media_player.ATTR_INPUT_SOURCE: attrs.get( - media_player.ATTR_INPUT_SOURCE - ) - } - ) + if media_player.ATTR_INPUT_SOURCE_LIST in attrs: + mode_settings["input source"] = attrs.get(media_player.ATTR_INPUT_SOURCE) + + if media_player.ATTR_SOUND_MODE_LIST in attrs: + mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE) + if mode_settings: response["on"] = self.state.state != STATE_OFF response["online"] = True @@ -1283,25 +1203,32 @@ class ModesTrait(_Trait): async def execute(self, command, data, params, challenge): """Execute an SetModes command.""" settings = params.get("updateModeSettings") - requested_source = settings.get( - self.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE) - ) + requested_source = settings.get("input source") + sound_mode = settings.get("sound mode") if requested_source: - for src in self.state.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST): - if src.lower() == requested_source.lower(): - source = src + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_SELECT_SOURCE, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_INPUT_SOURCE: requested_source, + }, + blocking=True, + context=data.context, + ) - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_SELECT_SOURCE, - { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_INPUT_SOURCE: source, - }, - blocking=True, - context=data.context, - ) + if sound_mode: + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_SELECT_SOUND_MODE, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_SOUND_MODE: sound_mode, + }, + blocking=True, + context=data.context, + ) @register_trait diff --git a/homeassistant/components/google_cloud/manifest.json b/homeassistant/components/google_cloud/manifest.json index bc9c71c5a61..bef8a2f08a9 100644 --- a/homeassistant/components/google_cloud/manifest.json +++ b/homeassistant/components/google_cloud/manifest.json @@ -1,12 +1,8 @@ { - "domain": "google_cloud", - "name": "Google Cloud Platform", - "documentation": "https://www.home-assistant.io/integrations/google_cloud", - "requirements": [ - "google-cloud-texttospeech==0.4.0" - ], - "dependencies": [], - "codeowners": [ - "@lufton" - ] + "domain": "google_cloud", + "name": "Google Cloud Platform", + "documentation": "https://www.home-assistant.io/integrations/google_cloud", + "requirements": ["google-cloud-texttospeech==0.4.0"], + "dependencies": [], + "codeowners": ["@lufton"] } diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 942ee0a4e48..6721520d130 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -1,11 +1,11 @@ """Support for the Google Cloud TTS service.""" +import asyncio import logging import os -import asyncio import async_timeout -import voluptuous as vol from google.cloud import texttospeech +import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/google_domains/__init__.py b/homeassistant/components/google_domains/__init__.py index 8f975db6fd8..d440567d9ad 100644 --- a/homeassistant/components/google_domains/__init__.py +++ b/homeassistant/components/google_domains/__init__.py @@ -7,8 +7,8 @@ import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/google_domains/manifest.json b/homeassistant/components/google_domains/manifest.json index 64076434aa5..0d47135be50 100644 --- a/homeassistant/components/google_domains/manifest.json +++ b/homeassistant/components/google_domains/manifest.json @@ -1,6 +1,6 @@ { "domain": "google_domains", - "name": "Google domains", + "name": "Google Domains", "documentation": "https://www.home-assistant.io/integrations/google_domains", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 75f370e502e..9e33ff5f715 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -53,6 +53,7 @@ class GoogleMapsScanner: self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(seconds=60) + self._prev_seen = {} credfile = "{}.{}".format( hass.config.path(CREDENTIALS_FILE), slugify(self.username) @@ -92,11 +93,22 @@ class GoogleMapsScanner: ) continue + last_seen = dt_util.as_utc(person.datetime) + if last_seen < self._prev_seen.get(dev_id, last_seen): + _LOGGER.warning( + "Ignoring %s update because timestamp " + "is older than last timestamp", + person.nickname, + ) + _LOGGER.debug("%s < %s", last_seen, self._prev_seen[dev_id]) + continue + self._prev_seen[dev_id] = last_seen + attrs = { ATTR_ADDRESS: person.address, ATTR_FULL_NAME: person.full_name, ATTR_ID: person.id, - ATTR_LAST_SEEN: dt_util.as_utc(person.datetime), + ATTR_LAST_SEEN: last_seen, ATTR_NICKNAME: person.nickname, ATTR_BATTERY_CHARGING: person.charging, ATTR_BATTERY_LEVEL: person.battery_level, diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index 30571c33865..dc93bbe5c94 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -1,10 +1,8 @@ { "domain": "google_maps", - "name": "Google maps", + "name": "Google Maps", "documentation": "https://www.home-assistant.io/integrations/google_maps", - "requirements": [ - "locationsharinglib==4.1.0" - ], + "requirements": ["locationsharinglib==4.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/google_pubsub/__init__.py b/homeassistant/components/google_pubsub/__init__.py index c4136c3b9cb..bc7811a7a8f 100644 --- a/homeassistant/components/google_pubsub/__init__.py +++ b/homeassistant/components/google_pubsub/__init__.py @@ -5,6 +5,7 @@ import logging import os from typing import Any, Dict +from google.cloud import pubsub_v1 import voluptuous as vol from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN @@ -38,7 +39,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): """Activate Google Pub/Sub component.""" - from google.cloud import pubsub_v1 config = yaml_config[DOMAIN] project_id = config[CONF_PROJECT_ID] diff --git a/homeassistant/components/google_pubsub/manifest.json b/homeassistant/components/google_pubsub/manifest.json index b23a101ca46..1a59e453c6e 100644 --- a/homeassistant/components/google_pubsub/manifest.json +++ b/homeassistant/components/google_pubsub/manifest.json @@ -1,10 +1,8 @@ { "domain": "google_pubsub", - "name": "Google pubsub", + "name": "Google Pub/Sub", "documentation": "https://www.home-assistant.io/integrations/google_pubsub", - "requirements": [ - "google-cloud-pubsub==0.39.1" - ], + "requirements": ["google-cloud-pubsub==0.39.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/google_translate/manifest.json b/homeassistant/components/google_translate/manifest.json index 8b9621b4236..dba7020d076 100644 --- a/homeassistant/components/google_translate/manifest.json +++ b/homeassistant/components/google_translate/manifest.json @@ -1,12 +1,8 @@ { "domain": "google_translate", - "name": "Google Translate", + "name": "Google Translate Text-to-Speech", "documentation": "https://www.home-assistant.io/integrations/google_translate", - "requirements": [ - "gTTS-token==1.1.3" - ], + "requirements": ["gTTS-token==1.1.3"], "dependencies": [], - "codeowners": [ - "@awarecan" - ] + "codeowners": ["@awarecan"] } diff --git a/homeassistant/components/google_translate/tts.py b/homeassistant/components/google_translate/tts.py index 3add45b8cb8..e35a229ab98 100644 --- a/homeassistant/components/google_translate/tts.py +++ b/homeassistant/components/google_translate/tts.py @@ -6,6 +6,7 @@ import re import aiohttp from aiohttp.hdrs import REFERER, USER_AGENT import async_timeout +from gtts_token import gtts_token import voluptuous as vol import yarl @@ -115,7 +116,6 @@ class GoogleProvider(Provider): async def async_get_tts_audio(self, message, language, options=None): """Load TTS from google.""" - from gtts_token import gtts_token token = gtts_token.Token() websession = async_get_clientsession(self.hass) diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index f4113f85a31..ce7ca9d10ab 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -1,12 +1,8 @@ { "domain": "google_travel_time", - "name": "Google travel time", + "name": "Google Maps Travel Time", "documentation": "https://www.home-assistant.io/integrations/google_travel_time", - "requirements": [ - "googlemaps==2.5.1" - ], + "requirements": ["googlemaps==2.5.1"], "dependencies": [], - "codeowners": [ - "@robbiet480" - ] + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/google_wifi/manifest.json b/homeassistant/components/google_wifi/manifest.json index 77062cbdd26..b46cea0ca46 100644 --- a/homeassistant/components/google_wifi/manifest.json +++ b/homeassistant/components/google_wifi/manifest.json @@ -1,6 +1,6 @@ { "domain": "google_wifi", - "name": "Google wifi", + "name": "Google Wifi", "documentation": "https://www.home-assistant.io/integrations/google_wifi", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/google_wifi/sensor.py b/homeassistant/components/google_wifi/sensor.py index 1d4ed8d84f8..9d6f3ea3d58 100644 --- a/homeassistant/components/google_wifi/sensor.py +++ b/homeassistant/components/google_wifi/sensor.py @@ -1,21 +1,20 @@ """Support for retrieving status info from Google Wifi/OnHub routers.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol import requests +import voluptuous as vol -from homeassistant.util import dt -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, CONF_MONITORED_CONDITIONS, + CONF_NAME, STATE_UNKNOWN, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gpmdp/manifest.json b/homeassistant/components/gpmdp/manifest.json index a3c2389478e..c2128b27eeb 100644 --- a/homeassistant/components/gpmdp/manifest.json +++ b/homeassistant/components/gpmdp/manifest.json @@ -1,10 +1,8 @@ { "domain": "gpmdp", - "name": "Gpmdp", + "name": "Google Play Music Desktop Player (GPMDP)", "documentation": "https://www.home-assistant.io/integrations/gpmdp", - "requirements": [ - "websocket-client==0.54.0" - ], + "requirements": ["websocket-client==0.54.0"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/homeassistant/components/gpsd/manifest.json b/homeassistant/components/gpsd/manifest.json index 7bb828cacf9..16a1bbd51df 100644 --- a/homeassistant/components/gpsd/manifest.json +++ b/homeassistant/components/gpsd/manifest.json @@ -1,12 +1,8 @@ { "domain": "gpsd", - "name": "Gpsd", + "name": "GPSD", "documentation": "https://www.home-assistant.io/integrations/gpsd", - "requirements": [ - "gps3==0.33.3" - ], + "requirements": ["gps3==0.33.3"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/gpslogger/.translations/da.json b/homeassistant/components/gpslogger/.translations/da.json index 6d5c2185718..b118783cd3c 100644 --- a/homeassistant/components/gpslogger/.translations/da.json +++ b/homeassistant/components/gpslogger/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage GPSLogger meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage GPSLogger-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 GPSLogger.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i GPSLogger.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/gpslogger/.translations/ko.json b/homeassistant/components/gpslogger/.translations/ko.json index 786a67b0b19..19bfc36e424 100644 --- a/homeassistant/components/gpslogger/.translations/ko.json +++ b/homeassistant/components/gpslogger/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "GPSLogger Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "GPSLogger Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "GPSLogger Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/gpslogger/__init__.py b/homeassistant/components/gpslogger/__init__.py index 3ac09457d81..aa95d17cbfc 100644 --- a/homeassistant/components/gpslogger/__init__.py +++ b/homeassistant/components/gpslogger/__init__.py @@ -1,30 +1,33 @@ """Support for GPSLogger.""" import logging -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.device_tracker import ATTR_BATTERY +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + DOMAIN as DEVICE_TRACKER, +) from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, - HTTP_OK, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_WEBHOOK_ID, + HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, ) from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER + from .const import ( - DOMAIN, - ATTR_ALTITUDE, ATTR_ACCURACY, ATTR_ACTIVITY, + ATTR_ALTITUDE, ATTR_DEVICE, ATTR_DIRECTION, ATTR_PROVIDER, ATTR_SPEED, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gpslogger/config_flow.py b/homeassistant/components/gpslogger/config_flow.py index 5173c02e7ff..ef90a8d1607 100644 --- a/homeassistant/components/gpslogger/config_flow.py +++ b/homeassistant/components/gpslogger/config_flow.py @@ -1,7 +1,7 @@ """Config flow for GPSLogger.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py index c9dbbfee026..d8afc377d40 100644 --- a/homeassistant/components/gpslogger/device_tracker.py +++ b/homeassistant/components/gpslogger/device_tracker.py @@ -1,15 +1,15 @@ """Support for the GPSLogger device tracking.""" import logging -from homeassistant.core import callback +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, ) -from homeassistant.components.device_tracker import SOURCE_TYPE_GPS -from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.core import callback from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity diff --git a/homeassistant/components/gpslogger/manifest.json b/homeassistant/components/gpslogger/manifest.json index cbfd79671eb..f4fc556961b 100644 --- a/homeassistant/components/gpslogger/manifest.json +++ b/homeassistant/components/gpslogger/manifest.json @@ -1,11 +1,9 @@ { "domain": "gpslogger", - "name": "Gpslogger", + "name": "GPSLogger", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gpslogger", "requirements": [], - "dependencies": [ - "webhook" - ], + "dependencies": ["webhook"], "codeowners": [] } diff --git a/homeassistant/components/graphite/__init__.py b/homeassistant/components/graphite/__init__.py index 3809249bea6..bf34bc3ddea 100644 --- a/homeassistant/components/graphite/__init__.py +++ b/homeassistant/components/graphite/__init__.py @@ -7,7 +7,6 @@ import time import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -17,6 +16,7 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, ) from homeassistant.helpers import state +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/greeneye_monitor/manifest.json b/homeassistant/components/greeneye_monitor/manifest.json index eb5f19bc1ee..0c55b644d94 100644 --- a/homeassistant/components/greeneye_monitor/manifest.json +++ b/homeassistant/components/greeneye_monitor/manifest.json @@ -1,10 +1,8 @@ { "domain": "greeneye_monitor", - "name": "Greeneye monitor", + "name": "GreenEye Monitor (GEM)", "documentation": "https://www.home-assistant.io/integrations/greeneye_monitor", - "requirements": [ - "greeneye_monitor==1.0.1" - ], + "requirements": ["greeneye_monitor==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/greenwave/manifest.json b/homeassistant/components/greenwave/manifest.json index 20a49e834b0..f0cdd6590d8 100644 --- a/homeassistant/components/greenwave/manifest.json +++ b/homeassistant/components/greenwave/manifest.json @@ -1,10 +1,8 @@ { "domain": "greenwave", - "name": "Greenwave", + "name": "Greenwave Reality", "documentation": "https://www.home-assistant.io/integrations/greenwave", - "requirements": [ - "greenwavereality==0.5.1" - ], + "requirements": ["greenwavereality==0.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index ba12e22b53e..c8a138abe41 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -7,34 +7,33 @@ import voluptuous as vol from homeassistant import core as ha from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_NAME, CONF_ICON, CONF_NAME, + SERVICE_RELOAD, STATE_CLOSED, STATE_HOME, + STATE_LOCKED, STATE_NOT_HOME, STATE_OFF, + STATE_OK, STATE_ON, STATE_OPEN, - STATE_LOCKED, - STATE_UNLOCKED, - STATE_OK, STATE_PROBLEM, STATE_UNKNOWN, - ATTR_ASSUMED_STATE, - SERVICE_RELOAD, - ATTR_NAME, - ATTR_ICON, + STATE_UNLOCKED, ) from homeassistant.core import callback -from homeassistant.loader import bind_hass +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.typing import HomeAssistantType - +from homeassistant.loader import bind_hass # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -583,12 +582,12 @@ class Group(Entity): self._async_update_group_state() async def async_added_to_hass(self): - """Handle addition to HASS.""" + """Handle addition to Home Assistant.""" if self.tracking: self.async_start() async def async_will_remove_from_hass(self): - """Handle removal from HASS.""" + """Handle removal from Home Assistant.""" if self._async_unsub_state_changed: self._async_unsub_state_changed() self._async_unsub_state_changed = None diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index f7a9643e5c8..d9efdfa53c6 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -4,18 +4,6 @@ from typing import Dict, Optional, Set import voluptuous as vol -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_ENTITIES, - CONF_NAME, - STATE_CLOSED, -) -from homeassistant.core import callback, State -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change - from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, @@ -41,7 +29,17 @@ from homeassistant.components.cover import ( SUPPORT_STOP_TILT, CoverDevice, ) - +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_CLOSED, +) +from homeassistant.core import State, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 2cd65028131..3abca98bd2c 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -8,20 +8,6 @@ from typing import Any, Callable, Iterator, List, Optional, Tuple, cast import voluptuous as vol from homeassistant.components import light -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_SUPPORTED_FEATURES, - CONF_ENTITIES, - CONF_NAME, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.core import CALLBACK_TYPE, State, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.util import color as color_util - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -42,7 +28,19 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, ) - +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_ENTITIES, + CONF_NAME, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import CALLBACK_TYPE, State, callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import color as color_util # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -115,7 +113,7 @@ class LightGroup(light.Light): await self.async_update() async def async_will_remove_from_hass(self): - """Handle removal from HASS.""" + """Handle removal from Home Assistant.""" if self._async_unsub_state_changed is not None: self._async_unsub_state_changed() self._async_unsub_state_changed = None diff --git a/homeassistant/components/group/manifest.json b/homeassistant/components/group/manifest.json index 195227ca242..bd117ac9a6f 100644 --- a/homeassistant/components/group/manifest.json +++ b/homeassistant/components/group/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/group", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index e17990690fa..2209e0e2333 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -6,9 +6,6 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_SERVICE -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, @@ -16,7 +13,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) - +from homeassistant.const import ATTR_SERVICE +import homeassistant.helpers.config_validation as cv # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index 827e9bb1dcb..78790701934 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -2,15 +2,16 @@ from typing import Iterable, Optional from homeassistant.core import Context, State +from homeassistant.helpers.state import async_reproduce_state from homeassistant.helpers.typing import HomeAssistantType +from . import get_entity_ids + async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None ) -> None: """Reproduce component states.""" - from . import get_entity_ids - from homeassistant.helpers.state import async_reproduce_state states_copy = [] for state in states: diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index 0d4508c26dc..7457ef14254 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -1,12 +1,8 @@ { "domain": "growatt_server", - "name": "Growatt Server", + "name": "Growatt", "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": [ - "growattServer==0.0.1" - ], + "requirements": ["growattServer==0.0.1"], "dependencies": [], - "codeowners": [ - "@indykoning" - ] + "codeowners": ["@indykoning"] } diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 3b7109222a4..2816b86be84 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -1,17 +1,17 @@ """Read status of growatt inverters.""" -import re +import datetime import json import logging -import datetime +import re 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 +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/gstreamer/manifest.json b/homeassistant/components/gstreamer/manifest.json index 66cae733d9c..81078b1a18b 100644 --- a/homeassistant/components/gstreamer/manifest.json +++ b/homeassistant/components/gstreamer/manifest.json @@ -1,10 +1,8 @@ { "domain": "gstreamer", - "name": "Gstreamer", + "name": "GStreamer", "documentation": "https://www.home-assistant.io/integrations/gstreamer", - "requirements": [ - "gstreamer-player==1.1.2" - ], + "requirements": ["gstreamer-player==1.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index b25134bb79e..a7959584504 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -1,12 +1,8 @@ { "domain": "gtfs", - "name": "Gtfs", + "name": "General Transit Feed Specification (GTFS)", "documentation": "https://www.home-assistant.io/integrations/gtfs", - "requirements": [ - "pygtfs==0.1.5" - ], + "requirements": ["pygtfs==0.1.5"], "dependencies": [], - "codeowners": [ - "@robbiet480" - ] + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index a3ac10a1c6d..ff0d0eb27ac 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -2,9 +2,7 @@ "domain": "habitica", "name": "Habitica", "documentation": "https://www.home-assistant.io/integrations/habitica", - "requirements": [ - "habitipy==0.2.0" - ], + "requirements": ["habitipy==0.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/hangouts/.translations/ca.json b/homeassistant/components/hangouts/.translations/ca.json index ea43c804f2d..0dcc0f029c2 100644 --- a/homeassistant/components/hangouts/.translations/ca.json +++ b/homeassistant/components/hangouts/.translations/ca.json @@ -14,6 +14,7 @@ "data": { "2fa": "Pin 2FA" }, + "description": "buit", "title": "Verificaci\u00f3 en dos passos" }, "user": { @@ -22,6 +23,7 @@ "email": "Correu electr\u00f2nic", "password": "Contrasenya" }, + "description": "buit", "title": "Inici de sessi\u00f3 de Google Hangouts" } }, diff --git a/homeassistant/components/hangouts/.translations/da.json b/homeassistant/components/hangouts/.translations/da.json index 4155da38f8f..2ceb78ddde8 100644 --- a/homeassistant/components/hangouts/.translations/da.json +++ b/homeassistant/components/hangouts/.translations/da.json @@ -5,7 +5,7 @@ "unknown": "Ukendt fejl opstod" }, "error": { - "invalid_2fa": "Ugyldig 2-faktor godkendelse, pr\u00f8v venligst igen.", + "invalid_2fa": "Ugyldig tofaktor-godkendelse, pr\u00f8v igen.", "invalid_2fa_method": "Ugyldig 2FA-metode (Bekr\u00e6ft p\u00e5 telefon).", "invalid_login": "Ugyldig login, pr\u00f8v venligst igen." }, @@ -14,12 +14,12 @@ "data": { "2fa": "2FA pin" }, - "title": "To-faktor autentificering" + "title": "Tofaktor-godkendelse" }, "user": { "data": { - "authorization_code": "Autorisationskode (kr\u00e6ves til manuel godkendelse)", - "email": "Email adresse", + "authorization_code": "Godkendelseskode (kr\u00e6vet til manuel godkendelse)", + "email": "Emailadresse", "password": "Adgangskode" }, "title": "Google Hangouts login" diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 15d90a672de..5bb98effb9f 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -14,6 +14,7 @@ "data": { "2fa": "\u041f\u0438\u043d-\u043a\u043e\u0434 \u0434\u043b\u044f \u0434\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0439 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "\u0414\u0432\u0443\u0445\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f" }, "user": { @@ -22,6 +23,7 @@ "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "Google Hangouts" } }, diff --git a/homeassistant/components/hangouts/hangouts_bot.py b/homeassistant/components/hangouts/hangouts_bot.py index 8575a547a9c..fd14ec0b094 100644 --- a/homeassistant/components/hangouts/hangouts_bot.py +++ b/homeassistant/components/hangouts/hangouts_bot.py @@ -86,15 +86,15 @@ class HangoutsBot: conv_id = self._resolve_conversation_id(conversation) if conv_id is not None: conversations.append(conv_id) - data["_" + CONF_CONVERSATIONS] = conversations + data[f"_{CONF_CONVERSATIONS}"] = conversations elif self._default_conv_ids: - data["_" + CONF_CONVERSATIONS] = self._default_conv_ids + data[f"_{CONF_CONVERSATIONS}"] = self._default_conv_ids else: - data["_" + CONF_CONVERSATIONS] = [ + data[f"_{CONF_CONVERSATIONS}"] = [ conv.id_ for conv in self._conversation_list.get_all() ] - for conv_id in data["_" + CONF_CONVERSATIONS]: + for conv_id in data[f"_{CONF_CONVERSATIONS}"]: if conv_id not in self._conversation_intents: self._conversation_intents[conv_id] = {} diff --git a/homeassistant/components/hangouts/manifest.json b/homeassistant/components/hangouts/manifest.json index 2f222b3c16e..b08387c7fd7 100644 --- a/homeassistant/components/hangouts/manifest.json +++ b/homeassistant/components/hangouts/manifest.json @@ -1,11 +1,9 @@ { "domain": "hangouts", - "name": "Hangouts", + "name": "Google Hangouts", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hangouts", - "requirements": [ - "hangups==0.4.9" - ], + "requirements": ["hangups==0.4.9"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/harman_kardon_avr/manifest.json b/homeassistant/components/harman_kardon_avr/manifest.json index 6bd64942619..060d78fbdee 100644 --- a/homeassistant/components/harman_kardon_avr/manifest.json +++ b/homeassistant/components/harman_kardon_avr/manifest.json @@ -1,10 +1,8 @@ { "domain": "harman_kardon_avr", - "name": "Harman kardon avr", + "name": "Harman Kardon AVR", "documentation": "https://www.home-assistant.io/integrations/harman_kardon_avr", - "requirements": [ - "hkavr==0.0.5" - ], + "requirements": ["hkavr==0.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index af4427c5db3..a0e8baa0b58 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -1,12 +1,8 @@ { "domain": "harmony", - "name": "Harmony", + "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", - "requirements": [ - "aioharmony==0.1.13" - ], + "requirements": ["aioharmony==0.1.13"], "dependencies": [], - "codeowners": [ - "@ehendrix23" - ] + "codeowners": ["@ehendrix23"] } diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 7f4d03ccbb0..c48d5fb00b0 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -369,7 +369,7 @@ class HarmonyRemote(remote.RemoteDevice): for result in result_list: _LOGGER.error( - "Sending command %s to device %s failed with code " "%s: %s", + "Sending command %s to device %s failed with code %s: %s", result.command.command, result.command.device, result.code, diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index e0c0a57375a..28e06cc5d6a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -10,9 +10,9 @@ from homeassistant.components.homeassistant import SERVICE_CHECK_CONFIG import homeassistant.config as conf_util from homeassistant.const import ( ATTR_NAME, + EVENT_CORE_CONFIG_UPDATE, SERVICE_HOMEASSISTANT_RESTART, SERVICE_HOMEASSISTANT_STOP, - EVENT_CORE_CONFIG_UPDATE, ) from homeassistant.core import DOMAIN as HASS_DOMAIN, callback from homeassistant.exceptions import HomeAssistantError @@ -20,8 +20,8 @@ import homeassistant.helpers.config_validation as cv from homeassistant.loader import bind_hass from homeassistant.util.dt import utcnow -from .auth import async_setup_auth_view from .addon_panel import async_setup_addon_panel +from .auth import async_setup_auth_view from .discovery import async_setup_discovery_view from .handler import HassIO, HassioAPIError from .http import HassIOView @@ -136,7 +136,7 @@ def get_homeassistant_version(hass): @callback @bind_hass def is_hassio(hass): - """Return true if hass.io is loaded. + """Return true if Hass.io is loaded. Async friendly. """ @@ -171,7 +171,7 @@ async def async_setup(hass, config): if user and user.refresh_tokens: refresh_token = list(user.refresh_tokens.values())[0] - # Migrate old hass.io users to be admin. + # Migrate old Hass.io users to be admin. if not user.is_admin: await hass.auth.async_update_user(user, group_ids=[GROUP_ID_ADMIN]) @@ -219,7 +219,7 @@ async def async_setup(hass, config): snapshot = data.pop(ATTR_SNAPSHOT, None) payload = None - # Pass data to hass.io API + # Pass data to Hass.io API if service.service == SERVICE_ADDON_STDIN: payload = data[ATTR_INPUT] elif MAP_SERVICE_API[service.service][3]: diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py index b60c864a893..cb509cb19a1 100644 --- a/homeassistant/components/hassio/addon_panel.py +++ b/homeassistant/components/hassio/addon_panel.py @@ -7,7 +7,7 @@ from aiohttp import web from homeassistant.components.http import HomeAssistantView from homeassistant.helpers.typing import HomeAssistantType -from .const import ATTR_PANELS, ATTR_TITLE, ATTR_ICON, ATTR_ADMIN, ATTR_ENABLE +from .const import ATTR_ADMIN, ATTR_ENABLE, ATTR_ICON, ATTR_PANELS, ATTR_TITLE from .handler import HassioAPIError _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index 19e4c63b5a5..800801b4350 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -1,18 +1,18 @@ """Implement the auth feature from Hass.io for Add-ons.""" +from ipaddress import ip_address import logging import os -from ipaddress import ip_address -import voluptuous as vol from aiohttp import web from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from .const import ATTR_ADDON, ATTR_PASSWORD, ATTR_USERNAME diff --git a/homeassistant/components/hassio/discovery.py b/homeassistant/components/hassio/discovery.py index 55336735133..fc6efbe0e58 100644 --- a/homeassistant/components/hassio/discovery.py +++ b/homeassistant/components/hassio/discovery.py @@ -5,9 +5,9 @@ import logging from aiohttp import web from aiohttp.web_exceptions import HTTPServiceUnavailable +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView from .const import ( ATTR_ADDON, diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 3b1b8374510..ddb9269219b 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -7,7 +7,7 @@ from typing import Dict, Union import aiohttp from aiohttp import web -from aiohttp.hdrs import CONTENT_TYPE, CONTENT_LENGTH +from aiohttp.hdrs import CONTENT_LENGTH, CONTENT_TYPE from aiohttp.web_exceptions import HTTPBadGateway import async_timeout diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 53235f80dca..c69d2078468 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -1,8 +1,8 @@ """Hass.io Add-on ingress service.""" import asyncio +from ipaddress import ip_address import logging import os -from ipaddress import ip_address from typing import Dict, Union import aiohttp diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index 23095064d55..d3dd7dc9c94 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -3,11 +3,6 @@ "name": "Hass.io", "documentation": "https://www.home-assistant.io/hassio", "requirements": [], - "dependencies": [ - "http", - "panel_custom" - ], - "codeowners": [ - "@home-assistant/hass-io" - ] + "dependencies": ["http", "panel_custom"], + "codeowners": ["@home-assistant/hass-io"] } diff --git a/homeassistant/components/haveibeenpwned/manifest.json b/homeassistant/components/haveibeenpwned/manifest.json index 00c7b7a19b8..0016bf586cd 100644 --- a/homeassistant/components/haveibeenpwned/manifest.json +++ b/homeassistant/components/haveibeenpwned/manifest.json @@ -1,8 +1,8 @@ { - "domain": "haveibeenpwned", - "name": "Haveibeenpwned", - "documentation": "https://www.home-assistant.io/integrations/haveibeenpwned", - "requirements": [], - "dependencies": [], - "codeowners": [] + "domain": "haveibeenpwned", + "name": "HaveIBeenPwned", + "documentation": "https://www.home-assistant.io/integrations/haveibeenpwned", + "requirements": [], + "dependencies": [], + "codeowners": [] } diff --git a/homeassistant/components/haveibeenpwned/sensor.py b/homeassistant/components/haveibeenpwned/sensor.py index 7fa3f422300..99f94499478 100644 --- a/homeassistant/components/haveibeenpwned/sensor.py +++ b/homeassistant/components/haveibeenpwned/sensor.py @@ -7,7 +7,7 @@ import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_EMAIL, CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_EMAIL import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import track_point_in_time @@ -178,7 +178,7 @@ class HaveIBeenPwnedData: else: _LOGGER.error( - "Failed fetching data for %s" "(HTTP Status_code = %d)", + "Failed fetching data for %s (HTTP Status_code = %d)", self._email, req.status_code, ) diff --git a/homeassistant/components/hddtemp/manifest.json b/homeassistant/components/hddtemp/manifest.json index 484886aff21..6f1d10a9355 100644 --- a/homeassistant/components/hddtemp/manifest.json +++ b/homeassistant/components/hddtemp/manifest.json @@ -1,6 +1,6 @@ { "domain": "hddtemp", - "name": "Hddtemp", + "name": "hddtemp", "documentation": "https://www.home-assistant.io/integrations/hddtemp", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/hddtemp/sensor.py b/homeassistant/components/hddtemp/sensor.py index d0dd5018dca..a1052b0440a 100644 --- a/homeassistant/components/hddtemp/sensor.py +++ b/homeassistant/components/hddtemp/sensor.py @@ -1,21 +1,21 @@ """Support for getting the disk temperature of a host.""" -import logging from datetime import timedelta -from telnetlib import Telnet +import logging import socket +from telnetlib import Telnet import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, + CONF_DISKS, CONF_HOST, + CONF_NAME, CONF_PORT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_DISKS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hdmi_cec/manifest.json b/homeassistant/components/hdmi_cec/manifest.json index 7c877f5c2a7..683b735ec50 100644 --- a/homeassistant/components/hdmi_cec/manifest.json +++ b/homeassistant/components/hdmi_cec/manifest.json @@ -1,10 +1,8 @@ { "domain": "hdmi_cec", - "name": "Hdmi cec", + "name": "HDMI-CEC", "documentation": "https://www.home-assistant.io/integrations/hdmi_cec", - "requirements": [ - "pyCEC==0.4.13" - ], + "requirements": ["pyCEC==0.4.13"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/heatmiser/climate.py b/homeassistant/components/heatmiser/climate.py index 1954749c21b..553ae8f4bc3 100644 --- a/homeassistant/components/heatmiser/climate.py +++ b/homeassistant/components/heatmiser/climate.py @@ -2,28 +2,27 @@ import logging from typing import List +from heatmiserV3 import connection, heatmiser import voluptuous as vol -from heatmiserV3 import heatmiser, connection from homeassistant.components.climate import ( - ClimateDevice, - PLATFORM_SCHEMA, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PLATFORM_SCHEMA, + ClimateDevice, ) from homeassistant.components.climate.const import SUPPORT_TARGET_TEMPERATURE from homeassistant.const import ( - TEMP_CELSIUS, - TEMP_FAHRENHEIT, ATTR_TEMPERATURE, CONF_HOST, - CONF_PORT, CONF_ID, CONF_NAME, + CONF_PORT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) CONF_THERMOSTATS = "tstats" diff --git a/homeassistant/components/heatmiser/manifest.json b/homeassistant/components/heatmiser/manifest.json index 89bcec08125..d8ecb505390 100644 --- a/homeassistant/components/heatmiser/manifest.json +++ b/homeassistant/components/heatmiser/manifest.json @@ -2,11 +2,7 @@ "domain": "heatmiser", "name": "Heatmiser", "documentation": "https://www.home-assistant.io/integrations/heatmiser", - "requirements": [ - "heatmiserV3==1.1.18" - ], + "requirements": ["heatmiserV3==1.1.18"], "dependencies": [], - "codeowners": [ - "@andylockran" - ] -} \ No newline at end of file + "codeowners": ["@andylockran"] +} diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 4380cb4d8ba..7e7fe067874 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,9 +1,12 @@ """Config flow to configure Heos.""" +from urllib.parse import urlparse + from pyheos import Heos, HeosError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.components import ssdp +from homeassistant.const import CONF_HOST from .const import DATA_DISCOVERED_HOSTS, DOMAIN @@ -23,11 +26,12 @@ class HeosFlowHandler(config_entries.ConfigFlow): async def async_step_ssdp(self, discovery_info): """Handle a discovered Heos device.""" # Store discovered host + hostname = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname friendly_name = "{} ({})".format( - discovery_info[CONF_NAME], discovery_info[CONF_HOST] + discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME], hostname ) self.hass.data.setdefault(DATA_DISCOVERED_HOSTS, {}) - self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = discovery_info[CONF_HOST] + self.hass.data[DATA_DISCOVERED_HOSTS][friendly_name] = hostname # Abort if other flows in progress or an entry already exists if self._async_in_progress() or self._async_current_entries(): return self.async_abort(reason="already_setup") diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 684127e519e..02f3d03ae52 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -1,18 +1,14 @@ { "domain": "heos", - "name": "HEOS", + "name": "Denon HEOS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/heos", - "requirements": [ - "pyheos==0.6.0" - ], + "requirements": ["pyheos==0.6.0"], "ssdp": [ { "st": "urn:schemas-denon-com:device:ACT-Denon:1" } ], "dependencies": [], - "codeowners": [ - "@andrewsayre" - ] + "codeowners": ["@andrewsayre"] } diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index 10ea28ca16c..9016a8b3cea 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -115,7 +115,6 @@ class HeosMediaPlayer(MediaPlayerDevice): async def async_added_to_hass(self): """Device added to hass.""" - self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] # Update state when attributes of the player change self._signals.append( self._player.heos.dispatcher.connect( @@ -242,6 +241,9 @@ class HeosMediaPlayer(MediaPlayerDevice): current_support = [CONTROL_TO_SUPPORT[control] for control in controls] self._supported_features = reduce(ior, current_support, BASE_SUPPORTED_FEATURES) + if self._source_manager is None: + self._source_manager = self.hass.data[HEOS_DOMAIN][DATA_SOURCE_MANAGER] + async def async_will_remove_from_hass(self): """Disconnect the device when removed.""" for signal_remove in self._signals: diff --git a/homeassistant/components/here_travel_time/__init__.py b/homeassistant/components/here_travel_time/__init__.py old mode 100755 new mode 100644 diff --git a/homeassistant/components/here_travel_time/manifest.json b/homeassistant/components/here_travel_time/manifest.json old mode 100755 new mode 100644 index 5ef71a249e6..fcef464aa88 --- a/homeassistant/components/here_travel_time/manifest.json +++ b/homeassistant/components/here_travel_time/manifest.json @@ -1,12 +1,8 @@ { - "domain": "here_travel_time", - "name": "HERE travel time", - "documentation": "https://www.home-assistant.io/integrations/here_travel_time", - "requirements": [ - "herepy==0.6.3.3" - ], - "dependencies": [], - "codeowners": [ - "@eifinger" - ] - } + "domain": "here_travel_time", + "name": "HERE Travel Time", + "documentation": "https://www.home-assistant.io/integrations/here_travel_time", + "requirements": ["herepy==2.0.0"], + "dependencies": [], + "codeowners": ["@eifinger"] +} diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py old mode 100755 new mode 100644 index 0b688a770c5..316e73dc096 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -32,8 +32,7 @@ CONF_DESTINATION_ENTITY_ID = "destination_entity_id" CONF_ORIGIN_LATITUDE = "origin_latitude" CONF_ORIGIN_LONGITUDE = "origin_longitude" CONF_ORIGIN_ENTITY_ID = "origin_entity_id" -CONF_APP_ID = "app_id" -CONF_APP_CODE = "app_code" +CONF_API_KEY = "api_key" CONF_TRAFFIC_MODE = "traffic_mode" CONF_ROUTE_MODE = "route_mode" @@ -97,8 +96,7 @@ PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), PLATFORM_SCHEMA.extend( { - vol.Required(CONF_APP_ID): cv.string, - vol.Required(CONF_APP_CODE): cv.string, + vol.Required(CONF_API_KEY): cv.string, vol.Inclusive( CONF_DESTINATION_LATITUDE, "destination_coordinates" ): cv.latitude, @@ -131,9 +129,8 @@ async def async_setup_platform( ) -> None: """Set up the HERE travel time platform.""" - app_id = config[CONF_APP_ID] - app_code = config[CONF_APP_CODE] - here_client = herepy.RoutingApi(app_id, app_code) + api_key = config[CONF_API_KEY] + here_client = herepy.RoutingApi(api_key) if not await hass.async_add_executor_job( _are_valid_client_credentials, here_client @@ -229,7 +226,7 @@ class HERETravelTimeSensor(Entity): @callback def delayed_sensor_update(event): - """Update sensor after homeassistant started.""" + """Update sensor after Home Assistant started.""" self.async_schedule_update_ha_state(True) self.hass.bus.async_listen_once( diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index d5171896464..45b2686ada2 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -2,11 +2,7 @@ "domain": "hikvision", "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", - "requirements": [ - "pyhik==0.2.5" - ], + "requirements": ["pyhik==0.2.5"], "dependencies": [], - "codeowners": [ - "@mezz64" - ] + "codeowners": ["@mezz64"] } diff --git a/homeassistant/components/hikvisioncam/manifest.json b/homeassistant/components/hikvisioncam/manifest.json index 8dcef17fad6..277617a9032 100644 --- a/homeassistant/components/hikvisioncam/manifest.json +++ b/homeassistant/components/hikvisioncam/manifest.json @@ -1,10 +1,8 @@ { "domain": "hikvisioncam", - "name": "Hikvisioncam", + "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvisioncam", - "requirements": [ - "hikvision==0.4" - ], + "requirements": ["hikvision==0.4"], "dependencies": [], "codeowners": ["@fbradyirl"] } diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 020b894c0f7..f86853a5468 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -5,7 +5,7 @@ import hikvision.api from hikvision.error import HikvisionError, MissingParamError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -16,7 +16,6 @@ from homeassistant.const import ( STATE_ON, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import ToggleEntity # This is the last working version, please test before updating @@ -60,7 +59,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([HikvisionMotionSwitch(name, hikvision_cam)]) -class HikvisionMotionSwitch(ToggleEntity): +class HikvisionMotionSwitch(SwitchDevice): """Representation of a switch to toggle on/off motion detection.""" def __init__(self, name, hikvision_cam): diff --git a/homeassistant/components/hisense_aehw4a1/.translations/da.json b/homeassistant/components/hisense_aehw4a1/.translations/da.json new file mode 100644 index 00000000000..3d479543231 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/da.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Ingen Hisense AEH-W4A1-enheder fundet p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af Hisense AEH-W4A1 er mulig." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/ko.json b/homeassistant/components/hisense_aehw4a1/.translations/ko.json new file mode 100644 index 00000000000..6d8b6b4b44c --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/ko.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Hisense AEH-W4A1 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 Hisense AEH-W4A1 \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "step": { + "confirm": { + "description": "Hisense AEH-W4A1 \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/.translations/nl.json b/homeassistant/components/hisense_aehw4a1/.translations/nl.json new file mode 100644 index 00000000000..7360908a11d --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen Hisense AEH-W4A1-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Slechts een enkele configuratie van Hisense AEH-W4A1 is mogelijk." + }, + "step": { + "confirm": { + "description": "Wilt u Hisense AEH-W4A1 instellen?", + "title": "Hisense AEH-W4A1" + } + }, + "title": "Hisense AEH-W4A1" + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/manifest.json b/homeassistant/components/hisense_aehw4a1/manifest.json index e4bdf581f9c..da8e3ad9419 100644 --- a/homeassistant/components/hisense_aehw4a1/manifest.json +++ b/homeassistant/components/hisense_aehw4a1/manifest.json @@ -3,11 +3,7 @@ "name": "Hisense AEH-W4A1", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hisense_aehw4a1", - "requirements": [ - "pyaehw4a1==0.3.1" - ], + "requirements": ["pyaehw4a1==0.3.1"], "dependencies": [], - "codeowners": [ - "@bannhead" - ] + "codeowners": ["@bannhead"] } diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 133151c7f73..7fcbf519bf3 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -8,7 +8,7 @@ import time from sqlalchemy import and_, func import voluptuous as vol -from homeassistant.components import recorder, script +from homeassistant.components import recorder from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope @@ -430,4 +430,4 @@ def _is_significant(state): Will only test for things that are not filtered out in SQL. """ # scripts that are not cancellable will never change state - return state.domain != "script" or state.attributes.get(script.ATTR_CAN_CANCEL) + return state.domain != "script" or state.attributes.get("can_cancel") diff --git a/homeassistant/components/history/manifest.json b/homeassistant/components/history/manifest.json index 00789c905c2..47f74ec4fde 100644 --- a/homeassistant/components/history/manifest.json +++ b/homeassistant/components/history/manifest.json @@ -3,11 +3,7 @@ "name": "History", "documentation": "https://www.home-assistant.io/integrations/history", "requirements": [], - "dependencies": [ - "http", - "recorder" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http", "recorder"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/history_graph/__init__.py b/homeassistant/components/history_graph/__init__.py index ad8398c75f5..2b89556818f 100644 --- a/homeassistant/components/history_graph/__init__.py +++ b/homeassistant/components/history_graph/__init__.py @@ -3,8 +3,8 @@ import logging import voluptuous as vol +from homeassistant.const import ATTR_ENTITY_ID, CONF_ENTITIES, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_ENTITIES, CONF_NAME, ATTR_ENTITY_ID from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent diff --git a/homeassistant/components/history_graph/manifest.json b/homeassistant/components/history_graph/manifest.json index a4a0eb4d3e9..e34907d05ce 100644 --- a/homeassistant/components/history_graph/manifest.json +++ b/homeassistant/components/history_graph/manifest.json @@ -1,12 +1,9 @@ { "domain": "history_graph", - "name": "History graph", + "name": "History Graph", "documentation": "https://www.home-assistant.io/integrations/history_graph", "requirements": [], - "dependencies": [ - "history" - ], - "codeowners": [ - "@andrey-git" - ] + "dependencies": ["history"], + "codeowners": ["@andrey-git"], + "quality_scale": "internal" } diff --git a/homeassistant/components/history_stats/manifest.json b/homeassistant/components/history_stats/manifest.json index 55a3449f4d6..e51fa20bb65 100644 --- a/homeassistant/components/history_stats/manifest.json +++ b/homeassistant/components/history_stats/manifest.json @@ -1,10 +1,9 @@ { "domain": "history_stats", - "name": "History stats", + "name": "History Stats", "documentation": "https://www.home-assistant.io/integrations/history_stats", "requirements": [], - "dependencies": [ - "history" - ], - "codeowners": [] + "dependencies": ["history"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 5c59b5f8e97..3eb604b3957 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -5,21 +5,21 @@ import math import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import history -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_NAME, CONF_ENTITY_ID, + CONF_NAME, CONF_STATE, CONF_TYPE, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def exactly_two_period_keys(conf): """Ensure exactly 2 of CONF_PERIOD_KEYS are provided.""" if sum(param in conf for param in CONF_PERIOD_KEYS) != 2: raise vol.Invalid( - "You must provide exactly 2 of the following:" " start, end, duration" + "You must provide exactly 2 of the following: start, end, duration" ) return conf @@ -262,7 +262,7 @@ class HistoryStatsSensor(Entity): ) except ValueError: _LOGGER.error( - "Parsing error: start must be a datetime" "or a timestamp" + "Parsing error: start must be a datetime or a timestamp" ) return @@ -281,7 +281,7 @@ class HistoryStatsSensor(Entity): ) except ValueError: _LOGGER.error( - "Parsing error: end must be a datetime " "or a timestamp" + "Parsing error: end must be a datetime or a timestamp" ) return diff --git a/homeassistant/components/hitron_coda/device_tracker.py b/homeassistant/components/hitron_coda/device_tracker.py index 2f3526d45b6..12b03acbcc5 100644 --- a/homeassistant/components/hitron_coda/device_tracker.py +++ b/homeassistant/components/hitron_coda/device_tracker.py @@ -1,17 +1,17 @@ """Support for the Hitron CODA-4582U, provided by Rogers.""" -import logging from collections import namedtuple +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TYPE, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hitron_coda/manifest.json b/homeassistant/components/hitron_coda/manifest.json index 6b0492881fb..05f82999198 100644 --- a/homeassistant/components/hitron_coda/manifest.json +++ b/homeassistant/components/hitron_coda/manifest.json @@ -1,6 +1,6 @@ { "domain": "hitron_coda", - "name": "Hitron coda", + "name": "Rogers Hitron CODA", "documentation": "https://www.home-assistant.io/integrations/hitron_coda", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/hive/binary_sensor.py b/homeassistant/components/hive/binary_sensor.py index ce7e53b77a5..fa91d6862a2 100644 --- a/homeassistant/components/hive/binary_sensor.py +++ b/homeassistant/components/hive/binary_sensor.py @@ -1,7 +1,7 @@ """Support for the Hive binary sensors.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DOMAIN, DATA_HIVE, HiveEntity +from . import DATA_HIVE, DOMAIN, HiveEntity DEVICETYPE_DEVICE_CLASS = {"motionsensor": "motion", "contactsensor": "opening"} diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index ed13e3019ce..202cea7bf8e 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -1,6 +1,9 @@ """Support for the Hive climate devices.""" from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, @@ -8,14 +11,10 @@ from homeassistant.components.climate.const import ( PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - CURRENT_HVAC_HEAT, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS - -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system HIVE_TO_HASS_STATE = { "SCHEDULE": HVAC_MODE_AUTO, diff --git a/homeassistant/components/hive/light.py b/homeassistant/components/hive/light.py index 41fc286d13b..33175de543d 100644 --- a/homeassistant/components/hive/light.py +++ b/homeassistant/components/hive/light.py @@ -10,7 +10,7 @@ from homeassistant.components.light import ( ) import homeassistant.util.color as color_util -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index e87e3387a62..6572b0dbda2 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -2,12 +2,7 @@ "domain": "hive", "name": "Hive", "documentation": "https://www.home-assistant.io/integrations/hive", - "requirements": [ - "pyhiveapi==0.2.19.3" - ], + "requirements": ["pyhiveapi==0.2.19.3"], "dependencies": [], - "codeowners": [ - "@Rendili", - "@KJonline" - ] -} \ No newline at end of file + "codeowners": ["@Rendili", "@KJonline"] +} diff --git a/homeassistant/components/hive/sensor.py b/homeassistant/components/hive/sensor.py index ccd635015de..360fb61bfbe 100644 --- a/homeassistant/components/hive/sensor.py +++ b/homeassistant/components/hive/sensor.py @@ -2,7 +2,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity -from . import DOMAIN, DATA_HIVE, HiveEntity +from . import DATA_HIVE, DOMAIN, HiveEntity FRIENDLY_NAMES = { "Hub_OnlineStatus": "Hive Hub Status", diff --git a/homeassistant/components/hive/switch.py b/homeassistant/components/hive/switch.py index 1447f5483a4..53e1ec6a069 100644 --- a/homeassistant/components/hive/switch.py +++ b/homeassistant/components/hive/switch.py @@ -1,7 +1,7 @@ """Support for the Hive switches.""" from homeassistant.components.switch import SwitchDevice -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index c60a9ec01d1..d7d98426df5 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -7,7 +7,8 @@ from homeassistant.components.water_heater import ( WaterHeaterDevice, ) from homeassistant.const import TEMP_CELSIUS -from . import DOMAIN, DATA_HIVE, HiveEntity, refresh_system + +from . import DATA_HIVE, DOMAIN, HiveEntity, refresh_system SUPPORT_FLAGS_HEATER = SUPPORT_OPERATION_MODE diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 037df20b35d..741c81b367c 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -1,10 +1,8 @@ { "domain": "hlk_sw16", - "name": "Hlk sw16", + "name": "Hi-Link HLK-SW16", "documentation": "https://www.home-assistant.io/integrations/hlk_sw16", - "requirements": [ - "hlk-sw16==0.0.7" - ], + "requirements": ["hlk-sw16==0.0.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index d2d6abdadb5..8aa1d7e020a 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -6,23 +6,22 @@ from typing import Awaitable import voluptuous as vol -import homeassistant.core as ha import homeassistant.config as conf_util -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.service import async_extract_entity_ids -from homeassistant.helpers import intent from homeassistant.const import ( ATTR_ENTITY_ID, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, - SERVICE_HOMEASSISTANT_STOP, - SERVICE_HOMEASSISTANT_RESTART, - RESTART_EXIT_CODE, ATTR_LATITUDE, ATTR_LONGITUDE, + RESTART_EXIT_CODE, + SERVICE_HOMEASSISTANT_RESTART, + SERVICE_HOMEASSISTANT_STOP, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, ) -from homeassistant.helpers import config_validation as cv +import homeassistant.core as ha +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, intent +from homeassistant.helpers.service import async_extract_entity_ids _LOGGER = logging.getLogger(__name__) DOMAIN = ha.DOMAIN diff --git a/homeassistant/components/homeassistant/manifest.json b/homeassistant/components/homeassistant/manifest.json index b4c03047a7a..50b771611d3 100644 --- a/homeassistant/components/homeassistant/manifest.json +++ b/homeassistant/components/homeassistant/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/homeassistant", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 576bf540e00..c79c22e36a3 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -4,6 +4,8 @@ import logging import voluptuous as vol +from homeassistant import config as conf_util +from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, @@ -11,21 +13,19 @@ from homeassistant.const import ( CONF_ID, CONF_NAME, CONF_PLATFORM, + SERVICE_RELOAD, STATE_OFF, STATE_ON, - SERVICE_RELOAD, ) -from homeassistant.core import State, DOMAIN as HA_DOMAIN -from homeassistant import config as conf_util +from homeassistant.core import DOMAIN as HA_DOMAIN, State from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import async_get_integration from homeassistant.helpers import ( config_per_platform, config_validation as cv, entity_platform, ) from homeassistant.helpers.state import async_reproduce_state -from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene +from homeassistant.loader import async_get_integration def _convert_states(states): @@ -109,7 +109,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up home assistant scene entries.""" + """Set up Home Assistant scene entries.""" _process_scenes_config(hass, async_add_entities, config) # This platform can be loaded multiple times. Only first time register the service. diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 91d0719e0e7..ca5a601068a 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -45,8 +45,8 @@ from .const import ( DEVICE_CLASS_PM25, DOMAIN, HOMEKIT_FILE, - SERVICE_HOMEKIT_START, SERVICE_HOMEKIT_RESET_ACCESSORY, + SERVICE_HOMEKIT_START, TYPE_FAUCET, TYPE_OUTLET, TYPE_SHOWER, @@ -54,7 +54,6 @@ from .const import ( TYPE_SWITCH, TYPE_VALVE, ) - from .util import ( show_setup_message, validate_entity_config, @@ -329,7 +328,7 @@ class HomeKit: aid = generate_aid(entity_id) if aid not in self.bridge.accessories: _LOGGER.warning( - "Could not reset accessory. entity_id " "not found %s", entity_id + "Could not reset accessory. entity_id not found %s", entity_id ) continue acc = self.remove_bridge_accessory(aid) @@ -363,7 +362,7 @@ class HomeKit: return self.status = STATUS_WAIT - from . import ( # noqa: F401 pylint: disable=unused-import + from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel type_covers, type_fans, type_lights, diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index c0ab61d8568..69e4554d81b 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -1,10 +1,8 @@ { "domain": "homekit", - "name": "Homekit", + "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": [ - "HAP-python==2.6.0" - ], + "requirements": ["HAP-python==2.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 3a33207e70e..807941c7a6d 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -17,7 +17,9 @@ from homeassistant.const import ( SERVICE_SET_COVER_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, ) from . import TYPES @@ -101,6 +103,9 @@ class WindowCovering(HomeAccessory): self.char_target_position = serv_cover.configure_char( CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover ) + self.char_position_state = serv_cover.configure_char( + CHAR_POSITION_STATE, value=2 + ) @debounce def move_cover(self, value): @@ -122,6 +127,12 @@ class WindowCovering(HomeAccessory): ): self.char_target_position.set_value(current_position) self._homekit_target = None + if new_state.state == STATE_OPENING: + self.char_position_state.set_value(1) + elif new_state.state == STATE_CLOSING: + self.char_position_state.set_value(0) + else: + self.char_position_state.set_value(2) @TYPES.register("WindowCoveringBasic") @@ -175,7 +186,6 @@ class WindowCoveringBasic(HomeAccessory): # Snap the current/target position to the expected final position. self.char_current_position.set_value(position) self.char_target_position.set_value(position) - self.char_position_state.set_value(2) def update_state(self, new_state): """Update cover position after state changed.""" @@ -184,4 +194,9 @@ class WindowCoveringBasic(HomeAccessory): if hk_position is not None: self.char_current_position.set_value(hk_position) self.char_target_position.set_value(hk_position) + if new_state.state == STATE_OPENING: + self.char_position_state.set_value(1) + elif new_state.state == STATE_CLOSING: + self.char_position_state.set_value(0) + else: self.char_position_state.set_value(2) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 7f195b276d6..3fc6a0628ff 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -66,15 +66,20 @@ class Light(HomeAccessory): self._features = self.hass.states.get(self.entity_id).attributes.get( ATTR_SUPPORTED_FEATURES ) + if self._features & SUPPORT_BRIGHTNESS: self.chars.append(CHAR_BRIGHTNESS) - if self._features & SUPPORT_COLOR_TEMP: - self.chars.append(CHAR_COLOR_TEMPERATURE) + if self._features & SUPPORT_COLOR: self.chars.append(CHAR_HUE) self.chars.append(CHAR_SATURATION) self._hue = None self._saturation = None + elif self._features & SUPPORT_COLOR_TEMP: + # ColorTemperature and Hue characteristic should not be + # exposed both. Both states are tracked separately in HomeKit, + # causing "source of truth" problems. + self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) self.char_on = serv_light.configure_char( @@ -88,6 +93,7 @@ class Light(HomeAccessory): self.char_brightness = serv_light.configure_char( CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness ) + if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( ATTR_MIN_MIREDS, 153 @@ -101,10 +107,12 @@ class Light(HomeAccessory): properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, setter_callback=self.set_color_temperature, ) + if CHAR_HUE in self.chars: self.char_hue = serv_light.configure_char( CHAR_HUE, value=0, setter_callback=self.set_hue ) + if CHAR_SATURATION in self.chars: self.char_saturation = serv_light.configure_char( CHAR_SATURATION, value=75, setter_callback=self.set_saturation diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index d9b24782610..3c5dce4fa7a 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -6,16 +6,16 @@ from pyhap.const import CATEGORY_SWITCH, CATEGORY_TELEVISION from homeassistant.components.media_player import ( ATTR_INPUT_SOURCE, ATTR_INPUT_SOURCE_LIST, - ATTR_MEDIA_VOLUME_MUTED, ATTR_MEDIA_VOLUME_LEVEL, - SERVICE_SELECT_SOURCE, + ATTR_MEDIA_VOLUME_MUTED, DOMAIN, + SERVICE_SELECT_SOURCE, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SELECT_SOURCE, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -26,13 +26,13 @@ from homeassistant.const import ( SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, STATE_OFF, - STATE_PLAYING, STATE_PAUSED, + STATE_PLAYING, STATE_UNKNOWN, ) @@ -46,23 +46,23 @@ from .const import ( CHAR_IDENTIFIER, CHAR_INPUT_SOURCE_TYPE, CHAR_IS_CONFIGURED, - CHAR_NAME, - CHAR_SLEEP_DISCOVER_MODE, CHAR_MUTE, + CHAR_NAME, CHAR_ON, CHAR_REMOTE_KEY, + CHAR_SLEEP_DISCOVER_MODE, + CHAR_VOLUME, CHAR_VOLUME_CONTROL_TYPE, CHAR_VOLUME_SELECTOR, - CHAR_VOLUME, CONF_FEATURE_LIST, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, FEATURE_PLAY_STOP, FEATURE_TOGGLE_MUTE, + SERV_INPUT_SOURCE, SERV_SWITCH, SERV_TELEVISION, SERV_TELEVISION_SPEAKER, - SERV_INPUT_SOURCE, ) _LOGGER = logging.getLogger(__name__) @@ -422,7 +422,7 @@ class TelevisionMediaPlayer(HomeAccessory): self.char_input_source.set_value(index) else: _LOGGER.warning( - "%s: Sources out of sync. " "Restart HomeAssistant", + "%s: Sources out of sync. Restart Home Assistant", self.entity_id, ) self.char_input_source.set_value(0) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index b6e1e75d3c6..79a9d156f10 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -20,12 +20,12 @@ from homeassistant.components.climate.const import ( DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, + HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - HVAC_MODE_AUTO, - HVAC_MODE_FAN_ONLY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, SUPPORT_TARGET_TEMPERATURE_RANGE, diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 608c9a974e5..0fe97cfca63 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -103,7 +103,7 @@ def validate_entity_config(values): if not isinstance(config, dict): raise vol.Invalid( - "The configuration for {} must be " " a dictionary.".format(entity) + "The configuration for {} must be a dictionary.".format(entity) ) if domain in ("alarm_control_panel", "lock"): diff --git a/homeassistant/components/homekit_controller/.translations/da.json b/homeassistant/components/homekit_controller/.translations/da.json index 2bcda4fb1ad..20b209752eb 100644 --- a/homeassistant/components/homekit_controller/.translations/da.json +++ b/homeassistant/components/homekit_controller/.translations/da.json @@ -3,38 +3,38 @@ "abort": { "accessory_not_found_error": "Parring kan ikke tilf\u00f8jes da enheden ikke l\u00e6ngere findes.", "already_configured": "Tilbeh\u00f8ret er allerede konfigureret med denne controller.", - "already_in_progress": "Enheds konfiguration er allerede i gang.", + "already_in_progress": "Enhedskonfiguration er allerede i gang.", "already_paired": "Dette tilbeh\u00f8r er allerede parret med en anden enhed. Nulstil tilbeh\u00f8ret og pr\u00f8v igen.", - "ignored_model": "HomeKit underst\u00f8ttelse til denne model er blokeret da en mere komplet native integration er til r\u00e5dighed.", + "ignored_model": "HomeKit-underst\u00f8ttelse af denne model er blokeret, da en mere funktionskomplet indbygget integration er tilg\u00e6ngelig.", "invalid_config_entry": "Denne enhed vises som klar til parring, men der er allerede en modstridende konfigurationspost for den i Home Assistant, som f\u00f8rst skal fjernes.", "no_devices": "Der blev ikke fundet nogen uparrede enheder" }, "error": { - "authentication_error": "Forkert HomeKit kode. Kontroller den og pr\u00f8v igen.", - "busy_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den allerede parrer med en anden controller.", - "max_peers_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den ikke har nok frit parrings lager.", - "max_tries_error": "Enheden n\u00e6gtede at tilf\u00f8je parring da den har modtaget mere end 100 mislykkede godkendelsesfors\u00f8g.", + "authentication_error": "Forkert HomeKit-kode. Kontroller den og pr\u00f8v igen.", + "busy_error": "Enheden n\u00e6gtede at parre da den allerede er parret med en anden controller.", + "max_peers_error": "Enheden n\u00e6gtede at parre da den ikke har nok frit parringslagerplads.", + "max_tries_error": "Enheden n\u00e6gtede at parre da den har modtaget mere end 100 mislykkede godkendelsesfors\u00f8g.", "pairing_failed": "En uh\u00e5ndteret fejl opstod under fors\u00f8g p\u00e5 at parre med denne enhed. Dette kan v\u00e6re en midlertidig fejl eller din enhed muligvis ikke underst\u00f8ttes i \u00f8jeblikket.", "unable_to_pair": "Kunne ikke parre, pr\u00f8v venligst igen.", "unknown_error": "Enhed rapporterede en ukendt fejl. Parring mislykkedes." }, - "flow_title": "HomeKit tilbeh\u00f8r: {name}", + "flow_title": "HomeKit-tilbeh\u00f8r: {name}", "step": { "pair": { "data": { "pairing_code": "Parringskode" }, - "description": "Indtast din HomeKit parringskode (i formatet XXX-XX-XXX) for at bruge dette tilbeh\u00f8r", - "title": "Par med HomeKit tilbeh\u00f8r" + "description": "Indtast din HomeKit-parringskode (i formatet XXX-XX-XXX) for at bruge dette tilbeh\u00f8r", + "title": "Par med HomeKit-tilbeh\u00f8r" }, "user": { "data": { "device": "Enhed" }, "description": "V\u00e6lg den enhed du vil parre med", - "title": "Par med HomeKit tilbeh\u00f8r" + "title": "Par med HomeKit-tilbeh\u00f8r" } }, - "title": "HomeKit tilbeh\u00f8r" + "title": "HomeKit-tilbeh\u00f8r" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/ru.json b/homeassistant/components/homekit_controller/.translations/ru.json index 44a57a1eb25..41393acb26b 100644 --- a/homeassistant/components/homekit_controller/.translations/ru.json +++ b/homeassistant/components/homekit_controller/.translations/ru.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043e.", "already_configured": "\u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u044d\u0442\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "already_paired": "\u042d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0431\u0440\u043e\u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "ignored_model": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 HomeKit \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u043b\u043d\u0430\u044f \u043d\u0430\u0442\u0438\u0432\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.", "invalid_config_entry": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0433\u043e\u0442\u043e\u0432\u043e\u0435 \u043a \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044e, \u043d\u043e \u0432 Home Assistant \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0443\u044e\u0449\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u0435\u0433\u043e, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c.", diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 6b53301e877..dc65796a569 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -5,15 +5,13 @@ import homekit from homekit.model.characteristics import CharacteristicsTypes from homeassistant.core import callback -from homeassistant.helpers.entity import Entity from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import Entity -# We need an import from .config_flow, without it .config_flow is never loaded. -from .config_flow import HomekitControllerFlowHandler # noqa: F401 -from .connection import get_accessory_information, HKDevice -from .const import CONTROLLER, ENTITY_MAP, KNOWN_DEVICES -from .const import DOMAIN +from .config_flow import normalize_hkid +from .connection import HKDevice, get_accessory_information +from .const import CONTROLLER, DOMAIN, ENTITY_MAP, KNOWN_DEVICES from .storage import EntityMapStorage _LOGGER = logging.getLogger(__name__) @@ -111,6 +109,16 @@ class HomeKitEntity(Entity): return setup_fn(char) + def get_hk_char_value(self, characteristic_type): + """Return the value for a given characteristic type enum.""" + state = self._accessory.current_state.get(self._aid) + if not state: + return None + char = self._chars.get(CharacteristicsTypes.get_short(characteristic_type)) + if not char: + return None + return state.get(char, {}).get("value") + @callback def async_state_changed(self): """Collect new data from bridge and update the entity state in hass.""" @@ -182,6 +190,12 @@ async def async_setup_entry(hass, entry): conn = HKDevice(hass, entry, entry.data) hass.data[KNOWN_DEVICES][conn.unique_id] = conn + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=normalize_hkid(conn.unique_id) + ) + if not await conn.async_setup(): del hass.data[KNOWN_DEVICES][conn.unique_id] raise ConfigEntryNotReady diff --git a/homeassistant/components/homekit_controller/air_quality.py b/homeassistant/components/homekit_controller/air_quality.py new file mode 100644 index 00000000000..854c12e6f88 --- /dev/null +++ b/homeassistant/components/homekit_controller/air_quality.py @@ -0,0 +1,98 @@ +"""Support for HomeKit Controller air quality sensors.""" +from homekit.model.characteristics import CharacteristicsTypes + +from homeassistant.components.air_quality import AirQualityEntity + +from . import KNOWN_DEVICES, HomeKitEntity + +AIR_QUALITY_TEXT = { + 0: "unknown", + 1: "excellent", + 2: "good", + 3: "fair", + 4: "inferior", + 5: "poor", +} + + +class HomeAirQualitySensor(HomeKitEntity, AirQualityEntity): + """Representation of a HomeKit Controller Air Quality sensor.""" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.AIR_QUALITY, + CharacteristicsTypes.DENSITY_PM25, + CharacteristicsTypes.DENSITY_PM10, + CharacteristicsTypes.DENSITY_OZONE, + CharacteristicsTypes.DENSITY_NO2, + CharacteristicsTypes.DENSITY_SO2, + CharacteristicsTypes.DENSITY_VOC, + ] + + @property + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self.get_hk_char_value(CharacteristicsTypes.DENSITY_PM25) + + @property + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self.get_hk_char_value(CharacteristicsTypes.DENSITY_PM10) + + @property + def ozone(self): + """Return the O3 (ozone) level.""" + return self.get_hk_char_value(CharacteristicsTypes.DENSITY_OZONE) + + @property + def sulphur_dioxide(self): + """Return the SO2 (sulphur dioxide) level.""" + return self.get_hk_char_value(CharacteristicsTypes.DENSITY_SO2) + + @property + def nitrogen_dioxide(self): + """Return the NO2 (nitrogen dioxide) level.""" + return self.get_hk_char_value(CharacteristicsTypes.DENSITY_NO2) + + @property + def air_quality_text(self): + """Return the Air Quality Index (AQI).""" + air_quality = self.get_hk_char_value(CharacteristicsTypes.AIR_QUALITY) + return AIR_QUALITY_TEXT.get(air_quality, "unknown") + + @property + def volatile_organic_compounds(self): + """Return the volatile organic compounds (VOC) level.""" + return self.get_hk_char_value(CharacteristicsTypes.DENSITY_VOC) + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + data = {"air_quality_text": self.air_quality_text} + + voc = self.volatile_organic_compounds + if voc: + data["volatile_organic_compounds"] = voc + + return data + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit air quality sensor.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + if service["stype"] != "air-quality": + return False + info = {"aid": aid, "iid": service["iid"]} + async_add_entities([HomeAirQualitySensor(conn, info)], True) + return True + + conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 1e1c8ef5d44..2998ce18641 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -3,7 +3,10 @@ import logging from homekit.model.characteristics import CharacteristicsTypes -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_SMOKE, + BinarySensorDevice, +) from . import KNOWN_DEVICES, HomeKitEntity @@ -57,7 +60,37 @@ class HomeKitContactSensor(HomeKitEntity, BinarySensorDevice): return self._state == 1 -ENTITY_TYPES = {"motion": HomeKitMotionSensor, "contact": HomeKitContactSensor} +class HomeKitSmokeSensor(HomeKitEntity, BinarySensorDevice): + """Representation of a Homekit smoke sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_SMOKE + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.SMOKE_DETECTED] + + def _update_smoke_detected(self, value): + self._state = value + + @property + def is_on(self): + """Return true if smoke is currently detected.""" + return self._state == 1 + + +ENTITY_TYPES = { + "motion": HomeKitMotionSensor, + "contact": HomeKitContactSensor, + "smoke": HomeKitSmokeSensor, +} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 1f9118ff838..d0ab7bd2e99 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -4,20 +4,20 @@ import logging from homekit.model.characteristics import CharacteristicsTypes from homeassistant.components.climate import ( - ClimateDevice, - DEFAULT_MIN_HUMIDITY, DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + ClimateDevice, ) from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 40bf87d6f0a..507a5cbb70a 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -1,22 +1,25 @@ """Config flow to configure homekit_controller.""" -import os import json import logging +import os +import re import homekit +from homekit.controller.ip_implementation import IpPairing import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback +from .connection import get_accessory_name, get_bridge_information from .const import DOMAIN, KNOWN_DEVICES -from .connection import get_bridge_information, get_accessory_name - HOMEKIT_IGNORE = ["Home Assistant Bridge"] HOMEKIT_DIR = ".homekit" PAIRING_FILE = "pairing.json" +PIN_FORMAT = re.compile(r"^(\d{3})-{0,1}(\d{2})-{0,1}(\d{3})$") + _LOGGER = logging.getLogger(__name__) @@ -46,6 +49,11 @@ def load_old_pairings(hass): return old_pairings +def normalize_hkid(hkid): + """Normalize a hkid so that it is safe to compare with other normalized hkids.""" + return hkid.lower() + + @callback def find_existing_host(hass, serial): """Return a set of the configured hosts.""" @@ -54,6 +62,20 @@ def find_existing_host(hass, serial): return entry +def ensure_pin_format(pin): + """ + Ensure a pin code is correctly formatted. + + Ensures a pin code is in the format 111-11-111. Handles codes with and without dashes. + + If incorrect code is entered, an exception is raised. + """ + match = PIN_FORMAT.search(pin) + if not match: + raise homekit.exceptions.MalformedPinError(f"Invalid PIN code f{pin}") + return "{}-{}-{}".format(*match.groups()) + + @config_entries.HANDLERS.register(DOMAIN) class HomekitControllerFlowHandler(config_entries.ConfigFlow): """Handle a HomeKit config flow.""" @@ -77,6 +99,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): key = user_input["device"] self.hkid = self.devices[key]["id"] self.model = self.devices[key]["md"] + await self.async_set_unique_id( + normalize_hkid(self.hkid), raise_on_progress=False + ) return await self.async_step_pair() all_hosts = await self.hass.async_add_executor_job(self.controller.discover, 5) @@ -100,6 +125,38 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): ), ) + async def async_step_unignore(self, user_input): + """Rediscover a previously ignored discover.""" + unique_id = user_input["unique_id"] + await self.async_set_unique_id(unique_id) + + records = await self.hass.async_add_executor_job(self.controller.discover, 5) + for record in records: + if normalize_hkid(record["id"]) != unique_id: + continue + return await self.async_step_zeroconf( + { + "host": record["address"], + "port": record["port"], + "hostname": record["name"], + "type": "_hap._tcp.local.", + "name": record["name"], + "properties": { + "md": record["md"], + "pv": record["pv"], + "id": unique_id, + "c#": record["c#"], + "s#": record["s#"], + "ff": record["ff"], + "ci": record["ci"], + "sf": record["sf"], + "sh": "", + }, + } + ) + + return self.async_abort(reason="no_devices") + async def async_step_zeroconf(self, discovery_info): """Handle a discovered HomeKit accessory. @@ -120,18 +177,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): status_flags = int(properties["sf"]) paired = not status_flags & 0x01 - _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) - - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context["hkid"] = hkid - self.context["title_placeholders"] = {"name": name} - - # If multiple HomekitControllerFlowHandler end up getting created - # for the same accessory dont let duplicates hang around - active_flows = self._async_in_progress() - if any(hkid == flow["context"]["hkid"] for flow in active_flows): - return self.async_abort(reason="already_in_progress") - # The configuration number increases every time the characteristic map # needs updating. Some devices use a slightly off-spec name so handle # both cases. @@ -143,21 +188,27 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): ) config_num = None - if paired: - if hkid in self.hass.data.get(KNOWN_DEVICES, {}): - # The device is already paired and known to us - # According to spec we should monitor c# (config_num) for - # changes. If it changes, we check for new entities - conn = self.hass.data[KNOWN_DEVICES][hkid] - if conn.config_num != config_num: - _LOGGER.debug( - "HomeKit info %s: c# incremented, refreshing entities", hkid - ) - self.hass.async_create_task( - conn.async_refresh_entity_map(config_num) - ) - return self.async_abort(reason="already_configured") + # If the device is already paired and known to us we should monitor c# + # (config_num) for changes. If it changes, we check for new entities + if paired and hkid in self.hass.data.get(KNOWN_DEVICES, {}): + conn = self.hass.data[KNOWN_DEVICES][hkid] + if conn.config_num != config_num: + _LOGGER.debug( + "HomeKit info %s: c# incremented, refreshing entities", hkid + ) + self.hass.async_create_task(conn.async_refresh_entity_map(config_num)) + return self.async_abort(reason="already_configured") + _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) + + await self.async_set_unique_id(normalize_hkid(hkid)) + self._abort_if_unique_id_configured() + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["hkid"] = hkid + self.context["title_placeholders"] = {"name": name} + + if paired: old_pairings = await self.hass.async_add_executor_job( load_old_pairings, self.hass ) @@ -194,7 +245,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): async def async_import_legacy_pairing(self, discovery_props, pairing_data): """Migrate a legacy pairing to config entries.""" - from homekit.controller.ip_implementation import IpPairing hkid = discovery_props["id"] @@ -244,6 +294,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): if pair_info: code = pair_info["pairing_code"] try: + code = ensure_pin_format(code) + await self.hass.async_add_executor_job(self.finish_pairing, code) pairing = self.controller.pairings.get(self.hkid) @@ -251,6 +303,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): return await self._entry_from_accessory(pairing) errors["pairing_code"] = "unable_to_pair" + except homekit.exceptions.MalformedPinError: + # Library claimed pin was invalid before even making an API call + errors["pairing_code"] = "authentication_error" except homekit.AuthenticationError: # PairSetup M4 - SRP proof failed # PairSetup M6 - Ed25519 signature verification failed diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 1cb2131fb8f..f3e728c6cdc 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -3,18 +3,18 @@ import asyncio import datetime import logging +from homekit.controller.ip_implementation import IpPairing from homekit.exceptions import ( AccessoryDisconnectedError, AccessoryNotFoundError, EncryptionError, ) -from homekit.model.services import ServicesTypes from homekit.model.characteristics import CharacteristicsTypes +from homekit.model.services import ServicesTypes from homeassistant.helpers.event import async_track_time_interval -from .const import DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP - +from .const import DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) RETRY_INTERVAL = 60 # seconds @@ -57,7 +57,6 @@ class HKDevice: def __init__(self, hass, config_entry, pairing_data): """Initialise a generic HomeKit device.""" - from homekit.controller.ip_implementation import IpPairing self.hass = hass self.config_entry = config_entry @@ -123,7 +122,7 @@ class HKDevice: self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) async def async_setup(self): - """Prepare to use a paired HomeKit device in homeassistant.""" + """Prepare to use a paired HomeKit device in Home Assistant.""" cache = self.hass.data[ENTITY_MAP].get_map(self.unique_id) if not cache: if await self.async_refresh_entity_map(self.config_num): @@ -136,20 +135,33 @@ class HKDevice: self.accessories = cache["accessories"] self.config_num = cache["config_num"] - # Ensure the Pairing object has access to the latest version of the - # entity map. + self._polling_interval_remover = async_track_time_interval( + self.hass, self.async_update, DEFAULT_SCAN_INTERVAL + ) + + self.hass.async_create_task(self.async_process_entity_map()) + + return True + + async def async_process_entity_map(self): + """ + Process the entity map and load any platforms or entities that need adding. + + This is idempotent and will be called at startup and when we detect metadata changes + via the c# counter on the zeroconf record. + """ + # Ensure the Pairing object has access to the latest version of the entity map. This + # is especially important for BLE, as the Pairing instance relies on the entity map + # to map aid/iid to GATT characteristics. So push it to there as well. + self.pairing.pairing_data["accessories"] = self.accessories - self.async_load_platforms() + await self.async_load_platforms() self.add_entities() await self.async_update() - self._polling_interval_remover = async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL - ) - return True async def async_unload(self): @@ -179,24 +191,14 @@ class HKDevice: except AccessoryDisconnectedError: # If we fail to refresh this data then we will naturally retry # later when Bonjour spots c# is still not up to date. - return + return False self.hass.data[ENTITY_MAP].async_create_or_update_map( self.unique_id, config_num, self.accessories ) self.config_num = config_num - - # For BLE, the Pairing instance relies on the entity map to map - # aid/iid to GATT characteristics. So push it to there as well. - self.pairing.pairing_data["accessories"] = self.accessories - - self.async_load_platforms() - - # Register and add new entities that are available - self.add_entities() - - await self.async_update() + self.hass.async_create_task(self.async_process_entity_map()) return True @@ -226,7 +228,7 @@ class HKDevice: self.entities.append((aid, iid)) break - def async_load_platforms(self): + async def async_load_platforms(self): """Load any platforms needed by this HomeKit device.""" for accessory in self.accessories: for service in accessory["services"]: @@ -238,12 +240,14 @@ class HKDevice: if platform in self.platforms: continue - self.hass.async_create_task( - self.hass.config_entries.async_forward_entry_setup( + self.platforms.add(platform) + try: + await self.hass.config_entries.async_forward_entry_setup( self.config_entry, platform ) - ) - self.platforms.add(platform) + except Exception: + self.platforms.remove(platform) + raise async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 09a7df2a2bf..684f83ba5d4 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -26,4 +26,8 @@ HOMEKIT_ACCESSORY_DISPATCH = { "light": "sensor", "temperature": "sensor", "battery": "sensor", + "smoke": "binary_sensor", + "fan": "fan", + "fanv2": "fan", + "air-quality": "air_quality", } diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 0606778acb5..7e5591d9505 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -11,8 +11,8 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, - SUPPORT_STOP, SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, CoverDevice, ) from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py new file mode 100644 index 00000000000..efb41808429 --- /dev/null +++ b/homeassistant/components/homekit_controller/fan.py @@ -0,0 +1,251 @@ +"""Support for Homekit fans.""" +import logging + +from homekit.model.characteristics import CharacteristicsTypes + +from homeassistant.components.fan import ( + DIRECTION_FORWARD, + DIRECTION_REVERSE, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, +) + +from . import KNOWN_DEVICES, HomeKitEntity + +_LOGGER = logging.getLogger(__name__) + +# 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that +# its consistent with homeassistant.components.homekit. +DIRECTION_TO_HK = { + DIRECTION_REVERSE: 1, + DIRECTION_FORWARD: 0, +} +HK_DIRECTION_TO_HA = {v: k for (k, v) in DIRECTION_TO_HK.items()} + +SPEED_TO_PCNT = { + SPEED_HIGH: 100, + SPEED_MEDIUM: 50, + SPEED_LOW: 25, + SPEED_OFF: 0, +} + + +class BaseHomeKitFan(HomeKitEntity, FanEntity): + """Representation of a Homekit fan.""" + + # This must be set in subclasses to the name of a boolean characteristic + # that controls whether the fan is on or off. + on_characteristic = None + + def __init__(self, *args): + """Initialise the fan.""" + self._on = None + self._features = 0 + self._rotation_direction = 0 + self._rotation_speed = 0 + self._swing_mode = 0 + + super().__init__(*args) + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.SWING_MODE, + CharacteristicsTypes.ROTATION_DIRECTION, + CharacteristicsTypes.ROTATION_SPEED, + ] + + def _setup_rotation_direction(self, char): + self._features |= SUPPORT_DIRECTION + + def _setup_rotation_speed(self, char): + self._features |= SUPPORT_SET_SPEED + + def _setup_swing_mode(self, char): + self._features |= SUPPORT_OSCILLATE + + def _update_rotation_direction(self, value): + self._rotation_direction = value + + def _update_rotation_speed(self, value): + self._rotation_speed = value + + def _update_swing_mode(self, value): + self._swing_mode = value + + @property + def is_on(self): + """Return true if device is on.""" + return self._on + + @property + def speed(self): + """Return the current speed.""" + if not self.is_on: + return SPEED_OFF + if self._rotation_speed > SPEED_TO_PCNT[SPEED_MEDIUM]: + return SPEED_HIGH + if self._rotation_speed > SPEED_TO_PCNT[SPEED_LOW]: + return SPEED_MEDIUM + if self._rotation_speed > SPEED_TO_PCNT[SPEED_OFF]: + return SPEED_LOW + return SPEED_OFF + + @property + def speed_list(self): + """Get the list of available speeds.""" + if self.supported_features & SUPPORT_SET_SPEED: + return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + return [] + + @property + def current_direction(self): + """Return the current direction of the fan.""" + return HK_DIRECTION_TO_HA[self._rotation_direction] + + @property + def oscillating(self): + """Return whether or not the fan is currently oscillating.""" + return self._swing_mode == 1 + + @property + def supported_features(self): + """Flag supported features.""" + return self._features + + async def async_set_direction(self, direction): + """Set the direction of the fan.""" + await self._accessory.put_characteristics( + [ + { + "aid": self._aid, + "iid": self._chars["rotation.direction"], + "value": DIRECTION_TO_HK[direction], + } + ] + ) + + async def async_set_speed(self, speed): + """Set the speed of the fan.""" + if speed == SPEED_OFF: + return await self.async_turn_off() + + await self._accessory.put_characteristics( + [ + { + "aid": self._aid, + "iid": self._chars["rotation.speed"], + "value": SPEED_TO_PCNT[speed], + } + ] + ) + + async def async_oscillate(self, oscillating: bool): + """Oscillate the fan.""" + await self._accessory.put_characteristics( + [ + { + "aid": self._aid, + "iid": self._chars["swing-mode"], + "value": 1 if oscillating else 0, + } + ] + ) + + async def async_turn_on(self, speed=None, **kwargs): + """Turn the specified fan on.""" + + characteristics = [] + + if not self.is_on: + characteristics.append( + { + "aid": self._aid, + "iid": self._chars[self.on_characteristic], + "value": True, + } + ) + + if self.supported_features & SUPPORT_SET_SPEED and speed: + characteristics.append( + { + "aid": self._aid, + "iid": self._chars["rotation.speed"], + "value": SPEED_TO_PCNT[speed], + }, + ) + + if not characteristics: + return + + await self._accessory.put_characteristics(characteristics) + + async def async_turn_off(self, **kwargs): + """Turn the specified fan off.""" + characteristics = [ + { + "aid": self._aid, + "iid": self._chars[self.on_characteristic], + "value": False, + } + ] + await self._accessory.put_characteristics(characteristics) + + +class HomeKitFanV1(BaseHomeKitFan): + """Implement fan support for public.hap.service.fan.""" + + on_characteristic = "on" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [CharacteristicsTypes.ON] + super().get_characteristic_types() + + def _update_on(self, value): + self._on = value == 1 + + +class HomeKitFanV2(BaseHomeKitFan): + """Implement fan support for public.hap.service.fanv2.""" + + on_characteristic = "active" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [CharacteristicsTypes.ACTIVE] + super().get_characteristic_types() + + def _update_active(self, value): + self._on = value == 1 + + +ENTITY_TYPES = { + "fan": HomeKitFanV1, + "fanv2": HomeKitFanV2, +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Legacy set up platform.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit fans.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + def async_add_service(aid, service): + entity_class = ENTITY_TYPES.get(service["stype"]) + if not entity_class: + return False + info = {"aid": aid, "iid": service["iid"]} + async_add_entities([entity_class(conn, info)], True) + return True + + conn.add_listener(async_add_service) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 2a660281311..c7eb02a479c 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -1,14 +1,10 @@ { "domain": "homekit_controller", - "name": "Homekit controller", + "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": [ - "homekit[IP]==0.15.0" - ], + "requirements": ["homekit[IP]==0.15.0"], "dependencies": [], "zeroconf": ["_hap._tcp.local."], - "codeowners": [ - "@Jc2k" - ] + "codeowners": ["@Jc2k"] } diff --git a/homeassistant/components/homekit_controller/storage.py b/homeassistant/components/homekit_controller/storage.py index 46d095b5631..ffc2da5fbf2 100644 --- a/homeassistant/components/homekit_controller/storage.py +++ b/homeassistant/components/homekit_controller/storage.py @@ -1,7 +1,7 @@ """Helpers for HomeKit data stored in HA storage.""" -from homeassistant.helpers.storage import Store from homeassistant.core import callback +from homeassistant.helpers.storage import Store from .const import DOMAIN diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 42fb73f6da2..24c9e37a3be 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,8 +1,9 @@ """Support for HomeMatic devices.""" -from datetime import timedelta, datetime +from datetime import datetime from functools import partial import logging +from pyhomematic import HMConnection import voluptuous as vol from homeassistant.const import ( @@ -17,231 +18,68 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_DISCOVER_DEVICES, + ATTR_DISCOVERY_TYPE, + ATTR_ERRORCODE, + ATTR_INTERFACE, + ATTR_LOW_BAT, + ATTR_LOWBAT, + ATTR_MESSAGE, + ATTR_PARAM, + ATTR_PARAMSET, + ATTR_PARAMSET_KEY, + ATTR_TIME, + ATTR_UNIQUE_ID, + ATTR_VALUE, + ATTR_VALUE_TYPE, + CONF_CALLBACK_IP, + CONF_CALLBACK_PORT, + CONF_INTERFACES, + CONF_JSONPORT, + CONF_LOCAL_IP, + CONF_LOCAL_PORT, + CONF_PATH, + CONF_PORT, + CONF_RESOLVENAMES, + CONF_RESOLVENAMES_OPTIONS, + DATA_CONF, + DATA_HOMEMATIC, + DATA_STORE, + DISCOVER_BATTERY, + DISCOVER_BINARY_SENSORS, + DISCOVER_CLIMATE, + DISCOVER_COVER, + DISCOVER_LIGHTS, + DISCOVER_LOCKS, + DISCOVER_SENSORS, + DISCOVER_SWITCHES, + DOMAIN, + EVENT_ERROR, + EVENT_IMPULSE, + EVENT_KEYPRESS, + HM_DEVICE_TYPES, + HM_IGNORE_DISCOVERY_NODE, + HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS, + HM_IMPULSE_EVENTS, + HM_PRESS_EVENTS, + SERVICE_PUT_PARAMSET, + SERVICE_RECONNECT, + SERVICE_SET_DEVICE_VALUE, + SERVICE_SET_INSTALL_MODE, + SERVICE_SET_VARIABLE_VALUE, + SERVICE_VIRTUALKEY, +) +from .entity import HMHub _LOGGER = logging.getLogger(__name__) -DOMAIN = "homematic" - -SCAN_INTERVAL_HUB = timedelta(seconds=300) -SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) - -DISCOVER_SWITCHES = "homematic.switch" -DISCOVER_LIGHTS = "homematic.light" -DISCOVER_SENSORS = "homematic.sensor" -DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" -DISCOVER_COVER = "homematic.cover" -DISCOVER_CLIMATE = "homematic.climate" -DISCOVER_LOCKS = "homematic.locks" -DISCOVER_BATTERY = "homematic.battery" - -ATTR_DISCOVER_DEVICES = "devices" -ATTR_PARAM = "param" -ATTR_CHANNEL = "channel" -ATTR_ADDRESS = "address" -ATTR_VALUE = "value" -ATTR_VALUE_TYPE = "value_type" -ATTR_INTERFACE = "interface" -ATTR_ERRORCODE = "error" -ATTR_MESSAGE = "message" -ATTR_TIME = "time" -ATTR_UNIQUE_ID = "unique_id" -ATTR_PARAMSET_KEY = "paramset_key" -ATTR_PARAMSET = "paramset" -ATTR_DISCOVERY_TYPE = "discovery_type" -ATTR_LOW_BAT = "LOW_BAT" -ATTR_LOWBAT = "LOWBAT" - - -EVENT_KEYPRESS = "homematic.keypress" -EVENT_IMPULSE = "homematic.impulse" -EVENT_ERROR = "homematic.error" - -SERVICE_VIRTUALKEY = "virtualkey" -SERVICE_RECONNECT = "reconnect" -SERVICE_SET_VARIABLE_VALUE = "set_variable_value" -SERVICE_SET_DEVICE_VALUE = "set_device_value" -SERVICE_SET_INSTALL_MODE = "set_install_mode" -SERVICE_PUT_PARAMSET = "put_paramset" - -HM_DEVICE_TYPES = { - DISCOVER_SWITCHES: [ - "Switch", - "SwitchPowermeter", - "IOSwitch", - "IPSwitch", - "RFSiren", - "IPSwitchPowermeter", - "HMWIOSwitch", - "Rain", - "EcoLogic", - "IPKeySwitchPowermeter", - "IPGarage", - "IPKeySwitch", - "IPKeySwitchLevel", - "IPMultiIO", - ], - DISCOVER_LIGHTS: [ - "Dimmer", - "KeyDimmer", - "IPKeyDimmer", - "IPDimmer", - "ColorEffectLight", - "IPKeySwitchLevel", - ], - DISCOVER_SENSORS: [ - "SwitchPowermeter", - "Motion", - "MotionV2", - "RemoteMotion", - "MotionIP", - "ThermostatWall", - "AreaThermostat", - "RotaryHandleSensor", - "WaterSensor", - "PowermeterGas", - "LuxSensor", - "WeatherSensor", - "WeatherStation", - "ThermostatWall2", - "TemperatureDiffSensor", - "TemperatureSensor", - "CO2Sensor", - "IPSwitchPowermeter", - "HMWIOSwitch", - "FillingLevel", - "ValveDrive", - "EcoLogic", - "IPThermostatWall", - "IPSmoke", - "RFSiren", - "PresenceIP", - "IPAreaThermostat", - "IPWeatherSensor", - "RotaryHandleSensorIP", - "IPPassageSensor", - "IPKeySwitchPowermeter", - "IPThermostatWall230V", - "IPWeatherSensorPlus", - "IPWeatherSensorBasic", - "IPBrightnessSensor", - "IPGarage", - "UniversalSensor", - "MotionIPV2", - "IPMultiIO", - "IPThermostatWall2", - ], - DISCOVER_CLIMATE: [ - "Thermostat", - "ThermostatWall", - "MAXThermostat", - "ThermostatWall2", - "MAXWallThermostat", - "IPThermostat", - "IPThermostatWall", - "ThermostatGroup", - "IPThermostatWall230V", - "IPThermostatWall2", - ], - DISCOVER_BINARY_SENSORS: [ - "ShutterContact", - "Smoke", - "SmokeV2", - "Motion", - "MotionV2", - "MotionIP", - "RemoteMotion", - "WeatherSensor", - "TiltSensor", - "IPShutterContact", - "HMWIOSwitch", - "MaxShutterContact", - "Rain", - "WiredSensor", - "PresenceIP", - "IPWeatherSensor", - "IPPassageSensor", - "SmartwareMotion", - "IPWeatherSensorPlus", - "MotionIPV2", - "WaterIP", - "IPMultiIO", - "TiltIP", - "IPShutterContactSabotage", - ], - DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], - DISCOVER_LOCKS: ["KeyMatic"], -} - -HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] - -HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { - "ACTUAL_TEMPERATURE": [ - "IPAreaThermostat", - "IPWeatherSensor", - "IPWeatherSensorPlus", - "IPWeatherSensorBasic", - "IPThermostatWall", - "IPThermostatWall2", - ] -} - -HM_ATTRIBUTE_SUPPORT = { - "LOWBAT": ["battery", {0: "High", 1: "Low"}], - "LOW_BAT": ["battery", {0: "High", 1: "Low"}], - "ERROR": ["error", {0: "No"}], - "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "RSSI_PEER": ["rssi_peer", {}], - "RSSI_DEVICE": ["rssi_device", {}], - "VALVE_STATE": ["valve", {}], - "LEVEL": ["level", {}], - "BATTERY_STATE": ["battery", {}], - "CONTROL_MODE": [ - "mode", - {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, - ], - "POWER": ["power", {}], - "CURRENT": ["current", {}], - "VOLTAGE": ["voltage", {}], - "OPERATING_VOLTAGE": ["voltage", {}], - "WORKING": ["working", {0: "No", 1: "Yes"}], - "STATE_UNCERTAIN": ["state_uncertain", {}], -} - -HM_PRESS_EVENTS = [ - "PRESS_SHORT", - "PRESS_LONG", - "PRESS_CONT", - "PRESS_LONG_RELEASE", - "PRESS", -] - -HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] - -CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] - -DATA_HOMEMATIC = "homematic" -DATA_STORE = "homematic_store" -DATA_CONF = "homematic_conf" - -CONF_INTERFACES = "interfaces" -CONF_LOCAL_IP = "local_ip" -CONF_LOCAL_PORT = "local_port" -CONF_PORT = "port" -CONF_PATH = "path" -CONF_CALLBACK_IP = "callback_ip" -CONF_CALLBACK_PORT = "callback_port" -CONF_RESOLVENAMES = "resolvenames" -CONF_JSONPORT = "jsonport" -CONF_VARIABLES = "variables" -CONF_DEVICES = "devices" -CONF_PRIMARY = "primary" - DEFAULT_LOCAL_IP = "0.0.0.0" DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False @@ -297,6 +135,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_HOSTS, default={}): { cv.match_all: { vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional( CONF_USERNAME, default=DEFAULT_USERNAME ): cv.string, @@ -366,7 +205,6 @@ SCHEMA_SERVICE_PUT_PARAMSET = vol.Schema( def setup(hass, config): """Set up the Homematic component.""" - from pyhomematic import HMConnection conf = config[DOMAIN] hass.data[DATA_CONF] = remotes = {} @@ -392,7 +230,7 @@ def setup(hass, config): for sname, sconfig in conf[CONF_HOSTS].items(): remotes[sname] = { "ip": sconfig.get(CONF_HOST), - "port": DEFAULT_PORT, + "port": sconfig[CONF_PORT], "username": sconfig.get(CONF_USERNAME), "password": sconfig.get(CONF_PASSWORD), "connect": False, @@ -411,7 +249,7 @@ def setup(hass, config): # Start server thread, connect to hosts, initialize to receive events homematic.start() - # Stops server when HASS is shutting down + # Stops server when Home Assistant is shutting down hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, hass.data[DATA_HOMEMATIC].stop) # Init homematic hubs @@ -599,7 +437,7 @@ def _system_callback_handler(hass, config, src, *args): if hmdevice.EVENTNODE: hmdevice.setEventCallback(callback=bound_event_callback, bequeath=True) - # Create HASS entities + # Create Home Assistant entities if addresses: for component_name, discovery_type in ( ("switch", DISCOVER_SWITCHES), @@ -615,7 +453,7 @@ def _system_callback_handler(hass, config, src, *args): found_devices = _get_devices(hass, discovery_type, addresses, interface) # When devices of this type are found - # they are setup in HASS and a discovery event is fired + # they are setup in Home Assistant and a discovery event is fired if found_devices: discovery.load_platform( hass, @@ -774,277 +612,3 @@ def _device_from_servicecall(hass, service): for devices in hass.data[DATA_HOMEMATIC].devices.values(): if address in devices: return devices[address] - - -class HMHub(Entity): - """The HomeMatic hub. (CCU2/HomeGear).""" - - def __init__(self, hass, homematic, name): - """Initialize HomeMatic hub.""" - self.hass = hass - self.entity_id = "{}.{}".format(DOMAIN, name.lower()) - self._homematic = homematic - self._variables = {} - self._name = name - self._state = None - - # Load data - self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) - self.hass.add_job(self._update_hub, None) - - self.hass.helpers.event.track_time_interval( - self._update_variables, SCAN_INTERVAL_VARIABLES - ) - self.hass.add_job(self._update_variables, None) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return false. HomeMatic Hub object updates variables.""" - return False - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def state_attributes(self): - """Return the state attributes.""" - attr = self._variables.copy() - return attr - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:gradient" - - def _update_hub(self, now): - """Retrieve latest state.""" - service_message = self._homematic.getServiceMessages(self._name) - state = None if service_message is None else len(service_message) - - # state have change? - if self._state != state: - self._state = state - self.schedule_update_ha_state() - - def _update_variables(self, now): - """Retrieve all variable data and update hmvariable states.""" - variables = self._homematic.getAllSystemVariables(self._name) - if variables is None: - return - - state_change = False - for key, value in variables.items(): - if key in self._variables and value == self._variables[key]: - continue - - state_change = True - self._variables.update({key: value}) - - if state_change: - self.schedule_update_ha_state() - - def hm_set_variable(self, name, value): - """Set variable value on CCU/Homegear.""" - if name not in self._variables: - _LOGGER.error("Variable %s not found on %s", name, self.name) - return - old_value = self._variables.get(name) - if isinstance(old_value, bool): - value = cv.boolean(value) - else: - value = float(value) - self._homematic.setSystemVariable(self.name, name, value) - - self._variables.update({name: value}) - self.schedule_update_ha_state() - - -class HMDevice(Entity): - """The HomeMatic device base object.""" - - def __init__(self, config): - """Initialize a generic HomeMatic device.""" - self._name = config.get(ATTR_NAME) - self._address = config.get(ATTR_ADDRESS) - self._interface = config.get(ATTR_INTERFACE) - self._channel = config.get(ATTR_CHANNEL) - self._state = config.get(ATTR_PARAM) - self._unique_id = config.get(ATTR_UNIQUE_ID) - self._data = {} - self._homematic = None - self._hmdevice = None - self._connected = False - self._available = False - - # Set parameter to uppercase - if self._state: - self._state = self._state.upper() - - async def async_added_to_hass(self): - """Load data init callbacks.""" - await self.hass.async_add_job(self.link_homematic) - - @property - def unique_id(self): - """Return unique ID. HomeMatic entity IDs are unique by default.""" - return self._unique_id.replace(" ", "_") - - @property - def should_poll(self): - """Return false. HomeMatic states are pushed by the XML-RPC Server.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def available(self): - """Return true if device is available.""" - return self._available - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - attr = {} - - # Generate a dictionary with attributes - for node, data in HM_ATTRIBUTE_SUPPORT.items(): - # Is an attribute and exists for this object - if node in self._data: - value = data[1].get(self._data[node], self._data[node]) - attr[data[0]] = value - - # Static attributes - attr["id"] = self._hmdevice.ADDRESS - attr["interface"] = self._interface - - return attr - - def link_homematic(self): - """Connect to HomeMatic.""" - if self._connected: - return True - - # Initialize - self._homematic = self.hass.data[DATA_HOMEMATIC] - self._hmdevice = self._homematic.devices[self._interface][self._address] - self._connected = True - - try: - # Initialize datapoints of this object - self._init_data() - self._load_data_from_hm() - - # Link events from pyhomematic - self._subscribe_homematic_events() - self._available = not self._hmdevice.UNREACH - except Exception as err: # pylint: disable=broad-except - self._connected = False - _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) - - def _hm_event_callback(self, device, caller, attribute, value): - """Handle all pyhomematic device events.""" - _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) - has_changed = False - - # Is data needed for this instance? - if attribute in self._data: - # Did data change? - if self._data[attribute] != value: - self._data[attribute] = value - has_changed = True - - # Availability has changed - if self.available != (not self._hmdevice.UNREACH): - self._available = not self._hmdevice.UNREACH - has_changed = True - - # If it has changed data point, update HASS - if has_changed: - self.schedule_update_ha_state() - - def _subscribe_homematic_events(self): - """Subscribe all required events to handle job.""" - channels_to_sub = set() - - # Push data to channels_to_sub from hmdevice metadata - for metadata in ( - self._hmdevice.SENSORNODE, - self._hmdevice.BINARYNODE, - self._hmdevice.ATTRIBUTENODE, - self._hmdevice.WRITENODE, - self._hmdevice.EVENTNODE, - self._hmdevice.ACTIONNODE, - ): - for node, channels in metadata.items(): - # Data is needed for this instance - if node in self._data: - # chan is current channel - if len(channels) == 1: - channel = channels[0] - else: - channel = self._channel - - # Prepare for subscription - try: - channels_to_sub.add(int(channel)) - except (ValueError, TypeError): - _LOGGER.error("Invalid channel in metadata from %s", self._name) - - # Set callbacks - for channel in channels_to_sub: - _LOGGER.debug("Subscribe channel %d from %s", channel, self._name) - self._hmdevice.setEventCallback( - callback=self._hm_event_callback, bequeath=False, channel=channel - ) - - def _load_data_from_hm(self): - """Load first value from pyhomematic.""" - if not self._connected: - return False - - # Read data from pyhomematic - for metadata, funct in ( - (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), - (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), - (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), - (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), - ): - for node in metadata: - if metadata[node] and node in self._data: - self._data[node] = funct(name=node, channel=self._channel) - - return True - - def _hm_set_state(self, value): - """Set data to main datapoint.""" - if self._state in self._data: - self._data[self._state] = value - - def _hm_get_state(self): - """Get data from main datapoint.""" - if self._state in self._data: - return self._data[self._state] - return None - - def _init_data(self): - """Generate a data dict (self._data) from the HomeMatic metadata.""" - # Add all attributes to data dictionary - for data_note in self._hmdevice.ATTRIBUTENODE: - self._data.update({data_note: STATE_UNKNOWN}) - - # Initialize device specific data - self._init_data_struct() - - def _init_data_struct(self): - """Generate a data dictionary from the HomeMatic device metadata.""" - raise NotImplementedError diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index cc2907c64fb..731525c8460 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -9,9 +9,9 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, BinarySensorDevice, ) -from homeassistant.components.homematic import ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES, ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -29,6 +29,7 @@ SENSOR_TYPES_CLASS = { "SmokeV2": DEVICE_CLASS_SMOKE, "TiltSensor": None, "WeatherSensor": None, + "IPContact": DEVICE_CLASS_OPENING, } @@ -44,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: devices.append(HMBinarySensor(conf)) - add_entities(devices) + add_entities(devices, True) class HMBinarySensor(HMDevice, BinarySensorDevice): diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 935ebb9b497..b4ab277a75b 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -14,7 +14,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice +from .const import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -44,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMThermostat(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMThermostat(HMDevice, ClimateDevice): diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py new file mode 100644 index 00000000000..cd2d528044a --- /dev/null +++ b/homeassistant/components/homematic/const.py @@ -0,0 +1,212 @@ +"""Constants for the homematic component.""" + +DOMAIN = "homematic" + +DISCOVER_SWITCHES = "homematic.switch" +DISCOVER_LIGHTS = "homematic.light" +DISCOVER_SENSORS = "homematic.sensor" +DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" +DISCOVER_COVER = "homematic.cover" +DISCOVER_CLIMATE = "homematic.climate" +DISCOVER_LOCKS = "homematic.locks" +DISCOVER_BATTERY = "homematic.battery" + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_PARAM = "param" +ATTR_CHANNEL = "channel" +ATTR_ADDRESS = "address" +ATTR_VALUE = "value" +ATTR_VALUE_TYPE = "value_type" +ATTR_INTERFACE = "interface" +ATTR_ERRORCODE = "error" +ATTR_MESSAGE = "message" +ATTR_TIME = "time" +ATTR_UNIQUE_ID = "unique_id" +ATTR_PARAMSET_KEY = "paramset_key" +ATTR_PARAMSET = "paramset" +ATTR_DISCOVERY_TYPE = "discovery_type" +ATTR_LOW_BAT = "LOW_BAT" +ATTR_LOWBAT = "LOWBAT" + +EVENT_KEYPRESS = "homematic.keypress" +EVENT_IMPULSE = "homematic.impulse" +EVENT_ERROR = "homematic.error" + +SERVICE_VIRTUALKEY = "virtualkey" +SERVICE_RECONNECT = "reconnect" +SERVICE_SET_VARIABLE_VALUE = "set_variable_value" +SERVICE_SET_DEVICE_VALUE = "set_device_value" +SERVICE_SET_INSTALL_MODE = "set_install_mode" +SERVICE_PUT_PARAMSET = "put_paramset" + +HM_DEVICE_TYPES = { + DISCOVER_SWITCHES: [ + "Switch", + "SwitchPowermeter", + "IOSwitch", + "IPSwitch", + "RFSiren", + "IPSwitchPowermeter", + "HMWIOSwitch", + "Rain", + "EcoLogic", + "IPKeySwitchPowermeter", + "IPGarage", + "IPKeySwitch", + "IPKeySwitchLevel", + "IPMultiIO", + ], + DISCOVER_LIGHTS: [ + "Dimmer", + "KeyDimmer", + "IPKeyDimmer", + "IPDimmer", + "ColorEffectLight", + "IPKeySwitchLevel", + ], + DISCOVER_SENSORS: [ + "SwitchPowermeter", + "Motion", + "MotionV2", + "RemoteMotion", + "MotionIP", + "ThermostatWall", + "AreaThermostat", + "RotaryHandleSensor", + "WaterSensor", + "PowermeterGas", + "LuxSensor", + "WeatherSensor", + "WeatherStation", + "ThermostatWall2", + "TemperatureDiffSensor", + "TemperatureSensor", + "CO2Sensor", + "IPSwitchPowermeter", + "HMWIOSwitch", + "FillingLevel", + "ValveDrive", + "EcoLogic", + "IPThermostatWall", + "IPSmoke", + "RFSiren", + "PresenceIP", + "IPAreaThermostat", + "IPWeatherSensor", + "RotaryHandleSensorIP", + "IPPassageSensor", + "IPKeySwitchPowermeter", + "IPThermostatWall230V", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPBrightnessSensor", + "IPGarage", + "UniversalSensor", + "MotionIPV2", + "IPMultiIO", + "IPThermostatWall2", + ], + DISCOVER_CLIMATE: [ + "Thermostat", + "ThermostatWall", + "MAXThermostat", + "ThermostatWall2", + "MAXWallThermostat", + "IPThermostat", + "IPThermostatWall", + "ThermostatGroup", + "IPThermostatWall230V", + "IPThermostatWall2", + ], + DISCOVER_BINARY_SENSORS: [ + "ShutterContact", + "Smoke", + "SmokeV2", + "Motion", + "MotionV2", + "MotionIP", + "RemoteMotion", + "WeatherSensor", + "TiltSensor", + "IPShutterContact", + "HMWIOSwitch", + "MaxShutterContact", + "Rain", + "WiredSensor", + "PresenceIP", + "IPWeatherSensor", + "IPPassageSensor", + "SmartwareMotion", + "IPWeatherSensorPlus", + "MotionIPV2", + "WaterIP", + "IPMultiIO", + "TiltIP", + "IPShutterContactSabotage", + "IPContact", + ], + DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], + DISCOVER_LOCKS: ["KeyMatic"], +} + +HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] + +HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { + "ACTUAL_TEMPERATURE": [ + "IPAreaThermostat", + "IPWeatherSensor", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPThermostatWall", + "IPThermostatWall2", + ] +} + +HM_ATTRIBUTE_SUPPORT = { + "LOWBAT": ["battery", {0: "High", 1: "Low"}], + "LOW_BAT": ["battery", {0: "High", 1: "Low"}], + "ERROR": ["error", {0: "No"}], + "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "RSSI_PEER": ["rssi_peer", {}], + "RSSI_DEVICE": ["rssi_device", {}], + "VALVE_STATE": ["valve", {}], + "LEVEL": ["level", {}], + "BATTERY_STATE": ["battery", {}], + "CONTROL_MODE": [ + "mode", + {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, + ], + "POWER": ["power", {}], + "CURRENT": ["current", {}], + "VOLTAGE": ["voltage", {}], + "OPERATING_VOLTAGE": ["voltage", {}], + "WORKING": ["working", {0: "No", 1: "Yes"}], + "STATE_UNCERTAIN": ["state_uncertain", {}], +} + +HM_PRESS_EVENTS = [ + "PRESS_SHORT", + "PRESS_LONG", + "PRESS_CONT", + "PRESS_LONG_RELEASE", + "PRESS", +] + +HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] + +CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] + +DATA_HOMEMATIC = "homematic" +DATA_STORE = "homematic_store" +DATA_CONF = "homematic_conf" + +CONF_INTERFACES = "interfaces" +CONF_LOCAL_IP = "local_ip" +CONF_LOCAL_PORT = "local_port" +CONF_PORT = "port" +CONF_PATH = "path" +CONF_CALLBACK_IP = "callback_ip" +CONF_CALLBACK_PORT = "callback_port" +CONF_RESOLVENAMES = "resolvenames" +CONF_JSONPORT = "jsonport" diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 893b3ce8921..0dea1181d73 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -6,9 +6,9 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, CoverDevice, ) -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMCover(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMCover(HMDevice, CoverDevice): @@ -68,9 +68,9 @@ class HMCover(HMDevice, CoverDevice): def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" self._state = "LEVEL" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) if "LEVEL_2" in self._hmdevice.WRITENODE: - self._data.update({"LEVEL_2": STATE_UNKNOWN}) + self._data.update({"LEVEL_2": None}) @property def current_cover_tilt_position(self): diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py new file mode 100644 index 00000000000..4ed893bbf14 --- /dev/null +++ b/homeassistant/components/homematic/entity.py @@ -0,0 +1,297 @@ +"""Homematic base entity.""" +from abc import abstractmethod +from datetime import timedelta +import logging + +from homeassistant.const import ATTR_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_INTERFACE, + ATTR_PARAM, + ATTR_UNIQUE_ID, + DATA_HOMEMATIC, + DOMAIN, + HM_ATTRIBUTE_SUPPORT, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL_HUB = timedelta(seconds=300) +SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) + + +class HMDevice(Entity): + """The HomeMatic device base object.""" + + def __init__(self, config): + """Initialize a generic HomeMatic device.""" + self._name = config.get(ATTR_NAME) + self._address = config.get(ATTR_ADDRESS) + self._interface = config.get(ATTR_INTERFACE) + self._channel = config.get(ATTR_CHANNEL) + self._state = config.get(ATTR_PARAM) + self._unique_id = config.get(ATTR_UNIQUE_ID) + self._data = {} + self._homematic = None + self._hmdevice = None + self._connected = False + self._available = False + + # Set parameter to uppercase + if self._state: + self._state = self._state.upper() + + async def async_added_to_hass(self): + """Load data init callbacks.""" + await self.hass.async_add_job(self._subscribe_homematic_events) + + @property + def unique_id(self): + """Return unique ID. HomeMatic entity IDs are unique by default.""" + return self._unique_id.replace(" ", "_") + + @property + def should_poll(self): + """Return false. HomeMatic states are pushed by the XML-RPC Server.""" + return False + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def available(self): + """Return true if device is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = {} + + # Generate a dictionary with attributes + for node, data in HM_ATTRIBUTE_SUPPORT.items(): + # Is an attribute and exists for this object + if node in self._data: + value = data[1].get(self._data[node], self._data[node]) + attr[data[0]] = value + + # Static attributes + attr["id"] = self._hmdevice.ADDRESS + attr["interface"] = self._interface + + return attr + + def update(self): + """Connect to HomeMatic init values.""" + if self._connected: + return True + + # Initialize + self._homematic = self.hass.data[DATA_HOMEMATIC] + self._hmdevice = self._homematic.devices[self._interface][self._address] + self._connected = True + + try: + # Initialize datapoints of this object + self._init_data() + self._load_data_from_hm() + + # Link events from pyhomematic + self._available = not self._hmdevice.UNREACH + except Exception as err: # pylint: disable=broad-except + self._connected = False + _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) + + def _hm_event_callback(self, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) + has_changed = False + + # Is data needed for this instance? + if attribute in self._data: + # Did data change? + if self._data[attribute] != value: + self._data[attribute] = value + has_changed = True + + # Availability has changed + if self.available != (not self._hmdevice.UNREACH): + self._available = not self._hmdevice.UNREACH + has_changed = True + + # If it has changed data point, update Home Assistant + if has_changed: + self.schedule_update_ha_state() + + def _subscribe_homematic_events(self): + """Subscribe all required events to handle job.""" + channels_to_sub = set() + + # Push data to channels_to_sub from hmdevice metadata + for metadata in ( + self._hmdevice.SENSORNODE, + self._hmdevice.BINARYNODE, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.WRITENODE, + self._hmdevice.EVENTNODE, + self._hmdevice.ACTIONNODE, + ): + for node, channels in metadata.items(): + # Data is needed for this instance + if node in self._data: + # chan is current channel + if len(channels) == 1: + channel = channels[0] + else: + channel = self._channel + + # Prepare for subscription + try: + channels_to_sub.add(int(channel)) + except (ValueError, TypeError): + _LOGGER.error("Invalid channel in metadata from %s", self._name) + + # Set callbacks + for channel in channels_to_sub: + _LOGGER.debug("Subscribe channel %d from %s", channel, self._name) + self._hmdevice.setEventCallback( + callback=self._hm_event_callback, bequeath=False, channel=channel + ) + + def _load_data_from_hm(self): + """Load first value from pyhomematic.""" + if not self._connected: + return False + + # Read data from pyhomematic + for metadata, funct in ( + (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), + (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), + (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), + (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), + ): + for node in metadata: + if metadata[node] and node in self._data: + self._data[node] = funct(name=node, channel=self._channel) + + return True + + def _hm_set_state(self, value): + """Set data to main datapoint.""" + if self._state in self._data: + self._data[self._state] = value + + def _hm_get_state(self): + """Get data from main datapoint.""" + if self._state in self._data: + return self._data[self._state] + return None + + def _init_data(self): + """Generate a data dict (self._data) from the HomeMatic metadata.""" + # Add all attributes to data dictionary + for data_note in self._hmdevice.ATTRIBUTENODE: + self._data.update({data_note: None}) + + # Initialize device specific data + self._init_data_struct() + + @abstractmethod + def _init_data_struct(self): + """Generate a data dictionary from the HomeMatic device metadata.""" + + +class HMHub(Entity): + """The HomeMatic hub. (CCU2/HomeGear).""" + + def __init__(self, hass, homematic, name): + """Initialize HomeMatic hub.""" + self.hass = hass + self.entity_id = "{}.{}".format(DOMAIN, name.lower()) + self._homematic = homematic + self._variables = {} + self._name = name + self._state = None + + # Load data + self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) + self.hass.add_job(self._update_hub, None) + + self.hass.helpers.event.track_time_interval( + self._update_variables, SCAN_INTERVAL_VARIABLES + ) + self.hass.add_job(self._update_variables, None) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Return false. HomeMatic Hub object updates variables.""" + return False + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + @property + def state_attributes(self): + """Return the state attributes.""" + attr = self._variables.copy() + return attr + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:gradient" + + def _update_hub(self, now): + """Retrieve latest state.""" + service_message = self._homematic.getServiceMessages(self._name) + state = None if service_message is None else len(service_message) + + # state have change? + if self._state != state: + self._state = state + self.schedule_update_ha_state() + + def _update_variables(self, now): + """Retrieve all variable data and update hmvariable states.""" + variables = self._homematic.getAllSystemVariables(self._name) + if variables is None: + return + + state_change = False + for key, value in variables.items(): + if key in self._variables and value == self._variables[key]: + continue + + state_change = True + self._variables.update({key: value}) + + if state_change: + self.schedule_update_ha_state() + + def hm_set_variable(self, name, value): + """Set variable value on CCU/Homegear.""" + if name not in self._variables: + _LOGGER.error("Variable %s not found on %s", name, self.name) + return + old_value = self._variables.get(name) + if isinstance(old_value, bool): + value = cv.boolean(value) + else: + value = float(value) + self._homematic.setSystemVariable(self.name, name, value) + + self._variables.update({name: value}) + self.schedule_update_ha_state() diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 29992bccef3..52b2f9a7996 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -12,7 +12,8 @@ from homeassistant.components.light import ( Light, ) -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -29,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMLight(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMLight(HMDevice, Light): diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index 7f796b32885..0094ecd2e81 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.lock import SUPPORT_OPEN, LockDevice -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for conf in discovery_info[ATTR_DISCOVER_DEVICES]: devices.append(HMLock(conf)) - add_entities(devices) + add_entities(devices, True) class HMLock(HMDevice, LockDevice): @@ -44,7 +44,7 @@ class HMLock(HMDevice, LockDevice): def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) @property def supported_features(self): diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 8a86fd19c7d..c4e09c36b8e 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,12 +2,7 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": [ - "pyhomematic==0.1.62" - ], + "requirements": ["pyhomematic==0.1.63"], "dependencies": [], - "codeowners": [ - "@pvizeli", - "@danielperna84" - ] + "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 9fd94b9832c..3d48adc6df2 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper -from . import ( +from .const import ( ATTR_ADDRESS, ATTR_CHANNEL, ATTR_INTERFACE, @@ -22,6 +22,7 @@ from . import ( ) _LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 10c402a0dd4..bba8325650d 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -8,10 +8,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ENERGY_WATT_HOUR, POWER_WATT, - STATE_UNKNOWN, ) -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMSensor(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMSensor(HMDevice): @@ -117,6 +117,6 @@ class HMSensor(HMDevice): def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" if self._state: - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) else: _LOGGER.critical("Unable to initialize sensor: %s", self._name) diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index b77b3a1f700..53679818083 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMSwitch(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMSwitch(HMDevice, SwitchDevice): @@ -55,8 +55,8 @@ class HMSwitch(HMDevice, SwitchDevice): def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) # Need sensor values for SwitchPowermeter for node in self._hmdevice.SENSORNODE: - self._data.update({node: STATE_UNKNOWN}) + self._data.update({node: None}) diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 35f52a7b284..1ba33b0e6ee 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 62f3f9ec5d4..f3e1fc9fbec 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -3,6 +3,7 @@ import logging from pathlib import Path from typing import Optional +from homematicip.aio.device import AsyncSwitchMeasuring from homematicip.aio.group import AsyncHeatingGroup from homematicip.aio.home import AsyncHome from homematicip.base.helpers import handle_config @@ -47,6 +48,7 @@ SERVICE_ACTIVATE_VACATION = "activate_vacation" SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" SERVICE_DUMP_HAP_CONFIG = "dump_hap_config" +SERVICE_RESET_ENERGY_COUNTER = "reset_energy_counter" SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" CONFIG_SCHEMA = vol.Schema( @@ -116,6 +118,10 @@ SCHEMA_DUMP_HAP_CONFIG = vol.Schema( } ) +SCHEMA_RESET_ENERGY_COUNTER = vol.Schema( + {vol.Required(ATTR_ENTITY_ID): comp_entity_ids} +) + async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -245,7 +251,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: if entity_id_list != "all": for entity_id in entity_id_list: group = hap.hmip_device_by_entity_id.get(entity_id) - if group: + if group and isinstance(group, AsyncHeatingGroup): await group.set_active_profile(climate_profile_index) else: for group in hap.home.groups: @@ -289,6 +295,28 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: schema=SCHEMA_DUMP_HAP_CONFIG, ) + async def _async_reset_energy_counter(service): + """Service to reset the energy counter.""" + entity_id_list = service.data[ATTR_ENTITY_ID] + + for hap in hass.data[DOMAIN].values(): + if entity_id_list != "all": + for entity_id in entity_id_list: + device = hap.hmip_device_by_entity_id.get(entity_id) + if device and isinstance(device, AsyncSwitchMeasuring): + await device.reset_energy_counter() + else: + for device in hap.home.devices: + if isinstance(device, AsyncSwitchMeasuring): + await device.reset_energy_counter() + + hass.helpers.service.async_register_admin_service( + DOMAIN, + SERVICE_RESET_ENERGY_COUNTER, + _async_reset_energy_counter, + schema=SCHEMA_RESET_ENERGY_COUNTER, + ) + def _get_home(hapid: str) -> Optional[AsyncHome]: """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 83d48d0a7b1..3efd4ad91bc 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -10,6 +10,7 @@ from homematicip.aio.device import ( AsyncMotionDetectorIndoor, AsyncMotionDetectorOutdoor, AsyncMotionDetectorPushButton, + AsyncPluggableMainsFailureSurveillance, AsyncPresenceDetectorIndoor, AsyncRotaryHandleSensor, AsyncShutterContact, @@ -31,6 +32,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, DEVICE_CLASS_MOVING, DEVICE_CLASS_OPENING, + DEVICE_CLASS_POWER, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_SAFETY, DEVICE_CLASS_SMOKE, @@ -105,6 +107,10 @@ async def async_setup_entry( ), ): entities.append(HomematicipMotionDetector(hap, device)) + if isinstance(device, AsyncPluggableMainsFailureSurveillance): + entities.append( + HomematicipPluggableMainsFailureSurveillanceSensor(hap, device) + ) if isinstance(device, AsyncPresenceDetectorIndoor): entities.append(HomematicipPresenceDetector(hap, device)) if isinstance(device, AsyncSmokeDetector): @@ -328,6 +334,26 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): return self._device.lowBat +class HomematicipPluggableMainsFailureSurveillanceSensor( + HomematicipGenericDevice, BinarySensorDevice +): + """Representation of a HomematicIP Cloud pluggable mains failure surveillance sensor.""" + + def __init__(self, hap: HomematicipHAP, device) -> None: + """Initialize pluggable mains failure surveillance sensor.""" + super().__init__(hap, device) + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_POWER + + @property + def is_on(self) -> bool: + """Return true if power mains fails.""" + return not self._device.powerMainsFailure + + class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud security zone group.""" diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index ef8cbacfde2..32f38637e36 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -2,7 +2,12 @@ import logging from typing import Optional -from homematicip.aio.device import AsyncFullFlushBlind, AsyncFullFlushShutter +from homematicip.aio.device import ( + AsyncFullFlushBlind, + AsyncFullFlushShutter, + AsyncGarageDoorModuleTormatic, +) +from homematicip.base.enums import DoorCommand, DoorState from homeassistant.components.cover import ( ATTR_POSITION, @@ -40,6 +45,8 @@ async def async_setup_entry( entities.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): entities.append(HomematicipCoverShutter(hap, device)) + elif isinstance(device, AsyncGarageDoorModuleTormatic): + entities.append(HomematicipGarageDoorModuleTormatic(hap, device)) if entities: async_add_entities(entities) @@ -51,7 +58,9 @@ class HomematicipCoverShutter(HomematicipGenericDevice, CoverDevice): @property def current_cover_position(self) -> int: """Return current position of cover.""" - return int((1 - self._device.shutterLevel) * 100) + if self._device.shutterLevel is not None: + return int((1 - self._device.shutterLevel) * 100) + return None async def async_set_cover_position(self, **kwargs) -> None: """Move the cover to a specific position.""" @@ -86,7 +95,9 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverDevice): @property def current_cover_tilt_position(self) -> int: """Return current tilt position of cover.""" - return int((1 - self._device.slatsLevel) * 100) + if self._device.slatsLevel is not None: + return int((1 - self._device.slatsLevel) * 100) + return None async def async_set_cover_tilt_position(self, **kwargs) -> None: """Move the cover to a specific tilt position.""" @@ -106,3 +117,35 @@ class HomematicipCoverSlats(HomematicipCoverShutter, CoverDevice): async def async_stop_cover_tilt(self, **kwargs) -> None: """Stop the device if in motion.""" await self._device.set_shutter_stop() + + +class HomematicipGarageDoorModuleTormatic(HomematicipGenericDevice, CoverDevice): + """Representation of a HomematicIP Garage Door Module for Tormatic.""" + + @property + def current_cover_position(self) -> int: + """Return current position of cover.""" + door_state_to_position = { + DoorState.CLOSED: 0, + DoorState.OPEN: 100, + DoorState.VENTILATION_POSITION: 10, + DoorState.POSITION_UNKNOWN: None, + } + return door_state_to_position.get(self._device.doorState) + + @property + def is_closed(self) -> Optional[bool]: + """Return if the cover is closed.""" + return self._device.doorState == DoorState.CLOSED + + async def async_open_cover(self, **kwargs) -> None: + """Open the cover.""" + await self._device.send_door_command(DoorCommand.OPEN) + + async def async_close_cover(self, **kwargs) -> None: + """Close the cover.""" + await self._device.send_door_command(DoorCommand.CLOSE) + + async def async_stop_cover(self, **kwargs) -> None: + """Stop the cover.""" + await self._device.send_door_command(DoorCommand.STOP) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 4feef19c8da..e920a847292 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -1,13 +1,9 @@ { "domain": "homematicip_cloud", - "name": "Homematicip cloud", + "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": [ - "homematicip==0.10.13" - ], + "requirements": ["homematicip==0.10.15"], "dependencies": [], - "codeowners": [ - "@SukramJ" - ] + "codeowners": ["@SukramJ"] } diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index 9a7d90eba9c..750528eddf8 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -69,3 +69,10 @@ dump_hap_config: anonymize: description: (Default is True) Should the Configuration be anonymized? example: True + +reset_energy_counter: + description: Reset the energy counter of a measuring entity. + fields: + entity_id: + description: The ID of the measuring entity. Use 'all' keyword to reset all energy counters. + example: switch.livingroom diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 8e15313a4fe..8f3f6a3a177 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -5,6 +5,7 @@ from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, AsyncFullFlushSwitchMeasuring, + AsyncHeatingSwitch2, AsyncMultiIOBox, AsyncOpenCollector8Module, AsyncPlugableSwitch, @@ -55,6 +56,9 @@ async def async_setup_entry( elif isinstance(device, AsyncOpenCollector8Module): for channel in range(1, 9): entities.append(HomematicipMultiSwitch(hap, device, channel)) + elif isinstance(device, AsyncHeatingSwitch2): + for channel in range(1, 3): + entities.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncMultiIOBox): for channel in range(1, 3): entities.append(HomematicipMultiSwitch(hap, device, channel)) diff --git a/homeassistant/components/homeworks/__init__.py b/homeassistant/components/homeworks/__init__.py index 55357acdad4..c6296d8f4c6 100644 --- a/homeassistant/components/homeworks/__init__.py +++ b/homeassistant/components/homeworks/__init__.py @@ -98,7 +98,7 @@ class HomeworksDevice: """Base class of a Homeworks device.""" def __init__(self, controller, addr, name): - """Controller, address, and name of the device.""" + """Initialize Homeworks device.""" self._addr = addr self._name = name self._controller = controller diff --git a/homeassistant/components/homeworks/manifest.json b/homeassistant/components/homeworks/manifest.json index f2929fb655e..e28230662f8 100644 --- a/homeassistant/components/homeworks/manifest.json +++ b/homeassistant/components/homeworks/manifest.json @@ -1,10 +1,8 @@ { "domain": "homeworks", - "name": "Homeworks", + "name": "Lutron Homeworks", "documentation": "https://www.home-assistant.io/integrations/homeworks", - "requirements": [ - "pyhomeworks==0.0.6" - ], + "requirements": ["pyhomeworks==0.0.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 42f4778eb4f..f8537bfe96a 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -1,43 +1,43 @@ """Support for Honeywell (US) Total Connect Comfort climate systems.""" import datetime import logging -from typing import Any, Dict, Optional, List +from typing import Any, Dict, List, Optional import requests -import voluptuous as vol import somecomfort +import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_AUTO, FAN_DIFFUSE, FAN_ON, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_FAN, - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - PRESET_AWAY, - PRESET_NONE, ) from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_PASSWORD, + CONF_REGION, CONF_USERNAME, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_TEMPERATURE, - CONF_REGION, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 9d644de4445..5d17824e0cb 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -1,10 +1,8 @@ { "domain": "honeywell", - "name": "Honeywell", + "name": "Honeywell Total Connect Comfort (TCC)", "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": [ - "somecomfort==0.5.2" - ], + "requirements": ["somecomfort==0.5.2"], "dependencies": [], "codeowners": ["@zxdavb"] } diff --git a/homeassistant/components/hook/switch.py b/homeassistant/components/hook/switch.py index d26f35e2dfc..582dc61af14 100644 --- a/homeassistant/components/hook/switch.py +++ b/homeassistant/components/hook/switch.py @@ -1,13 +1,13 @@ """Support Hook, available at hooksmarthome.com.""" -import logging import asyncio +import logging -import voluptuous as vol -import async_timeout import aiohttp +import async_timeout +import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_TOKEN +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -21,12 +21,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Exclusive( CONF_PASSWORD, "hook_secret", - msg="hook: provide " + "username/password OR token", + msg="hook: provide username/password OR token", ): cv.string, vol.Exclusive( - CONF_TOKEN, - "hook_secret", - msg="hook: provide " + "username/password OR token", + CONF_TOKEN, "hook_secret", msg="hook: provide username/password OR token", ): cv.string, vol.Inclusive(CONF_USERNAME, "hook_auth"): cv.string, vol.Inclusive(CONF_PASSWORD, "hook_auth"): cv.string, diff --git a/homeassistant/components/horizon/manifest.json b/homeassistant/components/horizon/manifest.json index 4ba3a61d8b7..620a90d6c09 100644 --- a/homeassistant/components/horizon/manifest.json +++ b/homeassistant/components/horizon/manifest.json @@ -1,10 +1,8 @@ { "domain": "horizon", - "name": "Horizon", + "name": "Unitymedia Horizon HD Recorder", "documentation": "https://www.home-assistant.io/integrations/horizon", - "requirements": [ - "horimote==0.4.1" - ], + "requirements": ["horimote==0.4.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/hp_ilo/manifest.json b/homeassistant/components/hp_ilo/manifest.json index 3dc591cac4c..c863651699a 100644 --- a/homeassistant/components/hp_ilo/manifest.json +++ b/homeassistant/components/hp_ilo/manifest.json @@ -1,10 +1,8 @@ { "domain": "hp_ilo", - "name": "Hp ilo", + "name": "HP Integrated Lights-Out (ILO)", "documentation": "https://www.home-assistant.io/integrations/hp_ilo", - "requirements": [ - "python-hpilo==4.3" - ], + "requirements": ["python-hpilo==4.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 667a5789182..1aaf4aed539 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -1,12 +1,8 @@ { "domain": "html5", - "name": "HTML5 Notifications", + "name": "HTML5 Push Notifications", "documentation": "https://www.home-assistant.io/integrations/html5", - "requirements": [ - "pywebpush==1.9.2" - ], - "dependencies": ["frontend"], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["pywebpush==1.9.2"], + "dependencies": ["http"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index 481a00e96e1..b966f5ae6a1 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -1,23 +1,30 @@ """HTML5 Push Messaging notification service.""" from datetime import datetime, timedelta - from functools import partial -from urllib.parse import urlparse import json import logging import time +from urllib.parse import urlparse import uuid from aiohttp.hdrs import AUTHORIZATION import jwt -from pywebpush import WebPusher from py_vapid import Vapid +from pywebpush import WebPusher import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.components import websocket_api from homeassistant.components.frontend import add_manifest_json_key from homeassistant.components.http import HomeAssistantView +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( HTTP_BAD_REQUEST, HTTP_INTERNAL_SERVER_ERROR, @@ -29,15 +36,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import ensure_unique_string from homeassistant.util.json import load_json, save_json -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - from .const import DOMAIN, SERVICE_DISMISS _LOGGER = logging.getLogger(__name__) @@ -347,12 +345,12 @@ class HTML5PushCallbackView(HomeAssistantView): if parts[0].lower() != "bearer": return self.json_message( - "Authorization header must " "start with Bearer", + "Authorization header must start with Bearer", status_code=HTTP_UNAUTHORIZED, ) if len(parts) != 2: return self.json_message( - "Authorization header must " "be Bearer token", + "Authorization header must be Bearer token", status_code=HTTP_UNAUTHORIZED, ) @@ -509,7 +507,7 @@ class HTML5NotificationService(BaseNotificationService): info = REGISTER_SCHEMA(info) except vol.Invalid: _LOGGER.error( - "%s is not a valid HTML5 push notification" " target", target + "%s is not a valid HTML5 push notification target", target ) continue payload[ATTR_DATA][ATTR_JWT] = add_jwt( diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 4d3985a7af3..0d93461f90f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -26,7 +26,6 @@ from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa: F401 - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "http" @@ -55,6 +54,8 @@ DEFAULT_DEVELOPMENT = "0" DEFAULT_CORS = "https://cast.home-assistant.io" NO_LOGIN_ATTEMPT_THRESHOLD = -1 +MAX_CLIENT_SIZE: int = 1024 ** 2 * 16 + HTTP_SCHEMA = vol.Schema( { @@ -189,7 +190,9 @@ class HomeAssistantHTTP: ssl_profile, ): """Initialize the HTTP Home Assistant server.""" - app = self.app = web.Application(middlewares=[]) + app = self.app = web.Application( + middlewares=[], client_max_size=MAX_CLIENT_SIZE + ) app[KEY_HASS] = hass # This order matters diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 97bd9b7d4bc..58814b77e2d 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,17 +1,16 @@ """Authentication for HTTP component.""" import logging +import secrets from aiohttp import hdrs from aiohttp.web import middleware import jwt -from homeassistant.auth.util import generate_secret from homeassistant.core import callback from homeassistant.util import dt as dt_util from .const import KEY_AUTHENTICATED, KEY_HASS_USER, KEY_REAL_IP - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -27,7 +26,7 @@ def async_sign_path(hass, refresh_token_id, path, expiration): secret = hass.data.get(DATA_SIGN_SECRET) if secret is None: - secret = hass.data[DATA_SIGN_SECRET] = generate_secret() + secret = hass.data[DATA_SIGN_SECRET] = secrets.token_hex() now = dt_util.utcnow() return "{}?{}={}".format( diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 7d1e24f3698..da406c071e4 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -17,7 +17,6 @@ from homeassistant.util.yaml import dump from .const import KEY_REAL_IP - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -97,7 +96,7 @@ async def process_wrong_login(request): """ remote_addr = request[KEY_REAL_IP] - msg = "Login attempt or request with invalid authentication " "from {}".format( + msg = "Login attempt or request with invalid authentication from {}".format( remote_addr ) _LOGGER.warning(msg) @@ -151,7 +150,7 @@ async def process_success_login(request): and request.app[KEY_FAILED_LOGIN_ATTEMPTS][remote_addr] > 0 ): _LOGGER.debug( - "Login success, reset failed login attempts counter" " from %s", remote_addr + "Login success, reset failed login attempts counter from %s", remote_addr ) request.app[KEY_FAILED_LOGIN_ATTEMPTS].pop(remote_addr) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index de4547f4782..2d99a049e4b 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,11 +1,10 @@ """Provide CORS support for the HTTP component.""" +from aiohttp.hdrs import ACCEPT, AUTHORIZATION, CONTENT_TYPE, ORIGIN from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource -from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback - # mypy: allow-untyped-defs, no-check-untyped-defs ALLOWED_CORS_HEADERS = [ @@ -23,6 +22,7 @@ def setup_cors(app, origins): """Set up CORS.""" # This import should remain here. That way the HTTP integration can always # be imported by other integrations without it's requirements being installed. + # pylint: disable=import-outside-toplevel import aiohttp_cors cors = aiohttp_cors.setup( diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 5945a4ca402..51b3b5617e4 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -4,7 +4,6 @@ import logging import voluptuous as vol - # mypy: allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -21,6 +20,9 @@ class RequestDataValidator: def __init__(self, schema, allow_empty=False): """Initialize the decorator.""" + if isinstance(schema, dict): + schema = vol.Schema(schema) + self._schema = schema self._allow_empty = allow_empty diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index 6db8b041cfd..6f8328b33fb 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -2,11 +2,8 @@ "domain": "http", "name": "HTTP", "documentation": "https://www.home-assistant.io/integrations/http", - "requirements": [ - "aiohttp_cors==0.7.0" - ], + "requirements": ["aiohttp_cors==0.7.0"], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index f327c86a4c1..f2334ce0a2f 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -8,7 +8,6 @@ from homeassistant.core import callback from .const import KEY_REAL_IP - # mypy: allow-untyped-defs diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index 7d84be0f6dd..a5fe686a651 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -3,10 +3,9 @@ from pathlib import Path from aiohttp import hdrs from aiohttp.web import FileResponse -from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden +from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound from aiohttp.web_urldispatcher import StaticResource - # mypy: allow-untyped-defs CACHE_TIME = 31 * 86400 # = 1 month diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 31f96833667..e60091684d3 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -142,9 +142,11 @@ def request_handler_factory(view, handler): elif result is None: result = b"" elif not isinstance(result, bytes): - assert False, ( - "Result should be None, string, bytes or Response. " "Got: {}" - ).format(result) + assert ( + False + ), "Result should be None, string, bytes or Response. Got: {}".format( + result + ) return web.Response(body=result, status=status_code) diff --git a/homeassistant/components/htu21d/manifest.json b/homeassistant/components/htu21d/manifest.json index 14b0d7b3f15..2b36c4b66fb 100644 --- a/homeassistant/components/htu21d/manifest.json +++ b/homeassistant/components/htu21d/manifest.json @@ -1,11 +1,8 @@ { "domain": "htu21d", - "name": "Htu21d", + "name": "HTU21D(F) Sensor", "documentation": "https://www.home-assistant.io/integrations/htu21d", - "requirements": [ - "i2csense==0.0.4", - "smbus-cffi==0.5.1" - ], + "requirements": ["i2csense==0.0.4", "smbus-cffi==0.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/huawei_lte/.translations/ca.json b/homeassistant/components/huawei_lte/.translations/ca.json index b213da018d2..594c2e3b16d 100644 --- a/homeassistant/components/huawei_lte/.translations/ca.json +++ b/homeassistant/components/huawei_lte/.translations/ca.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nom del servei de notificacions", "recipient": "Destinataris de notificacions SMS", "track_new_devices": "Segueix dispositius nous" } diff --git a/homeassistant/components/huawei_lte/.translations/da.json b/homeassistant/components/huawei_lte/.translations/da.json new file mode 100644 index 00000000000..19bc69b77fd --- /dev/null +++ b/homeassistant/components/huawei_lte/.translations/da.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "Denne enhed er allerede konfigureret", + "already_in_progress": "Denne enhed er allerede ved at blive konfigureret", + "not_huawei_lte": "Ikke en Huawei LTE-enhed" + }, + "error": { + "connection_failed": "Forbindelsen mislykkedes", + "connection_timeout": "Timeout for forbindelse", + "incorrect_password": "Forkert adgangskode", + "incorrect_username": "Forkert brugernavn", + "incorrect_username_or_password": "Forkert brugernavn eller adgangskode", + "invalid_url": "Ugyldig webadresse", + "login_attempts_exceeded": "Maksimale loginfors\u00f8g overskredet. Pr\u00f8v igen senere", + "response_error": "Ukendt fejl fra enheden", + "unknown_connection_error": "Ukendt fejl ved tilslutning til enheden" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "url": "Webadresse", + "username": "Brugernavn" + }, + "description": "Indtast oplysninger om enhedsadgang. Det er valgfrit at specificere brugernavn og adgangskode, men muligg\u00f8r underst\u00f8ttelse af flere integrationsfunktioner. P\u00e5 den anden side kan brug af en autoriseret forbindelse for\u00e5rsage problemer med at f\u00e5 adgang til enhedens webgr\u00e6nseflade uden for Home Assistant, mens integrationen er aktiv, og omvendt.", + "title": "Konfigurer Huawei LTE" + } + }, + "title": "Huawei LTE" + }, + "options": { + "step": { + "init": { + "data": { + "name": "Navn p\u00e5 meddelelsestjeneste (\u00e6ndring kr\u00e6ver genstart)", + "recipient": "Modtagere af SMS-meddelelse", + "track_new_devices": "Spor nye enheder" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/de.json b/homeassistant/components/huawei_lte/.translations/de.json index c3f4025b8b6..ddf6ad55eaa 100644 --- a/homeassistant/components/huawei_lte/.translations/de.json +++ b/homeassistant/components/huawei_lte/.translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert", - "already_in_progress": "Dieses Ger\u00e4t wurde bereits konfiguriert" + "already_in_progress": "Dieses Ger\u00e4t wurde bereits konfiguriert", + "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t" }, "error": { "connection_failed": "Verbindung fehlgeschlagen.", @@ -10,7 +11,10 @@ "incorrect_password": "Ung\u00fcltiges Passwort", "incorrect_username": "Ung\u00fcltiger Benutzername", "incorrect_username_or_password": "Ung\u00fcltiger Benutzername oder Kennwort", - "invalid_url": "Ung\u00fcltige URL" + "invalid_url": "Ung\u00fcltige URL", + "login_attempts_exceeded": "Maximale Anzahl von Anmeldeversuchen \u00fcberschritten. Bitte versuchen Sie es sp\u00e4ter erneut", + "response_error": "Unbekannter Fehler vom Ger\u00e4t", + "unknown_connection_error": "Unbekannter Fehler beim Herstellen der Verbindung zum Ger\u00e4t" }, "step": { "user": { @@ -18,15 +22,20 @@ "password": "Passwort", "url": "URL", "username": "Benutzername" - } + }, + "description": "Geben Sie die Zugangsdaten zum Ger\u00e4t ein. Die Angabe von Benutzername und Passwort ist optional, erm\u00f6glicht aber die Unterst\u00fctzung weiterer Integrationsfunktionen. Andererseits kann die Verwendung einer autorisierten Verbindung zu Problemen beim Zugriff auf die Web-Schnittstelle des Ger\u00e4ts von au\u00dferhalb des Home Assistant f\u00fchren, w\u00e4hrend die Integration aktiv ist, und umgekehrt.", + "title": "Konfigurieren Sie Huawei LTE" } - } + }, + "title": "Huawei LTE" }, "options": { "step": { "init": { "data": { - "recipient": "SMS-Benachrichtigungsempf\u00e4nger" + "name": "Name des Benachrichtigungsdienstes (\u00c4nderung erfordert Neustart)", + "recipient": "SMS-Benachrichtigungsempf\u00e4nger", + "track_new_devices": "Neue Ger\u00e4te verfolgen" } } } diff --git a/homeassistant/components/huawei_lte/.translations/en.json b/homeassistant/components/huawei_lte/.translations/en.json index 52aaafe595c..c5f2b4a2a02 100644 --- a/homeassistant/components/huawei_lte/.translations/en.json +++ b/homeassistant/components/huawei_lte/.translations/en.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices" } diff --git a/homeassistant/components/huawei_lte/.translations/es.json b/homeassistant/components/huawei_lte/.translations/es.json index 92ccf8fc048..c35d1eacf23 100644 --- a/homeassistant/components/huawei_lte/.translations/es.json +++ b/homeassistant/components/huawei_lte/.translations/es.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nombre del servicio de notificaci\u00f3n", "recipient": "Destinatarios de notificaciones por SMS", "track_new_devices": "Rastrea nuevos dispositivos" } diff --git a/homeassistant/components/huawei_lte/.translations/fr.json b/homeassistant/components/huawei_lte/.translations/fr.json index 34db4e93bc4..9f6ae9a09bf 100644 --- a/homeassistant/components/huawei_lte/.translations/fr.json +++ b/homeassistant/components/huawei_lte/.translations/fr.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nom du service de notification (red\u00e9marrage requis)", "recipient": "Destinataires des notifications SMS", "track_new_devices": "Suivre les nouveaux appareils" } diff --git a/homeassistant/components/huawei_lte/.translations/it.json b/homeassistant/components/huawei_lte/.translations/it.json index bcbae3b1b25..4ad17ecaa36 100644 --- a/homeassistant/components/huawei_lte/.translations/it.json +++ b/homeassistant/components/huawei_lte/.translations/it.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nome del servizio di notifica (la modifica richiede il riavvio)", "recipient": "Destinatari della notifica SMS", "track_new_devices": "Traccia nuovi dispositivi" } diff --git a/homeassistant/components/huawei_lte/.translations/ko.json b/homeassistant/components/huawei_lte/.translations/ko.json index a9ac8d7f62c..f6b3d855679 100644 --- a/homeassistant/components/huawei_lte/.translations/ko.json +++ b/homeassistant/components/huawei_lte/.translations/ko.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "\uc54c\ub9bc \uc11c\ube44\uc2a4 \uc774\ub984 (\ubcc0\uacbd \uc2dc \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud568)", "recipient": "SMS \uc54c\ub9bc \uc218\uc2e0\uc790", "track_new_devices": "\uc0c8\ub85c\uc6b4 \uae30\uae30 \ucd94\uc801" } diff --git a/homeassistant/components/huawei_lte/.translations/lb.json b/homeassistant/components/huawei_lte/.translations/lb.json index 3c8f0464a55..56d383edba3 100644 --- a/homeassistant/components/huawei_lte/.translations/lb.json +++ b/homeassistant/components/huawei_lte/.translations/lb.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Numm vum Notifikatioun's Service", "recipient": "Empf\u00e4nger vun SMS Notifikatioune", "track_new_devices": "Nei Apparater verfollegen" } diff --git a/homeassistant/components/huawei_lte/.translations/nl.json b/homeassistant/components/huawei_lte/.translations/nl.json index 6d5e5c3e957..297ec922abf 100644 --- a/homeassistant/components/huawei_lte/.translations/nl.json +++ b/homeassistant/components/huawei_lte/.translations/nl.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Naam meldingsservice (wijziging vereist opnieuw opstarten)", "recipient": "Ontvangers van sms-berichten", "track_new_devices": "Volg nieuwe apparaten" } diff --git a/homeassistant/components/huawei_lte/.translations/no.json b/homeassistant/components/huawei_lte/.translations/no.json index 35a5d531c5d..39cb5bf87fe 100644 --- a/homeassistant/components/huawei_lte/.translations/no.json +++ b/homeassistant/components/huawei_lte/.translations/no.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Navn p\u00e5 varslingstjeneste (endring krever omstart)", "recipient": "Mottakere av SMS-varsling", "track_new_devices": "Spor nye enheter" } diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index 3851d0a409f..a4e7d72852a 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Nazwa us\u0142ugi powiadomie\u0144 (zmiana wymaga ponownego uruchomienia)", "recipient": "Odbiorcy powiadomie\u0144 SMS", "track_new_devices": "\u015aled\u017a nowe urz\u0105dzenia" } diff --git a/homeassistant/components/huawei_lte/.translations/ru.json b/homeassistant/components/huawei_lte/.translations/ru.json index ec28325dcdd..c7c9e2033ef 100644 --- a/homeassistant/components/huawei_lte/.translations/ru.json +++ b/homeassistant/components/huawei_lte/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "not_huawei_lte": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Huawei LTE" }, "error": { @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u043b\u0443\u0436\u0431\u044b \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439 (\u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", "recipient": "\u041f\u043e\u043b\u0443\u0447\u0430\u0442\u0435\u043b\u0438 SMS-\u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u0439", "track_new_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } diff --git a/homeassistant/components/huawei_lte/.translations/sl.json b/homeassistant/components/huawei_lte/.translations/sl.json index 5022e358ca7..c2d7e0bd983 100644 --- a/homeassistant/components/huawei_lte/.translations/sl.json +++ b/homeassistant/components/huawei_lte/.translations/sl.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Ime storitve obve\u0161\u010danja (sprememba zahteva ponovni zagon)", "recipient": "Prejemniki obvestil SMS", "track_new_devices": "Sledi novim napravam" } diff --git a/homeassistant/components/huawei_lte/.translations/zh-Hant.json b/homeassistant/components/huawei_lte/.translations/zh-Hant.json index 37f1111b77f..201e9afec4b 100644 --- a/homeassistant/components/huawei_lte/.translations/zh-Hant.json +++ b/homeassistant/components/huawei_lte/.translations/zh-Hant.json @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "\u901a\u77e5\u670d\u52d9\u540d\u7a31\uff08\u8b8a\u66f4\u5f8c\u9700\u91cd\u555f\uff09", "recipient": "\u7c21\u8a0a\u901a\u77e5\u6536\u4ef6\u8005", "track_new_devices": "\u8ffd\u8e64\u65b0\u8a2d\u5099" } diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index ada1f0a8abd..97a57405ae0 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -3,12 +3,11 @@ from collections import defaultdict from datetime import timedelta from functools import partial -from urllib.parse import urlparse import ipaddress import logging from typing import Any, Callable, Dict, List, Set, Tuple +from urllib.parse import urlparse -import voluptuous as vol import attr from getmac import get_mac_address from huawei_lte_api.AuthorizedConnection import AuthorizedConnection @@ -20,14 +19,16 @@ from huawei_lte_api.exceptions import ( ) from requests.exceptions import Timeout from url_normalize import url_normalize +import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.notify import DOMAIN as NOTIFY_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, SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + CONF_NAME, CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, @@ -36,8 +37,11 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + discovery, +) from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -46,10 +50,13 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import HomeAssistantType + from .const import ( + ADMIN_SERVICES, ALL_KEYS, CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, + DEFAULT_NOTIFY_SERVICE_NAME, DOMAIN, KEY_DEVICE_BASIC_INFORMATION, KEY_DEVICE_INFORMATION, @@ -58,11 +65,14 @@ from .const import ( KEY_MONITORING_STATUS, KEY_MONITORING_TRAFFIC_STATISTICS, KEY_WLAN_HOST_LIST, + SERVICE_CLEAR_TRAFFIC_STATISTICS, + SERVICE_REBOOT, + SERVICE_RESUME_INTEGRATION, + SERVICE_SUSPEND_INTEGRATION, UPDATE_OPTIONS_SIGNAL, UPDATE_SIGNAL, ) - _LOGGER = logging.getLogger(__name__) # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. @@ -77,9 +87,10 @@ NOTIFY_SCHEMA = vol.Any( None, vol.Schema( { + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_RECIPIENT): vol.Any( None, vol.All(cv.ensure_list, [cv.string]) - ) + ), } ), ) @@ -103,6 +114,8 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url}) + CONFIG_ENTRY_PLATFORMS = ( BINARY_SENSOR_DOMAIN, DEVICE_TRACKER_DOMAIN, @@ -127,6 +140,7 @@ class Router: ) unload_handlers: List[CALLBACK_TYPE] = attr.ib(init=False, factory=list) client: Client + suspended = attr.ib(init=False, default=False) def __attrs_post_init__(self): """Set up internal state on init.""" @@ -150,52 +164,62 @@ class Router: """Get router connections for device registry.""" return {(dr.CONNECTION_NETWORK_MAC, self.mac)} if self.mac else set() + def _get_data(self, key: str, func: Callable[[None], Any]) -> None: + if not self.subscriptions.get(key): + return + _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) + try: + self.data[key] = func() + except ResponseErrorNotSupportedException: + _LOGGER.info( + "%s not supported by device, excluding from future updates", key + ) + self.subscriptions.pop(key) + except ResponseErrorLoginRequiredException: + if isinstance(self.connection, AuthorizedConnection): + _LOGGER.debug("Trying to authorize again...") + if self.connection.enforce_authorized_connection(): + _LOGGER.debug( + "...success, %s will be updated by a future periodic run", key, + ) + else: + _LOGGER.debug("...failed") + return + _LOGGER.info( + "%s requires authorization, excluding from future updates", key + ) + self.subscriptions.pop(key) + finally: + _LOGGER.debug("%s=%s", key, self.data.get(key)) + def update(self) -> None: """Update router data.""" - def get_data(key: str, func: Callable[[None], Any]) -> None: - if not self.subscriptions[key]: - return - _LOGGER.debug("Getting %s for subscribers %s", key, self.subscriptions[key]) - try: - self.data[key] = func() - except ResponseErrorNotSupportedException: - _LOGGER.info( - "%s not supported by device, excluding from future updates", key - ) - self.subscriptions.pop(key) - except ResponseErrorLoginRequiredException: - _LOGGER.info( - "%s requires authorization, excluding from future updates", key - ) - self.subscriptions.pop(key) - finally: - _LOGGER.debug("%s=%s", key, self.data.get(key)) + if self.suspended: + _LOGGER.debug("Integration suspended, not updating data") + return - get_data(KEY_DEVICE_INFORMATION, self.client.device.information) + self._get_data(KEY_DEVICE_INFORMATION, self.client.device.information) if self.data.get(KEY_DEVICE_INFORMATION): # Full information includes everything in basic self.subscriptions.pop(KEY_DEVICE_BASIC_INFORMATION, None) - get_data(KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information) - get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) - get_data(KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch) - get_data(KEY_MONITORING_STATUS, self.client.monitoring.status) - get_data( + self._get_data( + KEY_DEVICE_BASIC_INFORMATION, self.client.device.basic_information + ) + self._get_data(KEY_DEVICE_SIGNAL, self.client.device.signal) + self._get_data( + KEY_DIALUP_MOBILE_DATASWITCH, self.client.dial_up.mobile_dataswitch + ) + self._get_data(KEY_MONITORING_STATUS, self.client.monitoring.status) + self._get_data( KEY_MONITORING_TRAFFIC_STATISTICS, self.client.monitoring.traffic_statistics ) - get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) + self._get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) self.signal_update() - def cleanup(self, *_) -> None: - """Clean up resources.""" - - self.subscriptions.clear() - - for handler in self.unload_handlers: - handler() - self.unload_handlers.clear() - + def logout(self) -> None: + """Log out router session.""" if not isinstance(self.connection, AuthorizedConnection): return try: @@ -207,6 +231,17 @@ class Router: except Exception: # pylint: disable=broad-except _LOGGER.warning("Logout error", exc_info=True) + def cleanup(self, *_) -> None: + """Clean up resources.""" + + self.subscriptions.clear() + + for handler in self.unload_handlers: + handler() + self.unload_handlers.clear() + + self.logout() + @attr.s class HuaweiLteData: @@ -242,6 +277,13 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) ): new_options[f"{CONF_RECIPIENT}_from_yaml"] = yaml_recipient new_options[CONF_RECIPIENT] = yaml_recipient + yaml_notify_name = yaml_config.get(NOTIFY_DOMAIN, {}).get(CONF_NAME) + if ( + yaml_notify_name is not None + and yaml_notify_name != config_entry.options.get(f"{CONF_NAME}_from_yaml") + ): + new_options[f"{CONF_NAME}_from_yaml"] = yaml_notify_name + new_options[CONF_NAME] = yaml_notify_name # Update entry if overrides were found if new_data or new_options: hass.config_entries.async_update_entry( @@ -333,7 +375,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) hass, NOTIFY_DOMAIN, DOMAIN, - {CONF_URL: url, CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT)}, + { + CONF_URL: url, + CONF_NAME: config_entry.options.get(CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME), + CONF_RECIPIENT: config_entry.options.get(CONF_RECIPIENT), + }, hass.data[DOMAIN].hass_config, ) @@ -387,6 +433,56 @@ async def async_setup(hass: HomeAssistantType, config) -> bool: for router_config in config.get(DOMAIN, []): domain_config[url_normalize(router_config.pop(CONF_URL))] = router_config + def service_handler(service) -> None: + """Apply a service.""" + url = service.data.get(CONF_URL) + routers = hass.data[DOMAIN].routers + if url: + router = routers.get(url) + elif not routers: + _LOGGER.error("%s: no routers configured", service.service) + return + elif len(routers) == 1: + router = next(iter(routers.values())) + else: + _LOGGER.error( + "%s: more than one router configured, must specify one of URLs %s", + service.service, + sorted(routers), + ) + return + if not router: + _LOGGER.error("%s: router %s unavailable", service.service, url) + return + + if service.service == SERVICE_CLEAR_TRAFFIC_STATISTICS: + if router.suspended: + _LOGGER.debug("%s: ignored, integration suspended", service.service) + return + result = router.client.monitoring.set_clear_traffic() + _LOGGER.debug("%s: %s", service.service, result) + elif service.service == SERVICE_REBOOT: + if router.suspended: + _LOGGER.debug("%s: ignored, integration suspended", service.service) + return + result = router.client.device.reboot() + _LOGGER.debug("%s: %s", service.service, result) + elif service.service == SERVICE_RESUME_INTEGRATION: + # Login will be handled automatically on demand + router.suspended = False + _LOGGER.debug("%s: %s", service.service, "done") + elif service.service == SERVICE_SUSPEND_INTEGRATION: + router.logout() + router.suspended = True + _LOGGER.debug("%s: %s", service.service, "done") + else: + _LOGGER.error("%s: unsupported service", service.service) + + for service in ADMIN_SERVICES: + hass.helpers.service.async_register_admin_service( + DOMAIN, service, service_handler, schema=SERVICE_SCHEMA, + ) + for url, router_config in domain_config.items(): hass.async_create_task( hass.config_entries.flow.async_init( diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 4fcb400c32a..104933fe714 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -11,10 +11,10 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, ) from homeassistant.const import CONF_URL + from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_MONITORING_STATUS - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/config_flow.py b/homeassistant/components/huawei_lte/config_flow.py index 1bc3753bdd7..0dcdb6636c6 100644 --- a/homeassistant/components/huawei_lte/config_flow.py +++ b/homeassistant/components/huawei_lte/config_flow.py @@ -3,15 +3,16 @@ from collections import OrderedDict import logging from typing import Optional +from urllib.parse import urlparse from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection from huawei_lte_api.exceptions import ( - LoginErrorUsernameWrongException, LoginErrorPasswordWrongException, - LoginErrorUsernamePasswordWrongException, LoginErrorUsernamePasswordOverrunException, + LoginErrorUsernamePasswordWrongException, + LoginErrorUsernameWrongException, ResponseErrorException, ) from requests.exceptions import Timeout @@ -19,15 +20,20 @@ from url_normalize import url_normalize import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.ssdp import ATTR_HOST, ATTR_NAME, ATTR_PRESENTATIONURL -from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_URL, CONF_USERNAME +from homeassistant.components import ssdp +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_RECIPIENT, + CONF_URL, + CONF_USERNAME, +) from homeassistant.core import callback -from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME -# https://github.com/PyCQA/pylint/issues/3202 +# see https://github.com/PyCQA/pylint/issues/3202 about the DOMAIN's pylint issue +from .const import CONNECTION_TIMEOUT, DEFAULT_DEVICE_NAME, DEFAULT_NOTIFY_SERVICE_NAME from .const import DOMAIN # pylint: disable=unused-import - _LOGGER = logging.getLogger(__name__) @@ -209,13 +215,14 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle SSDP initiated config flow.""" # Attempt to distinguish from other non-LTE Huawei router devices, at least # some ones we are interested in have "Mobile Wi-Fi" friendlyName. - if "mobile" not in discovery_info.get(ATTR_NAME, "").lower(): + if "mobile" not in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "").lower(): return self.async_abort(reason="not_huawei_lte") # https://github.com/PyCQA/pylint/issues/3167 url = self.context[CONF_URL] = url_normalize( # pylint: disable=no-member discovery_info.get( - ATTR_PRESENTATIONURL, f"http://{discovery_info[ATTR_HOST]}/" + ssdp.ATTR_UPNP_PRESENTATION_URL, + f"http://{urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname}/", ) ) @@ -241,14 +248,22 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init(self, user_input=None): """Handle options flow.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + # Preserve existing options, for example *_from_yaml markers + data = {**self.config_entry.options, **user_input} + return self.async_create_entry(title="", data=data) data_schema = vol.Schema( { + vol.Optional( + CONF_NAME, + default=self.config_entry.options.get( + CONF_NAME, DEFAULT_NOTIFY_SERVICE_NAME + ), + ): str, vol.Optional( CONF_RECIPIENT, default=self.config_entry.options.get(CONF_RECIPIENT, ""), - ): str + ): str, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index b6e079576ac..c6837fce06c 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -3,6 +3,7 @@ DOMAIN = "huawei_lte" DEFAULT_DEVICE_NAME = "LTE" +DEFAULT_NOTIFY_SERVICE_NAME = DOMAIN UPDATE_SIGNAL = f"{DOMAIN}_update" UPDATE_OPTIONS_SIGNAL = f"{DOMAIN}_options_update" @@ -12,6 +13,18 @@ UNIT_SECONDS = "s" CONNECTION_TIMEOUT = 10 +SERVICE_CLEAR_TRAFFIC_STATISTICS = "clear_traffic_statistics" +SERVICE_REBOOT = "reboot" +SERVICE_RESUME_INTEGRATION = "resume_integration" +SERVICE_SUSPEND_INTEGRATION = "suspend_integration" + +ADMIN_SERVICES = { + SERVICE_CLEAR_TRAFFIC_STATISTICS, + SERVICE_REBOOT, + SERVICE_RESUME_INTEGRATION, + SERVICE_SUSPEND_INTEGRATION, +} + KEY_DEVICE_BASIC_INFORMATION = "device_basic_information" KEY_DEVICE_INFORMATION = "device_information" KEY_DEVICE_SIGNAL = "device_signal" diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index f5f834fa186..a9c61831fdd 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -15,10 +15,10 @@ from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.const import CONF_URL from homeassistant.helpers import entity_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_WLAN_HOST_LIST, UPDATE_SIGNAL - _LOGGER = logging.getLogger(__name__) _DEVICE_SCAN = f"{DEVICE_TRACKER_DOMAIN}/device_scan" diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 8fd4ba4bec1..1f5fa69d341 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -16,7 +16,5 @@ } ], "dependencies": [], - "codeowners": [ - "@scop" - ] + "codeowners": ["@scop"] } diff --git a/homeassistant/components/huawei_lte/notify.py b/homeassistant/components/huawei_lte/notify.py index 4b5a63756b5..5619a5d702c 100644 --- a/homeassistant/components/huawei_lte/notify.py +++ b/homeassistant/components/huawei_lte/notify.py @@ -6,13 +6,12 @@ from typing import Any, List import attr from huawei_lte_api.exceptions import ResponseErrorException -from homeassistant.components.notify import BaseNotificationService, ATTR_TARGET +from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT, CONF_URL from . import Router from .const import DOMAIN - _LOGGER = logging.getLogger(__name__) @@ -45,6 +44,12 @@ class HuaweiLteSmsNotificationService(BaseNotificationService): if not targets or not message: return + if self.router.suspended: + _LOGGER.debug( + "Integration suspended, not sending notification to %s", targets + ) + return + try: resp = self.router.client.sms.send_sms( phone_numbers=targets, message=message diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 3cc36b30d8e..3b6b75edfba 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -6,11 +6,11 @@ from typing import Optional import attr -from homeassistant.const import CONF_URL, STATE_UNKNOWN from homeassistant.components.sensor import ( DEVICE_CLASS_SIGNAL_STRENGTH, DOMAIN as SENSOR_DOMAIN, ) +from homeassistant.const import CONF_URL, STATE_UNKNOWN from . import HuaweiLteBaseEntity from .const import ( @@ -22,7 +22,6 @@ from .const import ( UNIT_SECONDS, ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_lte/services.yaml b/homeassistant/components/huawei_lte/services.yaml new file mode 100644 index 00000000000..bcb9be33299 --- /dev/null +++ b/homeassistant/components/huawei_lte/services.yaml @@ -0,0 +1,30 @@ +clear_traffic_statistics: + description: Clear traffic statistics. + fields: + url: + description: URL of router to clear; optional when only one is configured. + example: http://192.168.100.1/ + +reboot: + description: Reboot router. + fields: + url: + description: URL of router to reboot; optional when only one is configured. + example: http://192.168.100.1/ + +resume_integration: + description: Resume suspended integration. + fields: + url: + description: URL of router to resume integration for; optional when only one is configured. + example: http://192.168.100.1/ + +suspend_integration: + description: > + Suspend integration. Suspending logs the integration out from the router, and stops accessing it. + Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. + Invoke the resume_integration service to resume. + fields: + url: + description: URL of router to resume integration for; optional when only one is configured. + example: http://192.168.100.1/ diff --git a/homeassistant/components/huawei_lte/strings.json b/homeassistant/components/huawei_lte/strings.json index 17684253671..c5f2b4a2a02 100644 --- a/homeassistant/components/huawei_lte/strings.json +++ b/homeassistant/components/huawei_lte/strings.json @@ -7,13 +7,13 @@ }, "error": { "connection_failed": "Connection failed", + "connection_timeout": "Connection timeout", "incorrect_password": "Incorrect password", "incorrect_username": "Incorrect username", "incorrect_username_or_password": "Incorrect username or password", "invalid_url": "Invalid URL", "login_attempts_exceeded": "Maximum login attempts exceeded, please try again later", "response_error": "Unknown error from device", - "connection_timeout": "Connection timeout", "unknown_connection_error": "Unknown error connecting to device" }, "step": { @@ -33,6 +33,7 @@ "step": { "init": { "data": { + "name": "Notification service name (change requires restart)", "recipient": "SMS notification recipients", "track_new_devices": "Track new devices" } diff --git a/homeassistant/components/huawei_lte/switch.py b/homeassistant/components/huawei_lte/switch.py index bff82227b80..44d2da0c898 100644 --- a/homeassistant/components/huawei_lte/switch.py +++ b/homeassistant/components/huawei_lte/switch.py @@ -11,10 +11,10 @@ from homeassistant.components.switch import ( SwitchDevice, ) from homeassistant.const import CONF_URL + from . import HuaweiLteBaseEntity from .const import DOMAIN, KEY_DIALUP_MOBILE_DATASWITCH - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/huawei_router/device_tracker.py b/homeassistant/components/huawei_router/device_tracker.py index b7b5731dfd3..be34b26be0d 100644 --- a/homeassistant/components/huawei_router/device_tracker.py +++ b/homeassistant/components/huawei_router/device_tracker.py @@ -1,19 +1,19 @@ """Support for HUAWEI routers.""" import base64 +from collections import namedtuple import logging import re -from collections import namedtuple import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -88,7 +88,7 @@ class HuaweiDeviceScanner(DeviceScanner): _LOGGER.debug( "Active clients: %s", - "\n".join((client.mac + " " + client.name) for client in active_clients), + "\n".join(f"{client.mac} {client.name}" for client in active_clients), ) return True diff --git a/homeassistant/components/huawei_router/manifest.json b/homeassistant/components/huawei_router/manifest.json index 2affcb8ee2e..32e366a5a5a 100644 --- a/homeassistant/components/huawei_router/manifest.json +++ b/homeassistant/components/huawei_router/manifest.json @@ -1,10 +1,8 @@ { "domain": "huawei_router", - "name": "Huawei router", + "name": "Huawei Router", "documentation": "https://www.home-assistant.io/integrations/huawei_router", "requirements": [], "dependencies": [], - "codeowners": [ - "@abmantis" - ] + "codeowners": ["@abmantis"] } diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json index 756f3b7e44b..afcfd7071e7 100644 --- a/homeassistant/components/hue/.translations/da.json +++ b/homeassistant/components/hue/.translations/da.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "all_configured": "Alle Philips Hue brigdes er konfigureret", + "all_configured": "Alle Philips Hue-broer er allerede konfigureret", "already_configured": "Bridgen er allerede konfigureret", - "already_in_progress": "Bro konfiguration er allerede i gang.", + "already_in_progress": "Bro-konfiguration er allerede i gang.", "cannot_connect": "Kunne ikke oprette forbindelse til bridgen", - "discover_timeout": "Ingen Philips Hue bridge fundet", - "no_bridges": "Ingen Philips Hue bridge fundet", - "not_hue_bridge": "Ikke en Hue bro", + "discover_timeout": "Ingen Philips Hue-bro fundet", + "no_bridges": "Ingen Philips Hue-broer fundet", + "not_hue_bridge": "Ikke en Hue-bro", "unknown": "Ukendt fejl opstod" }, "error": { - "linking": "Ukendt sammenkoblings fejl opstod", + "linking": "Der opstod en ukendt linkfejl.", "register_failed": "Det lykkedes ikke at registrere, pr\u00f8v igen" }, "step": { @@ -22,8 +22,8 @@ "title": "V\u00e6lg Hue bridge" }, "link": { - "description": "Tryk p\u00e5 knappen p\u00e5 bridgen for at registrere Philips Hue med Home Assistant. \n\n ! [Placering af knap p\u00e5 bro] (/static/images/config_philips_hue.jpg)", - "title": "Link Hub" + "description": "Tryk p\u00e5 knappen p\u00e5 broen for at registrere Philips Hue med Home Assistant. \n\n ![Placering af knap p\u00e5 bro](/static/images/config_philips_hue.jpg)", + "title": "Forbind Hub" } }, "title": "Philips Hue" diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index c749a498e44..3425cb82d01 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -2,8 +2,8 @@ "config": { "abort": { "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 027ec205195..cbcb21db7d0 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -2,16 +2,14 @@ import ipaddress import logging +from aiohue.util import normalize_bridge_id import voluptuous as vol -from homeassistant import config_entries -from homeassistant.const import CONF_FILENAME, CONF_HOST +from homeassistant import config_entries, core +from homeassistant.const import CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr from .bridge import HueBridge -from .config_flow import ( - configured_hosts, -) # Loading the config flow file will register the flow from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -32,8 +30,6 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema( { # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), - # This is for legacy reasons and is only used for importing auth. - vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, vol.Optional( CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE ): cv.boolean, @@ -65,7 +61,6 @@ async def async_setup(hass, config): hass.data[DOMAIN] = {} hass.data[DATA_CONFIGS] = {} - configured = configured_hosts(hass) # User has configured bridges if CONF_BRIDGES not in conf: @@ -73,36 +68,37 @@ async def async_setup(hass, config): bridges = conf[CONF_BRIDGES] + configured_hosts = set( + entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN) + ) + for bridge_conf in bridges: host = bridge_conf[CONF_HOST] # Store config in hass.data so the config entry can find it hass.data[DATA_CONFIGS][host] = bridge_conf - # If configured, the bridge will be set up during config entry phase - if host in configured: + if host in configured_hosts: continue - # No existing config entry found, try importing it or trigger link - # config flow if no existing auth. Because we're inside the setup of - # this component we'll have to use hass.async_add_job to avoid a - # deadlock: creating a config entry will set up the component but the - # setup would block till the entry is created! + # No existing config entry found, trigger link config flow. Because we're + # inside the setup of this component we'll have to use hass.async_add_job + # to avoid a deadlock: creating a config entry will set up the component + # but the setup would block till the entry is created! hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={ - "host": bridge_conf[CONF_HOST], - "path": bridge_conf[CONF_FILENAME], - }, + data={"host": bridge_conf[CONF_HOST]}, ) ) return True -async def async_setup_entry(hass, entry): +async def async_setup_entry( + hass: core.HomeAssistant, entry: config_entries.ConfigEntry +): """Set up a bridge from a config entry.""" host = entry.data["host"] config = hass.data[DATA_CONFIGS].get(host) @@ -121,6 +117,13 @@ async def async_setup_entry(hass, entry): hass.data[DOMAIN][host] = bridge config = bridge.api.config + + # For backwards compat + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=normalize_bridge_id(config.bridgeid) + ) + device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, @@ -133,9 +136,7 @@ async def async_setup_entry(hass, entry): ) if config.swupdate2_bridge_state == "readytoinstall": - err = ( - "Please check for software updates of the bridge " "in the Philips Hue App." - ) + err = "Please check for software updates of the bridge in the Philips Hue App." _LOGGER.warning(err) return True diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 5a5e55773a5..58a744dd5b0 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -6,6 +6,7 @@ import async_timeout import slugify as unicode_slug import voluptuous as vol +from homeassistant import core from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv @@ -45,8 +46,15 @@ class HueBridge: host = self.host hass = self.hass + bridge = aiohue.Bridge( + host, + username=self.config_entry.data["username"], + websession=aiohttp_client.async_get_clientsession(hass), + ) + try: - self.api = await get_bridge(hass, host, self.config_entry.data["username"]) + await authenticate_bridge(hass, bridge) + except AuthenticationRequired: # Usernames can become invalid if hub is reset or user removed. # We are going to fail the config entry setup and initiate a new @@ -63,6 +71,8 @@ class HueBridge: LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) return False + self.api = bridge + hass.async_create_task( hass.config_entries.async_forward_entry_setup(self.config_entry, "light") ) @@ -175,16 +185,12 @@ class HueBridge: create_config_flow(self.hass, self.host) -async def get_bridge(hass, host, username=None): +async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): """Create a bridge object and verify authentication.""" - bridge = aiohue.Bridge( - host, username=username, websession=aiohttp_client.async_get_clientsession(hass) - ) - try: with async_timeout.timeout(10): # Create username if we don't have one - if not username: + if not bridge.username: device_name = unicode_slug.slugify( hass.config.location_name, max_length=19 ) @@ -193,7 +199,6 @@ async def get_bridge(hass, host, username=None): # Initialize bridge (and validate our username) await bridge.initialize() - return bridge except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): raise AuthenticationRequired except (asyncio.TimeoutError, aiohue.RequestError): diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 375042c8835..66b9c97a58a 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -1,48 +1,23 @@ """Config flow to configure Philips Hue.""" import asyncio -import json -import os +from typing import Dict, Optional +from urllib.parse import urlparse -from aiohue.discovery import discover_nupnp +import aiohue +from aiohue.discovery import discover_nupnp, normalize_bridge_id import async_timeout import voluptuous as vol -from homeassistant import config_entries -from homeassistant.core import callback +from homeassistant import config_entries, core +from homeassistant.components import ssdp from homeassistant.helpers import aiohttp_client -from .bridge import get_bridge -from .const import DOMAIN, LOGGER +from .bridge import authenticate_bridge +from .const import DOMAIN, LOGGER # pylint: disable=unused-import from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" -HUE_IGNORED_BRIDGE_NAMES = ["HASS Bridge", "Espalexa"] - - -@callback -def configured_hosts(hass): - """Return a set of the configured hosts.""" - return set( - entry.data["host"] for entry in hass.config_entries.async_entries(DOMAIN) - ) - - -def _find_username_from_config(hass, filename): - """Load username from config. - - This was a legacy way of configuring Hue until Home Assistant 0.67. - """ - path = hass.config.path(filename) - - if not os.path.isfile(path): - return None - - with open(path) as inp: - try: - return list(json.load(inp).values())[0]["username"] - except ValueError: - # If we get invalid JSON - return None +HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -55,23 +30,45 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Hue flow.""" - self.host = None + self.bridge: Optional[aiohue.Bridge] = None + self.discovered_bridges: Optional[Dict[str, aiohue.Bridge]] = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" + # This is for backwards compatibility. return await self.async_step_init(user_input) + @core.callback + def _async_get_bridge(self, host: str, bridge_id: Optional[str] = None): + """Return a bridge object.""" + if bridge_id is not None: + bridge_id = normalize_bridge_id(bridge_id) + + return aiohue.Bridge( + host, + websession=aiohttp_client.async_get_clientsession(self.hass), + bridge_id=bridge_id, + ) + async def async_step_init(self, user_input=None): """Handle a flow start.""" - if user_input is not None: - self.host = self.context["host"] = user_input["host"] - return await self.async_step_link() - - websession = aiohttp_client.async_get_clientsession(self.hass) + if ( + user_input is not None + and self.discovered_bridges is not None + # pylint: disable=unsupported-membership-test + and user_input["id"] in self.discovered_bridges + ): + # pylint: disable=unsubscriptable-object + self.bridge = self.discovered_bridges[user_input["id"]] + await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) + # We pass user input to link so it will attempt to link right away + return await self.async_step_link({}) try: with async_timeout.timeout(5): - bridges = await discover_nupnp(websession=websession) + bridges = await discover_nupnp( + websession=aiohttp_client.async_get_clientsession(self.hass) + ) except asyncio.TimeoutError: return self.async_abort(reason="discover_timeout") @@ -79,20 +76,28 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="no_bridges") # Find already configured hosts - configured = configured_hosts(self.hass) + already_configured = self._async_current_ids(False) + bridges = [bridge for bridge in bridges if bridge.id not in already_configured] - hosts = [bridge.host for bridge in bridges if bridge.host not in configured] - - if not hosts: + if not bridges: return self.async_abort(reason="all_configured") - if len(hosts) == 1: - self.host = hosts[0] + if len(bridges) == 1: + self.bridge = bridges[0] + await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) return await self.async_step_link() + self.discovered_bridges = {bridge.id: bridge for bridge in bridges} + return self.async_show_form( step_id="init", - data_schema=vol.Schema({vol.Required("host"): vol.In(hosts)}), + data_schema=vol.Schema( + { + vol.Required("id"): vol.In( + {bridge.id: bridge.host for bridge in bridges} + ) + } + ), ) async def async_step_link(self, user_input=None): @@ -101,31 +106,39 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): Given a configured host, will ask the user to press the link button to connect to the bridge. """ + if user_input is None: + return self.async_show_form(step_id="link") + + bridge = self.bridge + assert bridge is not None errors = {} - # We will always try linking in case the user has already pressed - # the link button. try: - bridge = await get_bridge(self.hass, self.host, username=None) + await authenticate_bridge(self.hass, bridge) - return await self._entry_from_bridge(bridge) + # Can happen if we come from import. + if self.unique_id is None: + await self.async_set_unique_id( + normalize_bridge_id(bridge.id), raise_on_progress=False + ) + + return self.async_create_entry( + title=bridge.config.name, + data={"host": bridge.host, "username": bridge.username}, + ) except AuthenticationRequired: errors["base"] = "register_failed" except CannotConnect: - LOGGER.error("Error connecting to the Hue bridge at %s", self.host) + LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host) errors["base"] = "linking" except Exception: # pylint: disable=broad-except LOGGER.exception( - "Unknown error connecting with Hue bridge at %s", self.host + "Unknown error connecting with Hue bridge at %s", bridge.host ) errors["base"] = "linking" - # If there was no user input, do not show the errors. - if user_input is None: - errors = {} - return self.async_show_form(step_id="link", errors=errors) async def async_step_ssdp(self, discovery_info): @@ -134,122 +147,58 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by the SSDP component. It will check if the host is already configured and delegate to the import step if not. """ - from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_NAME - - if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL: + # Filter out non-Hue bridges #1 + if discovery_info.get(ssdp.ATTR_UPNP_MANUFACTURER_URL) != HUE_MANUFACTURERURL: return self.async_abort(reason="not_hue_bridge") + # Filter out non-Hue bridges #2 if any( - name in discovery_info.get(ATTR_NAME, "") + name in discovery_info.get(ssdp.ATTR_UPNP_FRIENDLY_NAME, "") for name in HUE_IGNORED_BRIDGE_NAMES ): return self.async_abort(reason="not_hue_bridge") - host = self.context["host"] = discovery_info.get("host") - - if any( - host == flow["context"].get("host") for flow in self._async_in_progress() + if ( + ssdp.ATTR_SSDP_LOCATION not in discovery_info + or ssdp.ATTR_UPNP_SERIAL not in discovery_info ): - return self.async_abort(reason="already_in_progress") + return self.async_abort(reason="not_hue_bridge") - if host in configured_hosts(self.hass): - return self.async_abort(reason="already_configured") + host = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]).hostname - # This value is based off host/description.xml and is, weirdly, missing - # 4 characters in the middle of the serial compared to results returned - # from the NUPNP API or when querying the bridge API for bridgeid. - # (on first gen Hue hub) - serial = discovery_info.get("serial") + bridge = self._async_get_bridge(host, discovery_info[ssdp.ATTR_UPNP_SERIAL]) - return await self.async_step_import( - { - "host": host, - # This format is the legacy format that Hue used for discovery - "path": f"phue-{serial}.conf", - } - ) + await self.async_set_unique_id(bridge.id) + self._abort_if_unique_id_configured() + self.bridge = bridge + return await self.async_step_link() async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" - host = self.context["host"] = homekit_info.get("host") + bridge = self._async_get_bridge( + homekit_info["host"], homekit_info["properties"]["id"] + ) - if any( - host == flow["context"].get("host") for flow in self._async_in_progress() - ): - return self.async_abort(reason="already_in_progress") - - if host in configured_hosts(self.hass): - return self.async_abort(reason="already_configured") - - return await self.async_step_import({"host": host}) + await self.async_set_unique_id(bridge.id) + self._abort_if_unique_id_configured() + self.bridge = bridge + return await self.async_step_link() async def async_step_import(self, import_info): """Import a new bridge as a config entry. - Will read authentication from Phue config file if available. - This flow is triggered by `async_setup` for both configured and discovered bridges. Triggered for any bridge that does not have a config entry yet (based on host). This flow is also triggered by `async_step_discovery`. - - If an existing config file is found, we will validate the credentials - and create an entry. Otherwise we will delegate to `link` step which - will ask user to link the bridge. """ - host = self.context["host"] = import_info["host"] - path = import_info.get("path") + # Check if host exists, abort if so. + if any( + import_info["host"] == entry.data["host"] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") - if path is not None: - username = await self.hass.async_add_job( - _find_username_from_config, self.hass, self.hass.config.path(path) - ) - else: - username = None - - try: - bridge = await get_bridge(self.hass, host, username) - - LOGGER.info("Imported authentication for %s from %s", host, path) - - return await self._entry_from_bridge(bridge) - except AuthenticationRequired: - self.host = host - - LOGGER.info("Invalid authentication for %s, requesting link.", host) - - return await self.async_step_link() - - except CannotConnect: - LOGGER.error("Error connecting to the Hue bridge at %s", host) - return self.async_abort(reason="cannot_connect") - - except Exception: # pylint: disable=broad-except - LOGGER.exception("Unknown error connecting with Hue bridge at %s", host) - return self.async_abort(reason="unknown") - - async def _entry_from_bridge(self, bridge): - """Return a config entry from an initialized bridge.""" - # Remove all other entries of hubs with same ID or host - host = bridge.host - bridge_id = bridge.config.bridgeid - - same_hub_entries = [ - entry.entry_id - for entry in self.hass.config_entries.async_entries(DOMAIN) - if entry.data["bridge_id"] == bridge_id or entry.data["host"] == host - ] - - if same_hub_entries: - await asyncio.wait( - [ - self.hass.config_entries.async_remove(entry_id) - for entry_id in same_hub_entries - ] - ) - - return self.async_create_entry( - title=bridge.config.name, - data={"host": host, "bridge_id": bridge_id, "username": bridge.username}, - ) + self.bridge = self._async_get_bridge(import_info["host"]) + return await self.async_step_link() diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index af0f996b537..8a5fa973e4f 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -1,7 +1,7 @@ """Helper functions for Philips Hue.""" +from homeassistant import config_entries from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg -from homeassistant import config_entries from .const import DOMAIN diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index ad511639d57..2a668779cb5 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -261,11 +261,13 @@ class HueLight(Light): if is_group: self.is_osram = False self.is_philips = False + self.is_innr = False self.gamut_typ = GAMUT_TYPE_UNAVAILABLE self.gamut = None else: self.is_osram = light.manufacturername == "OSRAM" self.is_philips = light.manufacturername == "Philips" + self.is_innr = light.manufacturername == "innr" self.gamut_typ = self.light.colorgamuttype self.gamut = self.light.colorgamut _LOGGER.debug("Color gamut of %s: %s", self.name, str(self.gamut)) @@ -277,7 +279,7 @@ class HueLight(Light): _LOGGER.warning(err, self.name) if self.gamut: if not color.check_valid_gamut(self.gamut): - err = "Color gamut of %s: %s, not valid, " "setting gamut to None." + err = "Color gamut of %s: %s, not valid, setting gamut to None." _LOGGER.warning(err, self.name, str(self.gamut)) self.gamut_typ = GAMUT_TYPE_UNAVAILABLE self.gamut = None @@ -420,7 +422,7 @@ class HueLight(Light): elif flash == FLASH_SHORT: command["alert"] = "select" del command["on"] - else: + elif not self.is_innr: command["alert"] = "none" if ATTR_EFFECT in kwargs: @@ -453,7 +455,7 @@ class HueLight(Light): elif flash == FLASH_SHORT: command["alert"] = "select" del command["on"] - else: + elif not self.is_innr: command["alert"] = "none" if self.is_group: diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index c90b6181559..f6b14ec69d4 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -3,21 +3,16 @@ "name": "Philips Hue", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hue", - "requirements": [ - "aiohue==1.9.2" - ], + "requirements": ["aiohue==1.10.1"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" } ], "homekit": { - "models": [ - "BSB002" - ] + "models": ["BSB002"] }, "dependencies": [], - "codeowners": [ - "@balloob" - ] + "codeowners": ["@balloob"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/hunterdouglas_powerview/manifest.json b/homeassistant/components/hunterdouglas_powerview/manifest.json index 3aeffd025ee..b72efc431c3 100644 --- a/homeassistant/components/hunterdouglas_powerview/manifest.json +++ b/homeassistant/components/hunterdouglas_powerview/manifest.json @@ -1,10 +1,8 @@ { "domain": "hunterdouglas_powerview", - "name": "Hunterdouglas powerview", + "name": "Hunter Douglas PowerView", "documentation": "https://www.home-assistant.io/integrations/hunterdouglas_powerview", - "requirements": [ - "aiopvapi==1.6.14" - ], + "requirements": ["aiopvapi==1.6.14"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/hunterdouglas_powerview/scene.py b/homeassistant/components/hunterdouglas_powerview/scene.py index 3f2ac79306c..1ee82ce165c 100644 --- a/homeassistant/components/hunterdouglas_powerview/scene.py +++ b/homeassistant/components/hunterdouglas_powerview/scene.py @@ -36,7 +36,7 @@ STATE_ATTRIBUTE_ROOM_NAME = "roomName" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up home assistant scene entries.""" + """Set up Home Assistant scene entries.""" hub_address = config.get(HUB_ADDRESS) websession = async_get_clientsession(hass) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index eaa0d10622a..a9555beaa7b 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -1,10 +1,8 @@ { "domain": "hydrawise", - "name": "Hydrawise", + "name": "Hunter Hydrawise", "documentation": "https://www.home-assistant.io/integrations/hydrawise", - "requirements": [ - "hydrawiser==0.1.1" - ], + "requirements": ["hydrawiser==0.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ialarm/manifest.json b/homeassistant/components/ialarm/manifest.json index 6e575ef17bc..5ddb0a1f907 100644 --- a/homeassistant/components/ialarm/manifest.json +++ b/homeassistant/components/ialarm/manifest.json @@ -1,10 +1,8 @@ { "domain": "ialarm", - "name": "Ialarm", + "name": "Antifurto365 iAlarm", "documentation": "https://www.home-assistant.io/integrations/ialarm", - "requirements": [ - "pyialarm==0.3" - ], + "requirements": ["pyialarm==0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index fc1eb3b248a..16c8deac72e 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -5,8 +5,6 @@ import logging from typing import Any, Dict import aiohttp.client_exceptions -import voluptuous as vol - from iaqualink import ( AqualinkBinarySensor, AqualinkClient, @@ -17,6 +15,7 @@ from iaqualink import ( AqualinkThermostat, AqualinkToggle, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -29,18 +28,17 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv 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" diff --git a/homeassistant/components/iaqualink/binary_sensor.py b/homeassistant/components/iaqualink/binary_sensor.py index 09c9322a587..30d419c1bce 100644 --- a/homeassistant/components/iaqualink/binary_sensor.py +++ b/homeassistant/components/iaqualink/binary_sensor.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_COLD, DOMAIN, + BinarySensorDevice, ) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index f41d17837c2..36f3303774a 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -22,7 +22,7 @@ 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 +from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/iaqualink/config_flow.py b/homeassistant/components/iaqualink/config_flow.py index ec83477d253..d577fe448aa 100644 --- a/homeassistant/components/iaqualink/config_flow.py +++ b/homeassistant/components/iaqualink/config_flow.py @@ -1,9 +1,8 @@ """Config flow to configure zone component.""" from typing import Optional -import voluptuous as vol - from iaqualink import AqualinkClient, AqualinkLoginException +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/homeassistant/components/icloud/.translations/ca.json b/homeassistant/components/icloud/.translations/ca.json new file mode 100644 index 00000000000..30e6c50b81b --- /dev/null +++ b/homeassistant/components/icloud/.translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "El compte ja ha estat configurat" + }, + "error": { + "login": "Error d\u2019inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", + "send_verification_code": "No s'ha pogut enviar el codi de verificaci\u00f3", + "username_exists": "El compte ja ha estat configurat", + "validate_verification_code": "No s'ha pogut verificar el codi de verificaci\u00f3, tria un dispositiu de confian\u00e7a i torna a iniciar el proc\u00e9s" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositiu de confian\u00e7a" + }, + "description": "Selecciona el teu dispositiu de confian\u00e7a", + "title": "Dispositiu de confian\u00e7a iCloud" + }, + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "description": "Introdueix les teves credencials", + "title": "credencials d'iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Codi de verificaci\u00f3" + }, + "description": "Introdueix el codi de verificaci\u00f3 que rebis d'iCloud", + "title": "Codi de verificaci\u00f3 iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/da.json b/homeassistant/components/icloud/.translations/da.json new file mode 100644 index 00000000000..1a06bd8e0f2 --- /dev/null +++ b/homeassistant/components/icloud/.translations/da.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigureret" + }, + "error": { + "login": "Loginfejl: Kontroller din email og adgangskode", + "send_verification_code": "Bekr\u00e6ftelseskoden kunne ikke sendes", + "username_exists": "Kontoen er allerede konfigureret", + "validate_verification_code": "Bekr\u00e6ftelseskoden kunne ikke bekr\u00e6ftes, V\u00e6lg en betroet enhed, og start bekr\u00e6ftelsen igen" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Betroet enhed" + }, + "description": "V\u00e6lg din betroede enhed", + "title": "iCloud-enhed, der er tillid til" + }, + "user": { + "data": { + "password": "Adgangskode", + "username": "Email" + }, + "description": "Indtast dine legitimationsoplysninger", + "title": "iCloud-legitimationsoplysninger" + }, + "verification_code": { + "data": { + "verification_code": "Bekr\u00e6ftelseskode" + }, + "description": "Indtast venligst den bekr\u00e6ftelseskode, du lige har modtaget fra iCloud", + "title": "iCloud-bekr\u00e6ftelseskode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/de.json b/homeassistant/components/icloud/.translations/de.json new file mode 100644 index 00000000000..ac9a401e70d --- /dev/null +++ b/homeassistant/components/icloud/.translations/de.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Konto bereits konfiguriert" + }, + "error": { + "login": "Login-Fehler: Bitte \u00fcberpr\u00fcfen Sie Ihre E-Mail & Passwort", + "send_verification_code": "Fehler beim Senden des Best\u00e4tigungscodes", + "username_exists": "Konto bereits konfiguriert", + "validate_verification_code": "Verifizierung des Verifizierungscodes fehlgeschlagen. W\u00e4hlen Sie ein vertrauensw\u00fcrdiges Ger\u00e4t aus und starten Sie die Verifizierung erneut" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Vertrauensw\u00fcrdiges Ger\u00e4t" + }, + "description": "W\u00e4hlen Sie Ihr vertrauensw\u00fcrdiges Ger\u00e4t aus", + "title": "iCloud vertrauensw\u00fcrdiges Ger\u00e4t" + }, + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail" + }, + "description": "Geben Sie Ihre Zugangsdaten ein", + "title": "iCloud-Anmeldeinformationen" + }, + "verification_code": { + "data": { + "verification_code": "Verifizierungscode" + }, + "description": "Bitte geben Sie den Best\u00e4tigungscode ein, den Sie gerade von iCloud erhalten haben", + "title": "iCloud-Best\u00e4tigungscode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/en.json b/homeassistant/components/icloud/.translations/en.json new file mode 100644 index 00000000000..58101759356 --- /dev/null +++ b/homeassistant/components/icloud/.translations/en.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account already configured" + }, + "error": { + "login": "Login error: please check your email & password", + "send_verification_code": "Failed to send verification code", + "username_exists": "Account already configured", + "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Trusted device" + }, + "description": "Select your trusted device", + "title": "iCloud trusted device" + }, + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "description": "Enter your credentials", + "title": "iCloud credentials" + }, + "verification_code": { + "data": { + "verification_code": "Verification code" + }, + "description": "Please enter the verification code you just received from iCloud", + "title": "iCloud verification code" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/es.json b/homeassistant/components/icloud/.translations/es.json new file mode 100644 index 00000000000..13355fa2b8e --- /dev/null +++ b/homeassistant/components/icloud/.translations/es.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Cuenta ya configurada" + }, + "error": { + "login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a", + "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", + "username_exists": "Cuenta ya configurada", + "validate_verification_code": "No se pudo verificar el c\u00f3digo de verificaci\u00f3n, elegir un dispositivo de confianza e iniciar la verificaci\u00f3n de nuevo" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo de confianza" + }, + "description": "Seleccione su dispositivo de confianza", + "title": "Dispositivo de confianza iCloud" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "description": "Ingrese sus credenciales", + "title": "Credenciales iCloud" + }, + "verification_code": { + "data": { + "verification_code": "C\u00f3digo de verificaci\u00f3n" + }, + "description": "Por favor, introduzca el c\u00f3digo de verificaci\u00f3n que acaba de recibir de iCloud", + "title": "C\u00f3digo de verificaci\u00f3n de iCloud" + } + }, + "title": "iCloud de Apple" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/fr.json b/homeassistant/components/icloud/.translations/fr.json new file mode 100644 index 00000000000..81996d908a6 --- /dev/null +++ b/homeassistant/components/icloud/.translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe", + "send_verification_code": "\u00c9chec de l'envoi du code de v\u00e9rification", + "username_exists": "Compte d\u00e9j\u00e0 configur\u00e9", + "validate_verification_code": "Impossible de v\u00e9rifier votre code de v\u00e9rification, choisissez un appareil de confiance et recommencez la v\u00e9rification" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Appareil de confiance" + }, + "description": "S\u00e9lectionnez votre appareil de confiance", + "title": "Appareil de confiance iCloud" + }, + "user": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "description": "Entrez vos identifiants", + "title": "Identifiants iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Code de v\u00e9rification" + }, + "description": "Veuillez entrer le code de v\u00e9rification que vous venez de recevoir d'iCloud", + "title": "Code de v\u00e9rification iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/it.json b/homeassistant/components/icloud/.translations/it.json new file mode 100644 index 00000000000..0a986f1fe77 --- /dev/null +++ b/homeassistant/components/icloud/.translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account gi\u00e0 configurato" + }, + "error": { + "login": "Errore di accesso: si prega di controllare la tua e-mail e la password", + "send_verification_code": "Impossibile inviare il codice di verifica", + "username_exists": "Account gi\u00e0 configurato", + "validate_verification_code": "Impossibile verificare il codice di verifica, scegliere un dispositivo attendibile e riavviare la verifica" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo attendibile" + }, + "description": "Selezionare il dispositivo attendibile", + "title": "Dispositivo attendibile iCloud" + }, + "user": { + "data": { + "password": "Password", + "username": "E-mail" + }, + "description": "Inserisci le tue credenziali", + "title": "Credenziali iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Codice di verifica" + }, + "description": "Inserisci il codice di verifica che hai appena ricevuto da iCloud", + "title": "Codice di verifica iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ja.json b/homeassistant/components/icloud/.translations/ja.json new file mode 100644 index 00000000000..f5c3be53639 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ja.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" + }, + "description": "\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9\u3092\u9078\u629e\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "iCloud \u306e\u4fe1\u983c\u3067\u304d\u308b\u30c7\u30d0\u30a4\u30b9" + }, + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" + }, + "description": "\u8cc7\u683c\u60c5\u5831\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044", + "title": "iCloud \u306e\u8cc7\u683c\u60c5\u5831" + }, + "verification_code": { + "data": { + "verification_code": "\u8a8d\u8a3c\u30b3\u30fc\u30c9" + }, + "title": "iCloud \u306e\u8a8d\u8a3c\u30b3\u30fc\u30c9" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ko.json b/homeassistant/components/icloud/.translations/ko.json new file mode 100644 index 00000000000..a689a895278 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", + "send_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ubcf4\ub0b4\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "username_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "validate_verification_code": "\uc778\uc99d \ucf54\ub4dc\ub97c \ud655\uc778\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud558\uace0 \uc778\uc99d\uc744 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30" + }, + "description": "\uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uac00 \uc2e0\ub8b0\ud560 \uc218 \uc788\ub294 \uae30\uae30" + }, + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + }, + "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uc790\uaca9 \uc99d\uba85" + }, + "verification_code": { + "data": { + "verification_code": "\uc778\uc99d \ucf54\ub4dc" + }, + "description": "iCloud \uc5d0\uc11c \ubc1b\uc740 \uc778\uc99d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "iCloud \uc778\uc99d \ucf54\ub4dc" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/lb.json b/homeassistant/components/icloud/.translations/lb.json new file mode 100644 index 00000000000..eaeb300f7a8 --- /dev/null +++ b/homeassistant/components/icloud/.translations/lb.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert", + "send_verification_code": "Feeler beim sch\u00e9cken vum Verifikatiouns Code", + "username_exists": "Kont ass scho konfigur\u00e9iert", + "validate_verification_code": "Feeler beim iwwerpr\u00e9iwe vum Verifikatiouns Code, wielt ee vertrauten Apparat aus a start d'Iwwerpr\u00e9iwung nei" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Vertrauten Apparat" + }, + "description": "Wielt \u00e4ren vertrauten Apparat aus", + "title": "iCloud vertrauten Apparat" + }, + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus", + "title": "iCloud Umeldungs Informatiounen" + }, + "verification_code": { + "data": { + "verification_code": "Verifikatiouns Code" + }, + "description": "Gitt de Verifikatiouns Code an deen dir elo grad vun iCloud kritt hutt", + "title": "iCloud Verifikatiouns Code" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/nl.json b/homeassistant/components/icloud/.translations/nl.json new file mode 100644 index 00000000000..d35496b171b --- /dev/null +++ b/homeassistant/components/icloud/.translations/nl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Account reeds geconfigureerd" + }, + "error": { + "login": "Aanmeldingsfout: controleer uw e-mailadres en wachtwoord", + "send_verification_code": "Kan verificatiecode niet verzenden", + "username_exists": "Account reeds geconfigureerd", + "validate_verification_code": "Kan uw verificatiecode niet verifi\u00ebren, kies een vertrouwensapparaat en start de verificatie opnieuw" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Vertrouwd apparaat" + }, + "description": "Selecteer uw vertrouwde apparaat", + "title": "iCloud vertrouwd apparaat" + }, + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + }, + "description": "Voer uw gegevens in", + "title": "iCloud inloggegevens" + }, + "verification_code": { + "data": { + "verification_code": "Verificatiecode" + }, + "description": "Voer de verificatiecode in die u zojuist van iCloud hebt ontvangen", + "title": "iCloud verificatiecode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/no.json b/homeassistant/components/icloud/.translations/no.json new file mode 100644 index 00000000000..a582b916310 --- /dev/null +++ b/homeassistant/components/icloud/.translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Kontoen er allerede konfigurert" + }, + "error": { + "login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt", + "send_verification_code": "Kunne ikke sende bekreftelseskode", + "username_exists": "Kontoen er allerede konfigurert", + "validate_verification_code": "Kunne ikke bekrefte bekreftelseskoden din, velg en tillitsenhet og start bekreftelsen p\u00e5 nytt" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "P\u00e5litelig enhet" + }, + "description": "Velg den p\u00e5litelige enheten din", + "title": "iCloud p\u00e5litelig enhet" + }, + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "description": "Angi legitimasjonsbeskrivelsen", + "title": "iCloud-legitimasjon" + }, + "verification_code": { + "data": { + "verification_code": "iCloud-bekreftelseskode" + }, + "description": "Vennligst skriv inn bekreftelseskoden du nettopp har f\u00e5tt fra iCloud", + "title": "iCloud-bekreftelseskode" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json new file mode 100644 index 00000000000..f154f77f186 --- /dev/null +++ b/homeassistant/components/icloud/.translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o", + "send_verification_code": "Nie uda\u0142o si\u0119 wys\u0142a\u0107 kodu weryfikacyjnego", + "username_exists": "Konto jest ju\u017c skonfigurowane", + "validate_verification_code": "Nie uda\u0142o si\u0119 zweryfikowa\u0107 kodu weryfikacyjnego, wybierz urz\u0105dzenie zaufane i ponownie rozpocznij weryfikacj\u0119" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Zaufane urz\u0105dzenie" + }, + "description": "Wybierz zaufane urz\u0105dzenie", + "title": "Zaufane urz\u0105dzenie iCloud" + }, + "user": { + "data": { + "password": "Has\u0142o", + "username": "E-mail" + }, + "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", + "title": "Dane uwierzytelniaj\u0105ce iCloud" + }, + "verification_code": { + "data": { + "verification_code": "Kod weryfikacyjny" + }, + "description": "Wprowad\u017a kod weryfikacyjny, kt\u00f3ry w\u0142a\u015bnie otrzyma\u0142e\u015b z iCloud", + "title": "Kod weryfikacyjny iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/pt-BR.json b/homeassistant/components/icloud/.translations/pt-BR.json new file mode 100644 index 00000000000..c8bfe0e0a6d --- /dev/null +++ b/homeassistant/components/icloud/.translations/pt-BR.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Conta j\u00e1 configurada" + }, + "error": { + "login": "Erro de login: verifique seu e-mail e senha", + "send_verification_code": "Falha ao enviar c\u00f3digo de verifica\u00e7\u00e3o", + "username_exists": "Conta j\u00e1 configurada", + "validate_verification_code": "Falha ao verificar seu c\u00f3digo de verifica\u00e7\u00e3o, escolha um dispositivo confi\u00e1vel e inicie a verifica\u00e7\u00e3o novamente" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo confi\u00e1vel" + }, + "description": "Selecione seu dispositivo confi\u00e1vel", + "title": "Dispositivo confi\u00e1vel iCloud" + }, + "user": { + "data": { + "password": "Senha", + "username": "Email" + }, + "description": "Insira suas credenciais", + "title": "credenciais do iCloud" + }, + "verification_code": { + "data": { + "verification_code": "C\u00f3digo de verifica\u00e7\u00e3o" + }, + "description": "Digite o c\u00f3digo de verifica\u00e7\u00e3o que voc\u00ea acabou de receber do iCloud", + "title": "c\u00f3digo de verifica\u00e7\u00e3o do iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/ru.json b/homeassistant/components/icloud/.translations/ru.json new file mode 100644 index 00000000000..000edd71e00 --- /dev/null +++ b/homeassistant/components/icloud/.translations/ru.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "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.", + "send_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f.", + "username_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "validate_verification_code": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0438 \u043d\u0430\u0447\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u043d\u043e\u0432\u0430." + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u0414\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0434\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", + "title": "\u0414\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e iCloud" + }, + "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\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "title": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 iCloud" + }, + "verification_code": { + "data": { + "verification_code": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f, \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044b\u0439 \u043e\u0442 iCloud", + "title": "\u041a\u043e\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f iCloud" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/sl.json b/homeassistant/components/icloud/.translations/sl.json new file mode 100644 index 00000000000..91cb4312cb3 --- /dev/null +++ b/homeassistant/components/icloud/.translations/sl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "Ra\u010dun \u017ee nastavljen" + }, + "error": { + "login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo", + "send_verification_code": "Kode za preverjanje ni bilo mogo\u010de poslati", + "username_exists": "Ra\u010dun \u017ee nastavljen", + "validate_verification_code": "Kode za preverjanje ni bilo mogo\u010de preveriti, izberi napravo za zaupanje in znova za\u017eeni preverjanje" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Zaupanja vredna naprava" + }, + "description": "Izberite svojo zaupanja vredno napravo", + "title": "iCloud zaupanja vredna naprava" + }, + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "description": "Vnesite svoje poverilnice", + "title": "iCloud poverilnice" + }, + "verification_code": { + "data": { + "verification_code": "Koda za preverjanje" + }, + "description": "Prosimo, vnesite kodo za preverjanje, ki ste jo pravkar prejeli od iCloud", + "title": "iCloud koda za preverjanje" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/sv.json b/homeassistant/components/icloud/.translations/sv.json new file mode 100644 index 00000000000..8c4c45f9c89 --- /dev/null +++ b/homeassistant/components/icloud/.translations/sv.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/zh-Hant.json b/homeassistant/components/icloud/.translations/zh-Hant.json new file mode 100644 index 00000000000..80d8ba1485b --- /dev/null +++ b/homeassistant/components/icloud/.translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027", + "send_verification_code": "\u50b3\u9001\u9a57\u8b49\u78bc\u5931\u6557", + "username_exists": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "validate_verification_code": "\u7121\u6cd5\u9a57\u8b49\u8f38\u5165\u9a57\u8b49\u78bc\uff0c\u9078\u64c7\u4e00\u90e8\u4fe1\u4efb\u8a2d\u5099\u3001\u7136\u5f8c\u91cd\u65b0\u57f7\u884c\u9a57\u8b49\u3002" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "\u4fe1\u4efb\u8a2d\u5099" + }, + "description": "\u9078\u64c7\u4fe1\u4efb\u8a2d\u5099", + "title": "iCloud \u4fe1\u4efb\u8a2d\u5099" + }, + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "description": "\u8f38\u5165\u6191\u8b49", + "title": "iCloud \u6191\u8b49" + }, + "verification_code": { + "data": { + "verification_code": "\u9a57\u8b49\u78bc" + }, + "description": "\u8acb\u8f38\u5165\u6240\u6536\u5230\u7684 iCloud \u9a57\u8b49\u78bc", + "title": "iCloud \u9a57\u8b49\u78bc" + } + }, + "title": "Apple iCloud" + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/__init__.py b/homeassistant/components/icloud/__init__.py index 1169104c99d..e983f5fac22 100644 --- a/homeassistant/components/icloud/__init__.py +++ b/homeassistant/components/icloud/__init__.py @@ -1 +1,601 @@ -"""The icloud component.""" +"""The iCloud component.""" +from datetime import timedelta +import logging +import operator +from typing import Dict + +from pyicloud import PyiCloudService +from pyicloud.exceptions import PyiCloudFailedLoginException, PyiCloudNoDevicesException +from pyicloud.services.findmyiphone import AppleDevice +import voluptuous as vol + +from homeassistant.components.zone import async_active_zone +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType +from homeassistant.util import slugify +from homeassistant.util.async_ import run_callback_threadsafe +from homeassistant.util.dt import utcnow +from homeassistant.util.location import distance + +from .const import ( + CONF_ACCOUNT_NAME, + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, + DEVICE_BATTERY_LEVEL, + DEVICE_BATTERY_STATUS, + DEVICE_CLASS, + DEVICE_DISPLAY_NAME, + DEVICE_ID, + DEVICE_LOCATION, + DEVICE_LOCATION_LATITUDE, + DEVICE_LOCATION_LONGITUDE, + DEVICE_LOST_MODE_CAPABLE, + DEVICE_LOW_POWER_MODE, + DEVICE_NAME, + DEVICE_PERSON_ID, + DEVICE_RAW_DEVICE_MODEL, + DEVICE_STATUS, + DEVICE_STATUS_CODES, + DEVICE_STATUS_SET, + DOMAIN, + ICLOUD_COMPONENTS, + STORAGE_KEY, + STORAGE_VERSION, + TRACKER_UPDATE, +) + +ATTRIBUTION = "Data provided by Apple iCloud" + +# entity attributes +ATTR_ACCOUNT_FETCH_INTERVAL = "account_fetch_interval" +ATTR_BATTERY = "battery" +ATTR_BATTERY_STATUS = "battery_status" +ATTR_DEVICE_NAME = "device_name" +ATTR_DEVICE_STATUS = "device_status" +ATTR_LOW_POWER_MODE = "low_power_mode" +ATTR_OWNER_NAME = "owner_fullname" + +# services +SERVICE_ICLOUD_PLAY_SOUND = "play_sound" +SERVICE_ICLOUD_DISPLAY_MESSAGE = "display_message" +SERVICE_ICLOUD_LOST_DEVICE = "lost_device" +SERVICE_ICLOUD_UPDATE = "update" +ATTR_ACCOUNT = "account" +ATTR_LOST_DEVICE_MESSAGE = "message" +ATTR_LOST_DEVICE_NUMBER = "number" +ATTR_LOST_DEVICE_SOUND = "sound" + +SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ACCOUNT): cv.string}) + +SERVICE_SCHEMA_PLAY_SOUND = vol.Schema( + {vol.Required(ATTR_ACCOUNT): cv.string, vol.Required(ATTR_DEVICE_NAME): cv.string} +) + +SERVICE_SCHEMA_DISPLAY_MESSAGE = vol.Schema( + { + vol.Required(ATTR_ACCOUNT): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, + vol.Optional(ATTR_LOST_DEVICE_SOUND): cv.boolean, + } +) + +SERVICE_SCHEMA_LOST_DEVICE = vol.Schema( + { + vol.Required(ATTR_ACCOUNT): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_LOST_DEVICE_NUMBER): cv.string, + vol.Required(ATTR_LOST_DEVICE_MESSAGE): cv.string, + } +) + +ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_ACCOUNT_NAME): cv.string, + vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int, + vol.Optional( + CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD + ): cv.positive_int, + } +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))}, + extra=vol.ALLOW_EXTRA, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: + """Set up iCloud from legacy config file.""" + + conf = config.get(DOMAIN) + if conf is None: + return True + + for account_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=account_conf + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: + """Set up an iCloud account from a config entry.""" + + hass.data.setdefault(DOMAIN, {}) + + username = entry.data[CONF_USERNAME] + password = entry.data[CONF_PASSWORD] + account_name = entry.data.get(CONF_ACCOUNT_NAME) + max_interval = entry.data[CONF_MAX_INTERVAL] + gps_accuracy_threshold = entry.data[CONF_GPS_ACCURACY_THRESHOLD] + + icloud_dir = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + account = IcloudAccount( + hass, + username, + password, + icloud_dir, + account_name, + max_interval, + gps_accuracy_threshold, + ) + await hass.async_add_executor_job(account.setup) + hass.data[DOMAIN][username] = account + + for component in ICLOUD_COMPONENTS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + def play_sound(service: ServiceDataType) -> None: + """Play sound on the device.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + + for device in _get_account(account).get_devices_with_name(device_name): + device.play_sound() + + def display_message(service: ServiceDataType) -> None: + """Display a message on the device.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) + sound = service.data.get(ATTR_LOST_DEVICE_SOUND, False) + + for device in _get_account(account).get_devices_with_name(device_name): + device.display_message(message, sound) + + def lost_device(service: ServiceDataType) -> None: + """Make the device in lost state.""" + account = service.data[ATTR_ACCOUNT] + device_name = service.data.get(ATTR_DEVICE_NAME) + device_name = slugify(device_name.replace(" ", "", 99)) + number = service.data.get(ATTR_LOST_DEVICE_NUMBER) + message = service.data.get(ATTR_LOST_DEVICE_MESSAGE) + + for device in _get_account(account).get_devices_with_name(device_name): + device.lost_device(number, message) + + def update_account(service: ServiceDataType) -> None: + """Call the update function of an iCloud account.""" + account = service.data.get(ATTR_ACCOUNT) + + if account is None: + for account in hass.data[DOMAIN].values(): + account.keep_alive() + else: + _get_account(account).keep_alive() + + def _get_account(account_identifier: str) -> any: + if account_identifier is None: + return None + + icloud_account = hass.data[DOMAIN].get(account_identifier, None) + if icloud_account is None: + for account in hass.data[DOMAIN].values(): + if account.name == account_identifier: + icloud_account = account + + if icloud_account is None: + raise Exception( + f"No iCloud account with username or name {account_identifier}" + ) + return icloud_account + + hass.services.async_register( + DOMAIN, SERVICE_ICLOUD_PLAY_SOUND, play_sound, schema=SERVICE_SCHEMA_PLAY_SOUND + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ICLOUD_DISPLAY_MESSAGE, + display_message, + schema=SERVICE_SCHEMA_DISPLAY_MESSAGE, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_ICLOUD_LOST_DEVICE, + lost_device, + schema=SERVICE_SCHEMA_LOST_DEVICE, + ) + + hass.services.async_register( + DOMAIN, SERVICE_ICLOUD_UPDATE, update_account, schema=SERVICE_SCHEMA + ) + + return True + + +class IcloudAccount: + """Representation of an iCloud account.""" + + def __init__( + self, + hass: HomeAssistantType, + username: str, + password: str, + icloud_dir: Store, + account_name: str, + max_interval: int, + gps_accuracy_threshold: int, + ): + """Initialize an iCloud account.""" + self.hass = hass + self._username = username + self._password = password + self._name = account_name or slugify(username.partition("@")[0]) + self._fetch_interval = max_interval + self._max_interval = max_interval + self._gps_accuracy_threshold = gps_accuracy_threshold + + self._icloud_dir = icloud_dir + + self.api = None + self._owner_fullname = None + self._family_members_fullname = {} + self._devices = {} + + self.unsub_device_tracker = None + + def setup(self): + """Set up an iCloud account.""" + try: + self.api = PyiCloudService( + self._username, self._password, self._icloud_dir.path + ) + except PyiCloudFailedLoginException as error: + self.api = None + _LOGGER.error("Error logging into iCloud Service: %s", error) + return + + user_info = None + try: + # Gets device owners infos + user_info = self.api.devices.response["userInfo"] + except PyiCloudNoDevicesException: + _LOGGER.error("No iCloud Devices found") + + self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" + + self._family_members_fullname = {} + for prs_id, member in user_info["membersInfo"].items(): + self._family_members_fullname[ + prs_id + ] = f"{member['firstName']} {member['lastName']}" + + self._devices = {} + self.update_devices() + + def update_devices(self) -> None: + """Update iCloud devices.""" + if self.api is None: + return + + api_devices = {} + try: + api_devices = self.api.devices + except PyiCloudNoDevicesException: + _LOGGER.error("No iCloud Devices found") + + # Gets devices infos + for device in api_devices: + status = device.status(DEVICE_STATUS_SET) + device_id = status[DEVICE_ID] + device_name = status[DEVICE_NAME] + + if self._devices.get(device_id, None) is not None: + # Seen device -> updating + _LOGGER.debug("Updating iCloud device: %s", device_name) + self._devices[device_id].update(status) + else: + # New device, should be unique + _LOGGER.debug( + "Adding iCloud device: %s [model: %s]", + device_name, + status[DEVICE_RAW_DEVICE_MODEL], + ) + self._devices[device_id] = IcloudDevice(self, device, status) + self._devices[device_id].update(status) + + dispatcher_send(self.hass, TRACKER_UPDATE) + self._fetch_interval = self._determine_interval() + track_point_in_utc_time( + self.hass, + self.keep_alive, + utcnow() + timedelta(minutes=self._fetch_interval), + ) + + def _determine_interval(self) -> int: + """Calculate new interval between two API fetch (in minutes).""" + intervals = {} + for device in self._devices.values(): + if device.location is None: + continue + + current_zone = run_callback_threadsafe( + self.hass.loop, + async_active_zone, + self.hass, + device.location[DEVICE_LOCATION_LATITUDE], + device.location[DEVICE_LOCATION_LONGITUDE], + ).result() + + if current_zone is not None: + intervals[device.name] = self._max_interval + continue + + zones = ( + self.hass.states.get(entity_id) + for entity_id in sorted(self.hass.states.entity_ids("zone")) + ) + + distances = [] + for zone_state in zones: + zone_state_lat = zone_state.attributes[DEVICE_LOCATION_LATITUDE] + zone_state_long = zone_state.attributes[DEVICE_LOCATION_LONGITUDE] + zone_distance = distance( + device.location[DEVICE_LOCATION_LATITUDE], + device.location[DEVICE_LOCATION_LONGITUDE], + zone_state_lat, + zone_state_long, + ) + distances.append(round(zone_distance / 1000, 1)) + + if not distances: + continue + mindistance = min(distances) + + # Calculate out how long it would take for the device to drive + # to the nearest zone at 120 km/h: + interval = round(mindistance / 2, 0) + + # Never poll more than once per minute + interval = max(interval, 1) + + if interval > 180: + # Three hour drive? + # This is far enough that they might be flying + interval = self._max_interval + + if ( + device.battery_level is not None + and device.battery_level <= 33 + and mindistance > 3 + ): + # Low battery - let's check half as often + interval = interval * 2 + + intervals[device.name] = interval + + return max( + int(min(intervals.items(), key=operator.itemgetter(1))[1]), + self._max_interval, + ) + + def keep_alive(self, now=None) -> None: + """Keep the API alive.""" + if self.api is None: + self.setup() + + if self.api is None: + return + + self.api.authenticate() + self.update_devices() + + def get_devices_with_name(self, name: str) -> [any]: + """Get devices by name.""" + result = [] + name_slug = slugify(name.replace(" ", "", 99)) + for device in self.devices.values(): + if slugify(device.name.replace(" ", "", 99)) == name_slug: + result.append(device) + if not result: + raise Exception(f"No device with name {name}") + return result + + @property + def name(self) -> str: + """Return the account name.""" + return self._name + + @property + def username(self) -> str: + """Return the account username.""" + return self._username + + @property + def owner_fullname(self) -> str: + """Return the account owner fullname.""" + return self._owner_fullname + + @property + def family_members_fullname(self) -> Dict[str, str]: + """Return the account family members fullname.""" + return self._family_members_fullname + + @property + def fetch_interval(self) -> int: + """Return the account fetch interval.""" + return self._fetch_interval + + @property + def devices(self) -> Dict[str, any]: + """Return the account devices.""" + return self._devices + + +class IcloudDevice: + """Representation of a iCloud device.""" + + def __init__(self, account: IcloudAccount, device: AppleDevice, status): + """Initialize the iCloud device.""" + self._account = account + account_name = account.name + + self._device = device + self._status = status + + self._name = self._status[DEVICE_NAME] + self._device_id = self._status[DEVICE_ID] + self._device_class = self._status[DEVICE_CLASS] + self._device_model = self._status[DEVICE_DISPLAY_NAME] + + if self._status[DEVICE_PERSON_ID]: + owner_fullname = account.family_members_fullname[ + self._status[DEVICE_PERSON_ID] + ] + else: + owner_fullname = account.owner_fullname + + self._battery_level = None + self._battery_status = None + self._location = None + + self._attrs = { + ATTR_ATTRIBUTION: ATTRIBUTION, + CONF_ACCOUNT_NAME: account_name, + ATTR_ACCOUNT_FETCH_INTERVAL: self._account.fetch_interval, + ATTR_DEVICE_NAME: self._device_model, + ATTR_DEVICE_STATUS: None, + ATTR_OWNER_NAME: owner_fullname, + } + + def update(self, status) -> None: + """Update the iCloud device.""" + self._status = status + + self._status[ATTR_ACCOUNT_FETCH_INTERVAL] = self._account.fetch_interval + + device_status = DEVICE_STATUS_CODES.get(self._status[DEVICE_STATUS], "error") + self._attrs[ATTR_DEVICE_STATUS] = device_status + + if self._status[DEVICE_BATTERY_STATUS] != "Unknown": + self._battery_level = int(self._status.get(DEVICE_BATTERY_LEVEL, 0) * 100) + self._battery_status = self._status[DEVICE_BATTERY_STATUS] + low_power_mode = self._status[DEVICE_LOW_POWER_MODE] + + self._attrs[ATTR_BATTERY] = self._battery_level + self._attrs[ATTR_BATTERY_STATUS] = self._battery_status + self._attrs[ATTR_LOW_POWER_MODE] = low_power_mode + + if ( + self._status[DEVICE_LOCATION] + and self._status[DEVICE_LOCATION][DEVICE_LOCATION_LATITUDE] + ): + location = self._status[DEVICE_LOCATION] + self._location = location + + def play_sound(self) -> None: + """Play sound on the device.""" + if self._account.api is None: + return + + self._account.api.authenticate() + _LOGGER.debug("Playing sound for %s", self.name) + self.device.play_sound() + + def display_message(self, message: str, sound: bool = False) -> None: + """Display a message on the device.""" + if self._account.api is None: + return + + self._account.api.authenticate() + _LOGGER.debug("Displaying message for %s", self.name) + self.device.display_message("Subject not working", message, sound) + + def lost_device(self, number: str, message: str) -> None: + """Make the device in lost state.""" + if self._account.api is None: + return + + self._account.api.authenticate() + if self._status[DEVICE_LOST_MODE_CAPABLE]: + _LOGGER.debug("Make device lost for %s", self.name) + self.device.lost_device(number, message, None) + else: + _LOGGER.error("Cannot make device lost for %s", self.name) + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device_id + + @property + def name(self) -> str: + """Return the Apple device name.""" + return self._name + + @property + def device(self) -> AppleDevice: + """Return the Apple device.""" + return self._device + + @property + def device_class(self) -> str: + """Return the Apple device class.""" + return self._device_class + + @property + def device_model(self) -> str: + """Return the Apple device model.""" + return self._device_model + + @property + def battery_level(self) -> int: + """Return the Apple device battery level.""" + return self._battery_level + + @property + def battery_status(self) -> str: + """Return the Apple device battery status.""" + return self._battery_status + + @property + def location(self) -> Dict[str, any]: + """Return the Apple device location.""" + return self._location + + @property + def state_attributes(self) -> Dict[str, any]: + """Return the attributes.""" + return self._attrs diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py new file mode 100644 index 00000000000..cf05c07e26f --- /dev/null +++ b/homeassistant/components/icloud/config_flow.py @@ -0,0 +1,230 @@ +"""Config flow to configure the iCloud integration.""" +import logging +import os + +from pyicloud import PyiCloudService +from pyicloud.exceptions import PyiCloudException, PyiCloudFailedLoginException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.util import slugify + +from .const import ( + CONF_ACCOUNT_NAME, + CONF_GPS_ACCURACY_THRESHOLD, + CONF_MAX_INTERVAL, + DEFAULT_GPS_ACCURACY_THRESHOLD, + DEFAULT_MAX_INTERVAL, + STORAGE_KEY, + STORAGE_VERSION, +) +from .const import DOMAIN # pylint: disable=unused-import + +CONF_TRUSTED_DEVICE = "trusted_device" +CONF_VERIFICATION_CODE = "verification_code" + +_LOGGER = logging.getLogger(__name__) + + +class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a iCloud config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize iCloud config flow.""" + self.api = None + self._username = None + self._password = None + self._account_name = None + self._max_interval = None + self._gps_accuracy_threshold = None + + self._trusted_device = None + self._verification_code = None + + def _configuration_exists(self, username: str, account_name: str) -> bool: + """Return True if username or account_name exists in configuration.""" + for entry in self._async_current_entries(): + if ( + entry.data[CONF_USERNAME] == username + or entry.data.get(CONF_ACCOUNT_NAME) == account_name + or slugify(entry.data[CONF_USERNAME].partition("@")[0]) == account_name + ): + return True + return False + + async 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 = {} + + icloud_dir = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + + if not os.path.exists(icloud_dir.path): + await self.hass.async_add_executor_job(os.makedirs, icloud_dir.path) + + if user_input is None: + return await self._show_setup_form(user_input, errors) + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + self._account_name = user_input.get(CONF_ACCOUNT_NAME) + self._max_interval = user_input.get(CONF_MAX_INTERVAL, DEFAULT_MAX_INTERVAL) + self._gps_accuracy_threshold = user_input.get( + CONF_GPS_ACCURACY_THRESHOLD, DEFAULT_GPS_ACCURACY_THRESHOLD + ) + + if self._configuration_exists(self._username, self._account_name): + errors[CONF_USERNAME] = "username_exists" + return await self._show_setup_form(user_input, errors) + + try: + self.api = await self.hass.async_add_executor_job( + PyiCloudService, self._username, self._password, icloud_dir.path + ) + except PyiCloudFailedLoginException as error: + _LOGGER.error("Error logging into iCloud service: %s", error) + self.api = None + errors[CONF_USERNAME] = "login" + return await self._show_setup_form(user_input, errors) + + if self.api.requires_2fa: + return await self.async_step_trusted_device() + + return self.async_create_entry( + title=self._username, + data={ + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_ACCOUNT_NAME: self._account_name, + CONF_MAX_INTERVAL: self._max_interval, + CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold, + }, + ) + + async def async_step_import(self, user_input): + """Import a config entry.""" + if self._configuration_exists( + user_input[CONF_USERNAME], user_input.get(CONF_ACCOUNT_NAME) + ): + return self.async_abort(reason="username_exists") + + return await self.async_step_user(user_input) + + async def async_step_trusted_device(self, user_input=None, errors=None): + """We need a trusted device.""" + if errors is None: + errors = {} + + trusted_devices = await self.hass.async_add_executor_job( + getattr, self.api, "trusted_devices" + ) + trusted_devices_for_form = {} + for i, device in enumerate(trusted_devices): + trusted_devices_for_form[i] = device.get( + "deviceName", f"SMS to {device.get('phoneNumber')}" + ) + + if user_input is None: + return await self._show_trusted_device_form( + trusted_devices_for_form, user_input, errors + ) + + self._trusted_device = trusted_devices[int(user_input[CONF_TRUSTED_DEVICE])] + + if not await self.hass.async_add_executor_job( + self.api.send_verification_code, self._trusted_device + ): + _LOGGER.error("Failed to send verification code") + self._trusted_device = None + errors[CONF_TRUSTED_DEVICE] = "send_verification_code" + + return await self._show_trusted_device_form( + trusted_devices_for_form, user_input, errors + ) + + return await self.async_step_verification_code() + + async def _show_trusted_device_form( + self, trusted_devices, user_input=None, errors=None + ): + """Show the trusted_device form to the user.""" + + return self.async_show_form( + step_id=CONF_TRUSTED_DEVICE, + data_schema=vol.Schema( + { + vol.Required(CONF_TRUSTED_DEVICE): vol.All( + vol.Coerce(int), vol.In(trusted_devices) + ) + } + ), + errors=errors or {}, + ) + + async def async_step_verification_code(self, user_input=None): + """Ask the verification code to the user.""" + errors = {} + + if user_input is None: + return await self._show_verification_code_form(user_input) + + self._verification_code = user_input[CONF_VERIFICATION_CODE] + + try: + if not await self.hass.async_add_executor_job( + self.api.validate_verification_code, + self._trusted_device, + self._verification_code, + ): + raise PyiCloudException("The code you entered is not valid.") + except PyiCloudException as error: + # Reset to the initial 2FA state to allow the user to retry + _LOGGER.error("Failed to verify verification code: %s", error) + self._trusted_device = None + self._verification_code = None + errors["base"] = "validate_verification_code" + + return await self.async_step_trusted_device(None, errors) + + return await self.async_step_user( + { + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_ACCOUNT_NAME: self._account_name, + CONF_MAX_INTERVAL: self._max_interval, + CONF_GPS_ACCURACY_THRESHOLD: self._gps_accuracy_threshold, + } + ) + + async def _show_verification_code_form(self, user_input=None): + """Show the verification_code form to the user.""" + + return self.async_show_form( + step_id=CONF_VERIFICATION_CODE, + data_schema=vol.Schema({vol.Required(CONF_VERIFICATION_CODE): str}), + errors=None, + ) diff --git a/homeassistant/components/icloud/const.py b/homeassistant/components/icloud/const.py index fe8010df703..ed2fc78fe6d 100644 --- a/homeassistant/components/icloud/const.py +++ b/homeassistant/components/icloud/const.py @@ -1,6 +1,84 @@ -"""Constants for the iCloud component.""" +"""iCloud component constants.""" + DOMAIN = "icloud" -SERVICE_LOST_IPHONE = "lost_iphone" -SERVICE_UPDATE = "update" -SERVICE_RESET_ACCOUNT = "reset_account" -SERVICE_SET_INTERVAL = "set_interval" +TRACKER_UPDATE = f"{DOMAIN}_tracker_update" + +CONF_ACCOUNT_NAME = "account_name" +CONF_MAX_INTERVAL = "max_interval" +CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" + +DEFAULT_MAX_INTERVAL = 30 # min +DEFAULT_GPS_ACCURACY_THRESHOLD = 500 # meters + +# to store the cookie +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +ICLOUD_COMPONENTS = ["device_tracker", "sensor"] + +# pyicloud.AppleDevice status +DEVICE_BATTERY_LEVEL = "batteryLevel" +DEVICE_BATTERY_STATUS = "batteryStatus" +DEVICE_CLASS = "deviceClass" +DEVICE_DISPLAY_NAME = "deviceDisplayName" +DEVICE_ID = "id" +DEVICE_LOCATION = "location" +DEVICE_LOCATION_HORIZONTAL_ACCURACY = "horizontalAccuracy" +DEVICE_LOCATION_LATITUDE = "latitude" +DEVICE_LOCATION_LONGITUDE = "longitude" +DEVICE_LOST_MODE_CAPABLE = "lostModeCapable" +DEVICE_LOW_POWER_MODE = "lowPowerMode" +DEVICE_NAME = "name" +DEVICE_PERSON_ID = "prsId" +DEVICE_RAW_DEVICE_MODEL = "rawDeviceModel" +DEVICE_STATUS = "deviceStatus" + +DEVICE_STATUS_SET = [ + "features", + "maxMsgChar", + "darkWake", + "fmlyShare", + DEVICE_STATUS, + "remoteLock", + "activationLocked", + DEVICE_CLASS, + DEVICE_ID, + "deviceModel", + DEVICE_RAW_DEVICE_MODEL, + "passcodeLength", + "canWipeAfterLock", + "trackingInfo", + DEVICE_LOCATION, + "msg", + DEVICE_BATTERY_LEVEL, + "remoteWipe", + "thisDevice", + "snd", + DEVICE_PERSON_ID, + "wipeInProgress", + DEVICE_LOW_POWER_MODE, + "lostModeEnabled", + "isLocating", + DEVICE_LOST_MODE_CAPABLE, + "mesg", + DEVICE_NAME, + DEVICE_BATTERY_STATUS, + "lockedTimestamp", + "lostTimestamp", + "locationCapable", + DEVICE_DISPLAY_NAME, + "lostDevice", + "deviceColor", + "wipedTimestamp", + "modelDisplayName", + "locationEnabled", + "isMac", + "locFoundEnabled", +] + +DEVICE_STATUS_CODES = { + "200": "online", + "201": "offline", + "203": "pending", + "204": "unregistered", +} diff --git a/homeassistant/components/icloud/device_tracker.py b/homeassistant/components/icloud/device_tracker.py index 3d9fb4715da..511ce7f9447 100644 --- a/homeassistant/components/icloud/device_tracker.py +++ b/homeassistant/components/icloud/device_tracker.py @@ -1,544 +1,136 @@ -"""Platform that supports scanning iCloud.""" +"""Support for tracking for iCloud devices.""" import logging -import os -import random +from typing import Dict -from pyicloud import PyiCloudService -from pyicloud.exceptions import ( - PyiCloudException, - PyiCloudFailedLoginException, - PyiCloudNoDevicesException, -) -import voluptuous as vol - -from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.components.device_tracker.const import ( - ATTR_ATTRIBUTES, - ENTITY_ID_FORMAT, -) -from homeassistant.components.device_tracker.legacy import DeviceScanner -from homeassistant.components.zone import async_active_zone -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_utc_time_change -from homeassistant.util import slugify -from homeassistant.util.async_ import run_callback_threadsafe -import homeassistant.util.dt as dt_util -from homeassistant.util.location import distance +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType +from . import IcloudDevice from .const import ( + DEVICE_LOCATION_HORIZONTAL_ACCURACY, + DEVICE_LOCATION_LATITUDE, + DEVICE_LOCATION_LONGITUDE, DOMAIN, - SERVICE_LOST_IPHONE, - SERVICE_RESET_ACCOUNT, - SERVICE_SET_INTERVAL, - SERVICE_UPDATE, + TRACKER_UPDATE, ) _LOGGER = logging.getLogger(__name__) -CONF_ACCOUNTNAME = "account_name" -CONF_MAX_INTERVAL = "max_interval" -CONF_GPS_ACCURACY_THRESHOLD = "gps_accuracy_threshold" -# entity attributes -ATTR_ACCOUNTNAME = "account_name" -ATTR_INTERVAL = "interval" -ATTR_DEVICENAME = "device_name" -ATTR_BATTERY = "battery" -ATTR_DISTANCE = "distance" -ATTR_DEVICESTATUS = "device_status" -ATTR_LOWPOWERMODE = "low_power_mode" -ATTR_BATTERYSTATUS = "battery_status" - -ICLOUDTRACKERS = {} - -_CONFIGURING = {} - -DEVICESTATUSSET = [ - "features", - "maxMsgChar", - "darkWake", - "fmlyShare", - "deviceStatus", - "remoteLock", - "activationLocked", - "deviceClass", - "id", - "deviceModel", - "rawDeviceModel", - "passcodeLength", - "canWipeAfterLock", - "trackingInfo", - "location", - "msg", - "batteryLevel", - "remoteWipe", - "thisDevice", - "snd", - "prsId", - "wipeInProgress", - "lowPowerMode", - "lostModeEnabled", - "isLocating", - "lostModeCapable", - "mesg", - "name", - "batteryStatus", - "lockedTimestamp", - "lostTimestamp", - "locationCapable", - "deviceDisplayName", - "lostDevice", - "deviceColor", - "wipedTimestamp", - "modelDisplayName", - "locationEnabled", - "isMac", - "locFoundEnabled", -] - -DEVICESTATUSCODES = { - "200": "online", - "201": "offline", - "203": "pending", - "204": "unregistered", -} - -SERVICE_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ACCOUNTNAME): vol.All(cv.ensure_list, [cv.slugify]), - vol.Optional(ATTR_DEVICENAME): cv.slugify, - vol.Optional(ATTR_INTERVAL): cv.positive_int, - } -) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(ATTR_ACCOUNTNAME): cv.slugify, - vol.Optional(CONF_MAX_INTERVAL, default=30): cv.positive_int, - vol.Optional(CONF_GPS_ACCURACY_THRESHOLD, default=1000): cv.positive_int, - } -) +async def async_setup_scanner( + hass: HomeAssistantType, config, see, discovery_info=None +): + """Old way of setting up the iCloud tracker.""" + pass -def setup_scanner(hass, config: dict, see, discovery_info=None): - """Set up the iCloud Scanner.""" - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - account = config.get(CONF_ACCOUNTNAME, slugify(username.partition("@")[0])) - max_interval = config.get(CONF_MAX_INTERVAL) - gps_accuracy_threshold = config.get(CONF_GPS_ACCURACY_THRESHOLD) +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +): + """Configure a dispatcher connection based on a config entry.""" + username = entry.data[CONF_USERNAME] - icloudaccount = Icloud( - hass, username, password, account, max_interval, gps_accuracy_threshold, see - ) + for device in hass.data[DOMAIN][username].devices.values(): + if device.location is None: + _LOGGER.debug("No position found for %s", device.name) + continue - if icloudaccount.api is not None: - ICLOUDTRACKERS[account] = icloudaccount + _LOGGER.debug("Adding device_tracker for %s", device.name) - else: - _LOGGER.error("No ICLOUDTRACKERS added") + async_add_entities([IcloudTrackerEntity(device)]) + + +class IcloudTrackerEntity(TrackerEntity): + """Represent a tracked device.""" + + def __init__(self, device: IcloudDevice): + """Set up the iCloud tracker entity.""" + self._device = device + self._unsub_dispatcher = None + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._device.unique_id + + @property + def name(self) -> str: + """Return the name of the device.""" + return self._device.name + + @property + def location_accuracy(self): + """Return the location accuracy of the device.""" + return self._device.location[DEVICE_LOCATION_HORIZONTAL_ACCURACY] + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._device.location[DEVICE_LOCATION_LATITUDE] + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._device.location[DEVICE_LOCATION_LONGITUDE] + + @property + def should_poll(self) -> bool: + """No polling needed.""" return False - def lost_iphone(call): - """Call the lost iPhone function if the device is found.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].lost_iphone(devicename) + @property + def battery_level(self) -> int: + """Return the battery level of the device.""" + return self._device.battery_level - hass.services.register( - DOMAIN, SERVICE_LOST_IPHONE, lost_iphone, schema=SERVICE_SCHEMA - ) + @property + def source_type(self) -> str: + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS - def update_icloud(call): - """Call the update function of an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].update_icloud(devicename) + @property + def icon(self) -> str: + """Return the icon.""" + return icon_for_icloud_device(self._device) - hass.services.register(DOMAIN, SERVICE_UPDATE, update_icloud, schema=SERVICE_SCHEMA) + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return the device state attributes.""" + return self._device.state_attributes - def reset_account_icloud(call): - """Reset an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].reset_account_icloud() + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._device.unique_id)}, + "name": self._device.name, + "manufacturer": "Apple", + "model": self._device.device_model, + } - hass.services.register( - DOMAIN, SERVICE_RESET_ACCOUNT, reset_account_icloud, schema=SERVICE_SCHEMA - ) - - def setinterval(call): - """Call the update function of an iCloud account.""" - accounts = call.data.get(ATTR_ACCOUNTNAME, ICLOUDTRACKERS) - interval = call.data.get(ATTR_INTERVAL) - devicename = call.data.get(ATTR_DEVICENAME) - for account in accounts: - if account in ICLOUDTRACKERS: - ICLOUDTRACKERS[account].setinterval(interval, devicename) - - hass.services.register( - DOMAIN, SERVICE_SET_INTERVAL, setinterval, schema=SERVICE_SCHEMA - ) - - # Tells the bootstrapper that the component was successfully initialized - return True - - -class Icloud(DeviceScanner): - """Representation of an iCloud account.""" - - def __init__( - self, hass, username, password, name, max_interval, gps_accuracy_threshold, see - ): - """Initialize an iCloud account.""" - self.hass = hass - self.username = username - self.password = password - self.api = None - self.accountname = name - self.devices = {} - self.seen_devices = {} - self._overridestates = {} - self._intervals = {} - self._max_interval = max_interval - self._gps_accuracy_threshold = gps_accuracy_threshold - self.see = see - - self._trusted_device = None - self._verification_code = None - - self._attrs = {} - self._attrs[ATTR_ACCOUNTNAME] = name - - self.reset_account_icloud() - - randomseconds = random.randint(10, 59) - track_utc_time_change(self.hass, self.keep_alive, second=randomseconds) - - def reset_account_icloud(self): - """Reset an iCloud account.""" - icloud_dir = self.hass.config.path("icloud") - if not os.path.exists(icloud_dir): - os.makedirs(icloud_dir) - - try: - self.api = PyiCloudService( - self.username, self.password, cookie_directory=icloud_dir, verify=True - ) - except PyiCloudFailedLoginException as error: - self.api = None - _LOGGER.error("Error logging into iCloud Service: %s", error) - return - - try: - self.devices = {} - self._overridestates = {} - self._intervals = {} - for device in self.api.devices: - status = device.status(DEVICESTATUSSET) - _LOGGER.debug("Device Status is %s", status) - devicename = slugify(status["name"].replace(" ", "", 99)) - _LOGGER.info("Adding icloud device: %s", devicename) - if devicename in self.devices: - _LOGGER.error("Multiple devices with name: %s", devicename) - continue - self.devices[devicename] = device - self._intervals[devicename] = 1 - self._overridestates[devicename] = None - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found!") - - def icloud_trusted_device_callback(self, callback_data): - """Handle chosen trusted devices.""" - self._trusted_device = int(callback_data.get("trusted_device")) - self._trusted_device = self.api.trusted_devices[self._trusted_device] - - if not self.api.send_verification_code(self._trusted_device): - _LOGGER.error("Failed to send verification code") - self._trusted_device = None - return - - if self.accountname in _CONFIGURING: - request_id = _CONFIGURING.pop(self.accountname) - configurator = self.hass.components.configurator - configurator.request_done(request_id) - - # Trigger the next step immediately - self.icloud_need_verification_code() - - def icloud_need_trusted_device(self): - """We need a trusted device.""" - configurator = self.hass.components.configurator - if self.accountname in _CONFIGURING: - return - - devicesstring = "" - devices = self.api.trusted_devices - for i, device in enumerate(devices): - devicename = device.get( - "deviceName", "SMS to %s" % device.get("phoneNumber") - ) - devicesstring += f"{i}: {devicename};" - - _CONFIGURING[self.accountname] = configurator.request_config( - f"iCloud {self.accountname}", - self.icloud_trusted_device_callback, - description=( - "Please choose your trusted device by entering" - " the index from this list: " + devicesstring - ), - entity_picture="/static/images/config_icloud.png", - submit_caption="Confirm", - fields=[{"id": "trusted_device", "name": "Trusted Device"}], + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self.async_write_ha_state ) - def icloud_verification_callback(self, callback_data): - """Handle the chosen trusted device.""" - self._verification_code = callback_data.get("code") + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() - try: - if not self.api.validate_verification_code( - self._trusted_device, self._verification_code - ): - raise PyiCloudException("Unknown failure") - except PyiCloudException as error: - # Reset to the initial 2FA state to allow the user to retry - _LOGGER.error("Failed to verify verification code: %s", error) - self._trusted_device = None - self._verification_code = None - # Trigger the next step immediately - self.icloud_need_trusted_device() +def icon_for_icloud_device(icloud_device: IcloudDevice) -> str: + """Return a battery icon valid identifier.""" + switcher = { + "iPad": "mdi:tablet-ipad", + "iPhone": "mdi:cellphone-iphone", + "iPod": "mdi:ipod", + "iMac": "mdi:desktop-mac", + "MacBookPro": "mdi:laptop-mac", + } - if self.accountname in _CONFIGURING: - request_id = _CONFIGURING.pop(self.accountname) - configurator = self.hass.components.configurator - configurator.request_done(request_id) - - def icloud_need_verification_code(self): - """Return the verification code.""" - configurator = self.hass.components.configurator - if self.accountname in _CONFIGURING: - return - - _CONFIGURING[self.accountname] = configurator.request_config( - f"iCloud {self.accountname}", - self.icloud_verification_callback, - description=("Please enter the validation code:"), - entity_picture="/static/images/config_icloud.png", - submit_caption="Confirm", - fields=[{"id": "code", "name": "code"}], - ) - - def keep_alive(self, now): - """Keep the API alive.""" - if self.api is None: - self.reset_account_icloud() - - if self.api is None: - return - - if self.api.requires_2fa: - try: - if self._trusted_device is None: - self.icloud_need_trusted_device() - return - - if self._verification_code is None: - self.icloud_need_verification_code() - return - - self.api.authenticate() - if self.api.requires_2fa: - raise Exception("Unknown failure") - - self._trusted_device = None - self._verification_code = None - except PyiCloudException as error: - _LOGGER.error("Error setting up 2FA: %s", error) - else: - self.api.authenticate() - - currentminutes = dt_util.now().hour * 60 + dt_util.now().minute - try: - for devicename in self.devices: - interval = self._intervals.get(devicename, 1) - if (currentminutes % interval == 0) or ( - interval > 10 and currentminutes % interval in [2, 4] - ): - self.update_device(devicename) - except ValueError: - _LOGGER.debug("iCloud API returned an error") - - def determine_interval(self, devicename, latitude, longitude, battery): - """Calculate new interval.""" - currentzone = run_callback_threadsafe( - self.hass.loop, async_active_zone, self.hass, latitude, longitude - ).result() - - if ( - currentzone is not None - and currentzone == self._overridestates.get(devicename) - ) or (currentzone is None and self._overridestates.get(devicename) == "away"): - return - - zones = ( - self.hass.states.get(entity_id) - for entity_id in sorted(self.hass.states.entity_ids("zone")) - ) - - distances = [] - for zone_state in zones: - zone_state_lat = zone_state.attributes["latitude"] - zone_state_long = zone_state.attributes["longitude"] - zone_distance = distance( - latitude, longitude, zone_state_lat, zone_state_long - ) - distances.append(round(zone_distance / 1000, 1)) - - if distances: - mindistance = min(distances) - else: - mindistance = None - - self._overridestates[devicename] = None - - if currentzone is not None: - self._intervals[devicename] = self._max_interval - return - - if mindistance is None: - return - - # Calculate out how long it would take for the device to drive to the - # nearest zone at 120 km/h: - interval = round(mindistance / 2, 0) - - # Never poll more than once per minute - interval = max(interval, 1) - - if interval > 180: - # Three hour drive? This is far enough that they might be flying - interval = 30 - - if battery is not None and battery <= 33 and mindistance > 3: - # Low battery - let's check half as often - interval = interval * 2 - - self._intervals[devicename] = interval - - def update_device(self, devicename): - """Update the device_tracker entity.""" - # An entity will not be created by see() when track=false in - # 'known_devices.yaml', but we need to see() it at least once - entity = self.hass.states.get(ENTITY_ID_FORMAT.format(devicename)) - if entity is None and devicename in self.seen_devices: - return - attrs = {} - kwargs = {} - - if self.api is None: - return - - try: - for device in self.api.devices: - if str(device) != str(self.devices[devicename]): - continue - - status = device.status(DEVICESTATUSSET) - _LOGGER.debug("Device Status is %s", status) - dev_id = status["name"].replace(" ", "", 99) - dev_id = slugify(dev_id) - attrs[ATTR_DEVICESTATUS] = DEVICESTATUSCODES.get( - status["deviceStatus"], "error" - ) - attrs[ATTR_LOWPOWERMODE] = status["lowPowerMode"] - attrs[ATTR_BATTERYSTATUS] = status["batteryStatus"] - attrs[ATTR_ACCOUNTNAME] = self.accountname - status = device.status(DEVICESTATUSSET) - battery = status.get("batteryLevel", 0) * 100 - location = status["location"] - if location and location["horizontalAccuracy"]: - horizontal_accuracy = int(location["horizontalAccuracy"]) - if horizontal_accuracy < self._gps_accuracy_threshold: - self.determine_interval( - devicename, - location["latitude"], - location["longitude"], - battery, - ) - interval = self._intervals.get(devicename, 1) - attrs[ATTR_INTERVAL] = interval - accuracy = location["horizontalAccuracy"] - kwargs["dev_id"] = dev_id - kwargs["host_name"] = status["name"] - kwargs["gps"] = (location["latitude"], location["longitude"]) - kwargs["battery"] = battery - kwargs["gps_accuracy"] = accuracy - kwargs[ATTR_ATTRIBUTES] = attrs - self.see(**kwargs) - self.seen_devices[devicename] = True - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") - - def lost_iphone(self, devicename): - """Call the lost iPhone function if the device is found.""" - if self.api is None: - return - - self.api.authenticate() - for device in self.api.devices: - if str(device) == str(self.devices[devicename]): - _LOGGER.info("Playing Lost iPhone sound for %s", devicename) - device.play_sound() - - def update_icloud(self, devicename=None): - """Request device information from iCloud and update device_tracker.""" - if self.api is None: - return - - try: - if devicename is not None: - if devicename in self.devices: - self.update_device(devicename) - else: - _LOGGER.error( - "devicename %s unknown for account %s", - devicename, - self._attrs[ATTR_ACCOUNTNAME], - ) - else: - for device in self.devices: - self.update_device(device) - except PyiCloudNoDevicesException: - _LOGGER.error("No iCloud Devices found") - - def setinterval(self, interval=None, devicename=None): - """Set the interval of the given devices.""" - devs = [devicename] if devicename else self.devices - for device in devs: - devid = f"{DOMAIN}.{device}" - devicestate = self.hass.states.get(devid) - if interval is not None: - if devicestate is not None: - self._overridestates[device] = run_callback_threadsafe( - self.hass.loop, - async_active_zone, - self.hass, - float(devicestate.attributes.get("latitude", 0)), - float(devicestate.attributes.get("longitude", 0)), - ).result() - if self._overridestates[device] is None: - self._overridestates[device] = "away" - self._intervals[device] = interval - else: - self._overridestates[device] = None - self.update_device(device) + return switcher.get(icloud_device.device_class, "mdi:cellphone-link") diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index d3924ee61a8..9652ef10469 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -1,10 +1,9 @@ { "domain": "icloud", - "name": "Icloud", + "name": "Apple iCloud", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": [ - "pyicloud==0.9.1" - ], - "dependencies": ["configurator"], - "codeowners": [] + "requirements": ["pyicloud==0.9.1"], + "dependencies": [], + "codeowners": ["@Quentame"] } diff --git a/homeassistant/components/icloud/sensor.py b/homeassistant/components/icloud/sensor.py new file mode 100644 index 00000000000..4351d4ffa19 --- /dev/null +++ b/homeassistant/components/icloud/sensor.py @@ -0,0 +1,85 @@ +"""Support for iCloud sensors.""" +import logging +from typing import Dict + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_USERNAME, DEVICE_CLASS_BATTERY +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.icon import icon_for_battery_level +from homeassistant.helpers.typing import HomeAssistantType + +from . import IcloudDevice +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up iCloud devices sensors based on a config entry.""" + username = entry.data[CONF_USERNAME] + + entities = [] + for device in hass.data[DOMAIN][username].devices.values(): + if device.battery_level is not None: + _LOGGER.debug("Adding battery sensor for %s", device.name) + entities.append(IcloudDeviceBatterySensor(device)) + + async_add_entities(entities, True) + + +class IcloudDeviceBatterySensor(Entity): + """Representation of a iCloud device battery sensor.""" + + def __init__(self, device: IcloudDevice): + """Initialize the battery sensor.""" + self._device = device + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return f"{self._device.unique_id}_battery" + + @property + def name(self) -> str: + """Sensor name.""" + return f"{self._device.name} battery state" + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def state(self) -> int: + """Battery state percentage.""" + return self._device.battery_level + + @property + def unit_of_measurement(self) -> str: + """Battery state measured in percentage.""" + return "%" + + @property + def icon(self) -> str: + """Battery state icon handling.""" + return icon_for_battery_level( + battery_level=self._device.battery_level, + charging=self._device.battery_status == "Charging", + ) + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return default attributes for the iCloud device entity.""" + return self._device.state_attributes + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "identifiers": {(DOMAIN, self._device.unique_id)}, + "name": self._device.name, + "manufacturer": "Apple", + "model": self._device.device_model, + } diff --git a/homeassistant/components/icloud/services.yaml b/homeassistant/components/icloud/services.yaml index 7b2d3b80e84..ce239df7564 100644 --- a/homeassistant/components/icloud/services.yaml +++ b/homeassistant/components/icloud/services.yaml @@ -1,39 +1,49 @@ -lost_iphone: - description: Service to play the lost iphone sound on an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will play the sound. This is optional, if it isn't given it will play on all devices for the given account. - example: 'iphonebart' - -set_interval: - description: Service to set the interval of an iDevice. - fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will get a new interval. This is optional, if it isn't given it will change the interval for all devices for the given account. - example: 'iphonebart' - interval: - description: The interval (in minutes) that the iDevice will have until the according device_tracker entity changes from zone or until this service is used again. This is optional, if it isn't given the interval of the device will revert back to the original interval based on the current state. - example: 1 - update: - description: Service to ask for an update of an iDevice. + description: Update iCloud devices. fields: - account_name: - description: Name of the account in the config that will be used to look for the device. This is optional, if it isn't given it will use all accounts. - example: 'bart' - device_name: - description: Name of the device that will be updated. This is optional, if it isn't given it will update all devices for the given account. - example: 'iphonebart' + account: + description: Your iCloud account username (email) or account name. + example: 'steve@apple.com' -reset_account: - description: Service to restart an iCloud account. Helpful when not all devices are found after initializing or when you add a new device. +play_sound: + description: Play sound on an Apple device. fields: - account_name: - description: Name of the account in the config that will be restarted. This is optional, if it isn't given it will restart all accounts. - example: 'bart' + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' + device_name: + description: (required) The name of the Apple device to play a sound. + example: 'stevesiphone' + +display_message: + description: Display a message on an Apple device. + fields: + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' + device_name: + description: (required) The name of the Apple device to display the message. + example: 'stevesiphone' + message: + description: (required) The content of your message. + example: 'Hey Steve !' + sound: + description: To make a sound when displaying the message (boolean). + example: 'true' + +lost_device: + description: Make an Apple device in lost state. + fields: + account: + description: (required) Your iCloud account username (email) or account name. + example: 'steve@apple.com' + device_name: + description: (required) The name of the Apple device to set lost. + example: 'stevesiphone' + number: + description: (required) The phone number to call in lost mode (must contain country code). + example: '+33450020100' + message: + description: (required) The message to display in lost mode. + example: 'Call me' + diff --git a/homeassistant/components/icloud/strings.json b/homeassistant/components/icloud/strings.json new file mode 100644 index 00000000000..117e26c8830 --- /dev/null +++ b/homeassistant/components/icloud/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Apple iCloud", + "step": { + "user": { + "title": "iCloud credentials", + "description": "Enter your credentials", + "data": { + "username": "Email", + "password": "Password" + } + }, + "trusted_device": { + "title": "iCloud trusted device", + "description": "Select your trusted device", + "data": { + "trusted_device": "Trusted device" + } + }, + "verification_code": { + "title": "iCloud verification code", + "description": "Please enter the verification code you just received from iCloud", + "data": { + "verification_code": "Verification code" + } + } + }, + "error": { + "username_exists": "Account already configured", + "login": "Login error: please check your email & password", + "send_verification_code": "Failed to send verification code", + "validate_verification_code": "Failed to verify your verification code, choose a trust device and start the verification again" + }, + "abort": { + "username_exists": "Account already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/idteck_prox/__init__.py b/homeassistant/components/idteck_prox/__init__.py index 9cc4f3de9d6..46b3ff8e7f8 100644 --- a/homeassistant/components/idteck_prox/__init__.py +++ b/homeassistant/components/idteck_prox/__init__.py @@ -73,7 +73,7 @@ class IdteckReader: self._connection = rfk101py(self._host, self._port, self._callback) def _callback(self, card): - """Send a keycard event message into HASS whenever a card is read.""" + """Send a keycard event message into Home Assistant whenever a card is read.""" self.hass.bus.fire( EVENT_IDTECK_PROX_KEYCARD, {"card": card, "name": self._name} ) diff --git a/homeassistant/components/idteck_prox/manifest.json b/homeassistant/components/idteck_prox/manifest.json index 7a8e3955686..a82cfc50263 100644 --- a/homeassistant/components/idteck_prox/manifest.json +++ b/homeassistant/components/idteck_prox/manifest.json @@ -1,10 +1,8 @@ { "domain": "idteck_prox", - "name": "Idteck prox", + "name": "IDTECK Proximity Reader", "documentation": "https://www.home-assistant.io/integrations/idteck_prox", - "requirements": [ - "rfk101py==0.0.1" - ], + "requirements": ["rfk101py==0.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ifttt/.translations/da.json b/homeassistant/components/ifttt/.translations/da.json index 25c502ed05e..0e0c735eb89 100644 --- a/homeassistant/components/ifttt/.translations/da.json +++ b/homeassistant/components/ifttt/.translations/da.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage IFTTT meddelelser.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage IFTTT-meddelelser", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du bruge handlingen \"Foretag en web foresp\u00f8rgsel\" fra [IFTTT Webhook applet] ({applet_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du bruge handlingen \"Foretag en web-foresp\u00f8rgsel\" fra [IFTTT Webhook-applet] ({applet_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { - "description": "Er du sikker p\u00e5 at du vil oprette IFTTT?", + "description": "Er du sikker p\u00e5, at du vil konfigurere IFTTT?", "title": "Konfigurer IFTTT Webhook Applet" } }, diff --git a/homeassistant/components/ifttt/.translations/ko.json b/homeassistant/components/ifttt/.translations/ko.json index 75bdd0d99c8..9c8083a1d94 100644 --- a/homeassistant/components/ifttt/.translations/ko.json +++ b/homeassistant/components/ifttt/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "IFTTT \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "IFTTT \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "IFTTT Webhook \uc560\ud50c\ub9bf \uc124\uc815" } }, diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index 362b01bb5d8..3011f5a2a0a 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow import homeassistant.helpers.config_validation as cv + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ifttt/alarm_control_panel.py b/homeassistant/components/ifttt/alarm_control_panel.py index 9c9ec88ccc7..2c281e58c48 100644 --- a/homeassistant/components/ifttt/alarm_control_panel.py +++ b/homeassistant/components/ifttt/alarm_control_panel.py @@ -5,11 +5,11 @@ import re import voluptuous as vol from homeassistant.components.alarm_control_panel import ( - AlarmControlPanel, FORMAT_NUMBER, FORMAT_TEXT, + PLATFORM_SCHEMA, + AlarmControlPanel, ) -from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, SUPPORT_ALARM_ARM_HOME, diff --git a/homeassistant/components/ifttt/config_flow.py b/homeassistant/components/ifttt/config_flow.py index ae9be6b698c..dc28f6bbaa2 100644 --- a/homeassistant/components/ifttt/config_flow.py +++ b/homeassistant/components/ifttt/config_flow.py @@ -1,7 +1,7 @@ """Config flow for IFTTT.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/ifttt/manifest.json b/homeassistant/components/ifttt/manifest.json index 975a3128f36..5dff164d640 100644 --- a/homeassistant/components/ifttt/manifest.json +++ b/homeassistant/components/ifttt/manifest.json @@ -1,13 +1,9 @@ { "domain": "ifttt", - "name": "Ifttt", + "name": "IFTTT", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ifttt", - "requirements": [ - "pyfttt==0.3" - ], - "dependencies": [ - "webhook" - ], + "requirements": ["pyfttt==0.3"], + "dependencies": ["webhook"], "codeowners": [] } diff --git a/homeassistant/components/iglo/manifest.json b/homeassistant/components/iglo/manifest.json index 8771ada45e1..5263d5db8bc 100644 --- a/homeassistant/components/iglo/manifest.json +++ b/homeassistant/components/iglo/manifest.json @@ -1,10 +1,8 @@ { "domain": "iglo", - "name": "Iglo", + "name": "iGlo", "documentation": "https://www.home-assistant.io/integrations/iglo", - "requirements": [ - "iglo==1.2.7" - ], + "requirements": ["iglo==1.2.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 8ad045c9f7a..deecc389e7e 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from typing import Optional +from georss_ign_sismologia_client import IgnSismologiaFeedManager import voluptuous as vol from homeassistant.components.geo_location import PLATFORM_SCHEMA, GeolocationEvent @@ -87,7 +88,6 @@ class IgnSismologiaFeedEntityManager: minimum_magnitude, ): """Initialize the Feed Entity Manager.""" - from georss_ign_sismologia_client import IgnSismologiaFeedManager self._hass = hass self._feed_manager = IgnSismologiaFeedManager( diff --git a/homeassistant/components/ign_sismologia/manifest.json b/homeassistant/components/ign_sismologia/manifest.json index edb77f1dc6d..3a969e7fa3f 100644 --- a/homeassistant/components/ign_sismologia/manifest.json +++ b/homeassistant/components/ign_sismologia/manifest.json @@ -1,12 +1,8 @@ { "domain": "ign_sismologia", - "name": "IGN Sismologia", + "name": "IGN Sismología", "documentation": "https://www.home-assistant.io/integrations/ign_sismologia", - "requirements": [ - "georss_ign_sismologia_client==0.2" - ], + "requirements": ["georss_ign_sismologia_client==0.2"], "dependencies": [], - "codeowners": [ - "@exxamalte" - ] -} \ No newline at end of file + "codeowners": ["@exxamalte"] +} diff --git a/homeassistant/components/ihc/ihc_auto_setup.yaml b/homeassistant/components/ihc/ihc_auto_setup.yaml index 0495ed58458..6b5003a04b5 100644 --- a/homeassistant/components/ihc/ihc_auto_setup.yaml +++ b/homeassistant/components/ihc/ihc_auto_setup.yaml @@ -1,5 +1,5 @@ # IHC auto setup configuration. -# To customize this, copy this file to the home assistant configuration +# To customize this, copy this file to the Home Assistant configuration # folder and make your changes. binary_sensor: diff --git a/homeassistant/components/ihc/manifest.json b/homeassistant/components/ihc/manifest.json index a415b0e3103..4c5ab49c83e 100644 --- a/homeassistant/components/ihc/manifest.json +++ b/homeassistant/components/ihc/manifest.json @@ -1,11 +1,8 @@ { "domain": "ihc", - "name": "Ihc", + "name": "IHC Controller", "documentation": "https://www.home-assistant.io/integrations/ihc", - "requirements": [ - "defusedxml==0.6.0", - "ihcsdk==2.3.0" - ], + "requirements": ["defusedxml==0.6.0", "ihcsdk==2.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 1e9f2963a38..e50ea8d25fd 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -1,6 +1,6 @@ { "domain": "image_processing", - "name": "Image processing", + "name": "Image Processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "requirements": [], "dependencies": ["camera"], diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 20767a0d49d..c861588771e 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -1,10 +1,8 @@ { "domain": "imap", - "name": "Imap", + "name": "IMAP", "documentation": "https://www.home-assistant.io/integrations/imap", - "requirements": [ - "aioimaplib==0.7.15" - ], + "requirements": ["aioimaplib==0.7.15"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index db2f528153b..ceef8acf7c3 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -162,7 +162,7 @@ class ImapSensor(Entity): self._email_count = len(lines[0].split()) else: _LOGGER.error( - "Can't parse IMAP server response to search " "'%s': %s / %s", + "Can't parse IMAP server response to search '%s': %s / %s", self._search, result, lines[0], diff --git a/homeassistant/components/imap_email_content/manifest.json b/homeassistant/components/imap_email_content/manifest.json index e689cb859da..c11d6f49edb 100644 --- a/homeassistant/components/imap_email_content/manifest.json +++ b/homeassistant/components/imap_email_content/manifest.json @@ -1,6 +1,6 @@ { "domain": "imap_email_content", - "name": "Imap email content", + "name": "IMAP Email Content", "documentation": "https://www.home-assistant.io/integrations/imap_email_content", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index 62dceae0dad..307d5a22c1e 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -1,24 +1,24 @@ """Email sensor support.""" -import logging +from collections import deque import datetime import email -from collections import deque - import imaplib +import logging + import voluptuous as vol -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_DATE, CONF_NAME, + CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - CONF_PASSWORD, CONF_VALUE_TEMPLATE, CONTENT_TYPE_TEXT_PLAIN, - ATTR_DATE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/incomfort/manifest.json b/homeassistant/components/incomfort/manifest.json index 45365c7e354..d69c916bda3 100644 --- a/homeassistant/components/incomfort/manifest.json +++ b/homeassistant/components/incomfort/manifest.json @@ -2,11 +2,7 @@ "domain": "incomfort", "name": "Intergas InComfort/Intouch Lan2RF gateway", "documentation": "https://www.home-assistant.io/integrations/incomfort", - "requirements": [ - "incomfort-client==0.4.0" - ], + "requirements": ["incomfort-client==0.4.0"], "dependencies": [], - "codeowners": [ - "@zxdavb" - ] + "codeowners": ["@zxdavb"] } diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 86d489621ea..48852f27910 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -1,11 +1,12 @@ """Support for sending data to an Influx database.""" import logging -import re +import math import queue +import re import threading import time -import math +from influxdb import InfluxDBClient, exceptions import requests.exceptions import voluptuous as vol @@ -20,12 +21,12 @@ from homeassistant.const import ( CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, STATE_UNAVAILABLE, STATE_UNKNOWN, ) -from homeassistant.helpers import state as state_helper, event as event_helper +from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues @@ -118,7 +119,6 @@ RE_DECIMAL = re.compile(r"[^\d.]+") def setup(hass, config): """Set up the InfluxDB component.""" - from influxdb import InfluxDBClient, exceptions conf = config[DOMAIN] @@ -341,7 +341,6 @@ class InfluxThread(threading.Thread): def write_to_influxdb(self, json): """Write preprocessed events to influxdb, with retry.""" - from influxdb import exceptions for retry in range(self.max_tries + 1): try: diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index df936eafa90..bd5249a3858 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -1,12 +1,8 @@ { "domain": "influxdb", - "name": "Influxdb", + "name": "InfluxDB", "documentation": "https://www.home-assistant.io/integrations/influxdb", - "requirements": [ - "influxdb==5.2.3" - ], + "requirements": ["influxdb==5.2.3"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] -} \ No newline at end of file + "codeowners": ["@fabaff"] +} diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index e94824c9abb..4a169453e35 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from influxdb import InfluxDBClient, exceptions import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -94,7 +95,6 @@ class InfluxSensor(Entity): def __init__(self, hass, influx_conf, query): """Initialize the sensor.""" - from influxdb import InfluxDBClient, exceptions self._name = query.get(CONF_NAME) self._unit_of_measurement = query.get(CONF_UNIT_OF_MEASUREMENT) @@ -205,14 +205,13 @@ class InfluxSensorData: points = list(self.influx.query(self.query).get_points()) if not points: _LOGGER.warning( - "Query returned no points, sensor state set " "to UNKNOWN: %s", - self.query, + "Query returned no points, sensor state set to UNKNOWN: %s", self.query, ) self.value = None else: if len(points) > 1: _LOGGER.warning( - "Query returned multiple points, only first " "one shown: %s", + "Query returned multiple points, only first one shown: %s", self.query, ) self.value = points[0].get("value") diff --git a/homeassistant/components/input_boolean/__init__.py b/homeassistant/components/input_boolean/__init__.py index 4a32ad16797..a12c6552399 100644 --- a/homeassistant/components/input_boolean/__init__.py +++ b/homeassistant/components/input_boolean/__init__.py @@ -1,21 +1,30 @@ """Support to keep track of user controlled booleans for within automation.""" import logging +import typing import voluptuous as vol from homeassistant.const import ( + ATTR_EDITABLE, CONF_ICON, + CONF_ID, CONF_NAME, + SERVICE_RELOAD, + SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, - SERVICE_TOGGLE, STATE_ON, ) -from homeassistant.loader import bind_hass +from homeassistant.core import callback +from homeassistant.helpers import collection, entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service +from homeassistant.helpers.storage import Store +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.loader import bind_hass DOMAIN = "input_boolean" @@ -25,22 +34,48 @@ _LOGGER = logging.getLogger(__name__) CONF_INITIAL = "initial" +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_INITIAL): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, +} + +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_INITIAL): cv.boolean, + vol.Optional(CONF_ICON): cv.icon, +} + CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: cv.schema_with_slug_keys( - vol.Any( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_INITIAL): cv.boolean, - vol.Optional(CONF_ICON): cv.icon, - }, - None, - ) - ) - }, + {DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))}, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + + +class InputBooleanStorageCollection(collection.StorageCollection): + """Input boolean collection stored in storage.""" + + CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) + + async def _process_create_data(self, data: typing.Dict) -> typing.Dict: + """Validate the config is valid.""" + return self.CREATE_SCHEMA(data) + + @callback + def _get_suggested_id(self, info: typing.Dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_NAME] + + async def _update_data(self, data: dict, update_data: typing.Dict) -> typing.Dict: + """Return a new updated data object.""" + update_data = self.UPDATE_SCHEMA(update_data) + return {**data, **update_data} + @bind_hass def is_on(hass, entity_id): @@ -48,24 +83,65 @@ def is_on(hass, entity_id): return hass.states.is_state(entity_id, STATE_ON) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up an input boolean.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() - entities = [] + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + collection.attach_entity_component_collection( + component, yaml_collection, lambda conf: InputBoolean(conf, from_yaml=True) + ) - for object_id, cfg in config[DOMAIN].items(): - if not cfg: - cfg = {} + storage_collection = InputBooleanStorageCollection( + Store(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}_storage_collection"), + id_manager, + ) + collection.attach_entity_component_collection( + component, storage_collection, InputBoolean + ) - name = cfg.get(CONF_NAME) - initial = cfg.get(CONF_INITIAL) - icon = cfg.get(CONF_ICON) + await yaml_collection.async_load( + [{CONF_ID: id_, **(conf or {})} for id_, conf in config[DOMAIN].items()] + ) + await storage_collection.async_load() - entities.append(InputBoolean(object_id, name, initial, icon)) + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass) - if not entities: - return False + async def _collection_changed( + change_type: str, item_id: str, config: typing.Optional[typing.Dict] + ) -> None: + """Handle a collection change: clean up entity registry on removals.""" + if change_type != collection.CHANGE_REMOVED: + return + + ent_reg = await entity_registry.async_get_registry(hass) + ent_reg.async_remove(ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id)) + + yaml_collection.async_add_listener(_collection_changed) + storage_collection.async_add_listener(_collection_changed) + + async def reload_service_handler(service_call: ServiceCallType) -> None: + """Remove all input booleans and load new ones from config.""" + conf = await component.async_prepare_reload(skip_reset=True) + if conf is None: + return + await yaml_collection.async_load( + [{CONF_ID: id_, **(conf or {})} for id_, conf in conf[DOMAIN].items()] + ) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on") @@ -73,19 +149,20 @@ async def async_setup(hass, config): component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") - await component.async_add_entities(entities) return True class InputBoolean(ToggleEntity, RestoreEntity): """Representation of a boolean input.""" - def __init__(self, object_id, name, initial, icon): + def __init__(self, config: typing.Optional[dict], from_yaml: bool = False): """Initialize a boolean input.""" - self.entity_id = ENTITY_ID_FORMAT.format(object_id) - self._name = name - self._state = initial - self._icon = icon + self._config = config + self._editable = True + self._state = config.get(CONF_INITIAL) + if from_yaml: + self._editable = False + self.entity_id = ENTITY_ID_FORMAT.format(self.unique_id) @property def should_poll(self): @@ -95,18 +172,28 @@ class InputBoolean(ToggleEntity, RestoreEntity): @property def name(self): """Return name of the boolean input.""" - return self._name + return self._config.get(CONF_NAME) + + @property + def state_attributes(self): + """Return the state attributes of the entity.""" + return {ATTR_EDITABLE: self._editable} @property def icon(self): """Return the icon to be used for this entity.""" - return self._icon + return self._config.get(CONF_ICON) @property def is_on(self): """Return true if entity is on.""" return self._state + @property + def unique_id(self): + """Return a unique ID for the person.""" + return self._config[CONF_ID] + async def async_added_to_hass(self): """Call when entity about to be added to hass.""" # If not None, we got an initial value. @@ -126,3 +213,8 @@ class InputBoolean(ToggleEntity, RestoreEntity): """Turn the entity off.""" self._state = False await self.async_update_ha_state() + + async def async_update_config(self, config: typing.Dict) -> None: + """Handle when the config is updated.""" + self._config = config + self.async_write_ha_state() diff --git a/homeassistant/components/input_boolean/manifest.json b/homeassistant/components/input_boolean/manifest.json index 09ae235e6b8..f697d94c893 100644 --- a/homeassistant/components/input_boolean/manifest.json +++ b/homeassistant/components/input_boolean/manifest.json @@ -1,10 +1,9 @@ { "domain": "input_boolean", - "name": "Input boolean", + "name": "Input Boolean", "documentation": "https://www.home-assistant.io/integrations/input_boolean", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py index b8bc18edfac..558d57ae862 100644 --- a/homeassistant/components/input_boolean/reproduce_state.py +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -4,11 +4,11 @@ import logging from typing import Iterable, Optional from homeassistant.const import ( + ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, - ATTR_ENTITY_ID, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/input_boolean/services.yaml b/homeassistant/components/input_boolean/services.yaml index e49d46c9b86..e391c15d3a8 100644 --- a/homeassistant/components/input_boolean/services.yaml +++ b/homeassistant/components/input_boolean/services.yaml @@ -10,3 +10,6 @@ turn_on: description: Turns on an input boolean. fields: entity_id: {description: Entity id of the input boolean to turn on., example: input_boolean.notify_alerts} +reload: + description: Reload the input_boolean configuration. + diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 36180ed2bad..da684e03ddc 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -1,16 +1,22 @@ """Support to select a date and/or a time.""" -import logging import datetime +import logging import voluptuous as vol -from homeassistant.const import ATTR_DATE, ATTR_TIME, CONF_ICON, CONF_NAME +from homeassistant.const import ( + ATTR_DATE, + ATTR_TIME, + CONF_ICON, + CONF_NAME, + SERVICE_RELOAD, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service from homeassistant.util import dt as dt_util - _LOGGER = logging.getLogger(__name__) DOMAIN = "input_datetime" @@ -52,26 +58,31 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input datetime.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - entities = [] + entities = await _async_process_config(config) - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - has_time = cfg.get(CONF_HAS_TIME) - has_date = cfg.get(CONF_HAS_DATE) - icon = cfg.get(CONF_ICON) - initial = cfg.get(CONF_INITIAL) - entities.append( - InputDatetime(object_id, name, has_date, has_time, icon, initial) - ) + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) - if not entities: - return False + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) async def async_set_datetime_service(entity, call): """Handle a call to the input datetime 'set datetime' service.""" @@ -87,7 +98,7 @@ async def async_setup(hass, config): and not (time or dttm) ): _LOGGER.error( - "Invalid service data for %s " "input_datetime.set_datetime: %s", + "Invalid service data for %s input_datetime.set_datetime: %s", entity.entity_id, str(call.data), ) @@ -108,10 +119,28 @@ async def async_setup(hass, config): async_set_datetime_service, ) - await component.async_add_entities(entities) + if entities: + await component.async_add_entities(entities) return True +async def _async_process_config(config): + """Process config and create list of entities.""" + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + name = cfg.get(CONF_NAME) + has_time = cfg.get(CONF_HAS_TIME) + has_date = cfg.get(CONF_HAS_DATE) + icon = cfg.get(CONF_ICON) + initial = cfg.get(CONF_INITIAL) + entities.append( + InputDatetime(object_id, name, has_date, has_time, icon, initial) + ) + + return entities + + class InputDatetime(RestoreEntity): """Representation of a datetime input.""" diff --git a/homeassistant/components/input_datetime/manifest.json b/homeassistant/components/input_datetime/manifest.json index 9808c45aa74..bde5b6e6b90 100644 --- a/homeassistant/components/input_datetime/manifest.json +++ b/homeassistant/components/input_datetime/manifest.json @@ -1,10 +1,9 @@ { "domain": "input_datetime", - "name": "Input datetime", + "name": "Input Datetime", "documentation": "https://www.home-assistant.io/integrations/input_datetime", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/input_datetime/services.yaml b/homeassistant/components/input_datetime/services.yaml index 8a40be47acd..472bd1b83b9 100644 --- a/homeassistant/components/input_datetime/services.yaml +++ b/homeassistant/components/input_datetime/services.yaml @@ -9,3 +9,6 @@ set_datetime: example: '"time": "05:30:00"'} datetime: {description: The target date & time the entity should be set to. Do not use with date or time., example: '"datetime": "2019-04-22 05:30:00"'} + +reload: + description: Reload the input_datetime configuration. diff --git a/homeassistant/components/input_number/__init__.py b/homeassistant/components/input_number/__init__.py index 77625ffa7f8..a4438020886 100644 --- a/homeassistant/components/input_number/__init__.py +++ b/homeassistant/components/input_number/__init__.py @@ -3,16 +3,18 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_MODE, + ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, - CONF_NAME, CONF_MODE, + CONF_NAME, + SERVICE_RELOAD, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service _LOGGER = logging.getLogger(__name__) @@ -77,12 +79,49 @@ CONFIG_SCHEMA = vol.Schema( required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input slider.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service( + SERVICE_SET_VALUE, + {vol.Required(ATTR_VALUE): vol.Coerce(float)}, + "async_set_value", + ) + + component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") + + component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -101,21 +140,7 @@ async def async_setup(hass, config): ) ) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SET_VALUE, - {vol.Required(ATTR_VALUE): vol.Coerce(float)}, - "async_set_value", - ) - - component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment") - - component.async_register_entity_service(SERVICE_DECREMENT, {}, "async_decrement") - - await component.async_add_entities(entities) - return True + return entities class InputNumber(RestoreEntity): diff --git a/homeassistant/components/input_number/manifest.json b/homeassistant/components/input_number/manifest.json index 31e00d0fce8..98376e77d04 100644 --- a/homeassistant/components/input_number/manifest.json +++ b/homeassistant/components/input_number/manifest.json @@ -1,10 +1,9 @@ { "domain": "input_number", - "name": "Input number", + "name": "Input Number", "documentation": "https://www.home-assistant.io/integrations/input_number", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py index 97a4837d371..22a91f74000 100644 --- a/homeassistant/components/input_number/reproduce_state.py +++ b/homeassistant/components/input_number/reproduce_state.py @@ -7,7 +7,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE +from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 650abc056a9..9cd1b913ccd 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -14,3 +14,5 @@ set_value: entity_id: {description: Entity id of the input number to set the new value., example: input_number.threshold} value: {description: The target value the entity should be set to., example: 42} +reload: + description: Reload the input_number configuration. diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index ae609e09271..b2b4b2083e8 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -3,10 +3,11 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME +from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service _LOGGER = logging.getLogger(__name__) @@ -61,23 +62,31 @@ CONFIG_SCHEMA = vol.Schema( required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input select.""" component = EntityComponent(_LOGGER, DOMAIN, hass) - entities = [] + entities = await _async_process_config(config) - for object_id, cfg in config[DOMAIN].items(): - name = cfg.get(CONF_NAME) - options = cfg.get(CONF_OPTIONS) - initial = cfg.get(CONF_INITIAL) - icon = cfg.get(CONF_ICON) - entities.append(InputSelect(object_id, name, initial, options, icon)) + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) - if not entities: - return False + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) component.async_register_entity_service( SERVICE_SELECT_OPTION, @@ -103,10 +112,25 @@ async def async_setup(hass, config): "async_set_options", ) - await component.async_add_entities(entities) + if entities: + await component.async_add_entities(entities) return True +async def _async_process_config(config): + """Process config and create list of entities.""" + entities = [] + + for object_id, cfg in config[DOMAIN].items(): + name = cfg.get(CONF_NAME) + options = cfg.get(CONF_OPTIONS) + initial = cfg.get(CONF_INITIAL) + icon = cfg.get(CONF_ICON) + entities.append(InputSelect(object_id, name, initial, options, icon)) + + return entities + + class InputSelect(RestoreEntity): """Representation of a select input.""" diff --git a/homeassistant/components/input_select/manifest.json b/homeassistant/components/input_select/manifest.json index d71674fd40c..892794c2616 100644 --- a/homeassistant/components/input_select/manifest.json +++ b/homeassistant/components/input_select/manifest.json @@ -1,10 +1,9 @@ { "domain": "input_select", - "name": "Input select", + "name": "Input Select", "documentation": "https://www.home-assistant.io/integrations/input_select", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py index 657f518cd3d..818510bee4a 100644 --- a/homeassistant/components/input_select/reproduce_state.py +++ b/homeassistant/components/input_select/reproduce_state.py @@ -9,11 +9,11 @@ from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from . import ( + ATTR_OPTION, + ATTR_OPTIONS, DOMAIN, SERVICE_SELECT_OPTION, SERVICE_SET_OPTIONS, - ATTR_OPTION, - ATTR_OPTIONS, ) ATTR_GROUP = [ATTR_OPTION, ATTR_OPTIONS] diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index 8084e56b731..2cce496d0b6 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -20,3 +20,5 @@ set_options: for., example: input_select.my_select} options: {description: Options for the input select entity., example: '["Item A", "Item B", "Item C"]'} +reload: + description: Reload the input_select configuration. diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index d43e47c11ca..2049de7ab27 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -3,16 +3,18 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - ATTR_UNIT_OF_MEASUREMENT, ATTR_MODE, + ATTR_UNIT_OF_MEASUREMENT, CONF_ICON, - CONF_NAME, CONF_MODE, + CONF_NAME, + SERVICE_RELOAD, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service _LOGGER = logging.getLogger(__name__) @@ -21,7 +23,9 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" CONF_INITIAL = "initial" CONF_MIN = "min" +CONF_MIN_VALUE = 0 CONF_MAX = "max" +CONF_MAX_VALUE = 100 MODE_TEXT = "text" MODE_PASSWORD = "password" @@ -57,8 +61,8 @@ CONFIG_SCHEMA = vol.Schema( 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_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), + vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), vol.Optional(CONF_INITIAL, ""): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, @@ -76,20 +80,51 @@ CONFIG_SCHEMA = vol.Schema( required=True, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) async def async_setup(hass, config): """Set up an input text box.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(config) + + async def reload_service_handler(service_call): + """Remove all entities and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + + component.async_register_entity_service( + SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value" + ) + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(config): + """Process config and create list of entities.""" 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) + minimum = cfg.get(CONF_MIN, CONF_MIN_VALUE) + maximum = cfg.get(CONF_MAX, CONF_MAX_VALUE) initial = cfg.get(CONF_INITIAL) icon = cfg.get(CONF_ICON) unit = cfg.get(ATTR_UNIT_OF_MEASUREMENT) @@ -102,15 +137,7 @@ async def async_setup(hass, config): ) ) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_SET_VALUE, {vol.Required(ATTR_VALUE): cv.string}, "async_set_value" - ) - - await component.async_add_entities(entities) - return True + return entities class InputText(RestoreEntity): diff --git a/homeassistant/components/input_text/manifest.json b/homeassistant/components/input_text/manifest.json index eaddaf49b84..34fd0681e16 100644 --- a/homeassistant/components/input_text/manifest.json +++ b/homeassistant/components/input_text/manifest.json @@ -1,10 +1,9 @@ { "domain": "input_text", - "name": "Input text", + "name": "Input Text", "documentation": "https://www.home-assistant.io/integrations/input_text", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py index f64c5c019f6..28a2f27ee84 100644 --- a/homeassistant/components/input_text/reproduce_state.py +++ b/homeassistant/components/input_text/reproduce_state.py @@ -7,7 +7,7 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE +from . import ATTR_VALUE, DOMAIN, SERVICE_SET_VALUE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/input_text/services.yaml b/homeassistant/components/input_text/services.yaml index 219eecf2fd6..e9b709b0c03 100644 --- a/homeassistant/components/input_text/services.yaml +++ b/homeassistant/components/input_text/services.yaml @@ -4,3 +4,5 @@ set_value: entity_id: {description: Entity id of the input text to set the new value., example: input_text.text1} value: {description: The target value the entity should be set to., example: This is an example text} +reload: + description: Reload the input_text configuration. diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 11f224dbfcc..df6fa626a4f 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -614,7 +614,7 @@ class InsteonEntity(Entity): # Get an extension label if there is one extension = self._get_label() if extension: - extension = " " + extension + extension = f" {extension}" name = "{:s} {:s}{:s}".format( description, self._insteon_device.address.human, extension ) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index c8821f3b176..de15fbee66e 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,9 +2,7 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": [ - "insteonplm==0.16.5" - ], + "requirements": ["insteonplm==0.16.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/integration/manifest.json b/homeassistant/components/integration/manifest.json index d910e7bdc78..d7063e12e9b 100644 --- a/homeassistant/components/integration/manifest.json +++ b/homeassistant/components/integration/manifest.json @@ -1,10 +1,9 @@ { "domain": "integration", - "name": "Integration", + "name": "Integration - Riemann sum integral", "documentation": "https://www.home-assistant.io/integrations/integration", "requirements": [], "dependencies": [], - "codeowners": [ - "@dgomes" - ] + "codeowners": ["@dgomes"], + "quality_scale": "internal" } diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 236a996794a..560a7cbd33c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -1,22 +1,21 @@ """Numeric integration of data coming from a source sensor over time.""" +from decimal import Decimal, DecimalException import logging -from decimal import Decimal, DecimalException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, ATTR_UNIT_OF_MEASUREMENT, - STATE_UNKNOWN, + CONF_NAME, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 31ab36ecc89..bdf612b2e83 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -1,16 +1,12 @@ """The Intent integration.""" -import asyncio import logging import voluptuous as vol -from homeassistant.core import HomeAssistant -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.setup import ATTR_COMPONENT from homeassistant.components import http from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.helpers import config_validation as cv, intent -from homeassistant.loader import async_get_integration, IntegrationNotFound +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv, integration_platform, intent from .const import DOMAIN @@ -22,32 +18,16 @@ async def async_setup(hass: HomeAssistant, config: dict): """Set up the Intent component.""" hass.http.register_view(IntentHandleView()) - tasks = [_async_process_intent(hass, comp) for comp in hass.config.components] - - async def async_component_loaded(event): - """Handle a new component loaded.""" - await _async_process_intent(hass, event.data[ATTR_COMPONENT]) - - hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded) - - if tasks: - await asyncio.gather(*tasks) + await integration_platform.async_process_integration_platforms( + hass, DOMAIN, _async_process_intent + ) return True -async def _async_process_intent(hass: HomeAssistant, component_name: str): - """Process the intents of a component.""" - try: - integration = await async_get_integration(hass, component_name) - platform = integration.get_platform(DOMAIN) - except (IntegrationNotFound, ImportError): - return - - try: - await platform.async_setup_intents(hass) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Error setting up intents for %s", component_name) +async def _async_process_intent(hass: HomeAssistant, domain: str, platform): + """Process the intents of an integration.""" + await platform.async_setup_intents(hass) class IntentHandleView(http.HomeAssistantView): diff --git a/homeassistant/components/intent_script/__init__.py b/homeassistant/components/intent_script/__init__.py index ce4b8b27a51..38f93ed3506 100644 --- a/homeassistant/components/intent_script/__init__.py +++ b/homeassistant/components/intent_script/__init__.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.helpers import intent, template, script, config_validation as cv +from homeassistant.helpers import config_validation as cv, intent, script, template DOMAIN = "intent_script" diff --git a/homeassistant/components/intent_script/manifest.json b/homeassistant/components/intent_script/manifest.json index f2d9a338560..6b204d0e83c 100644 --- a/homeassistant/components/intent_script/manifest.json +++ b/homeassistant/components/intent_script/manifest.json @@ -1,8 +1,9 @@ { "domain": "intent_script", - "name": "Intent script", + "name": "Intent Script", "documentation": "https://www.home-assistant.io/integrations/intent_script", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/intesishome/__init__.py b/homeassistant/components/intesishome/__init__.py new file mode 100644 index 00000000000..fd4ae1f03e3 --- /dev/null +++ b/homeassistant/components/intesishome/__init__.py @@ -0,0 +1 @@ +"""Intesishome platform.""" diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py new file mode 100644 index 00000000000..669d1155d80 --- /dev/null +++ b/homeassistant/components/intesishome/climate.py @@ -0,0 +1,407 @@ +"""Support for IntesisHome and airconwithme Smart AC Controllers.""" +import logging +from random import randrange + +from pyintesishome import IHAuthenticationError, IHConnectionError, IntesisHome +import voluptuous as vol + +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_SWING_MODE, + SUPPORT_TARGET_TEMPERATURE, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_VERTICAL, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_DEVICE, + CONF_PASSWORD, + CONF_USERNAME, + TEMP_CELSIUS, +) +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later + +_LOGGER = logging.getLogger(__name__) + +IH_DEVICE_INTESISHOME = "IntesisHome" +IH_DEVICE_AIRCONWITHME = "airconwithme" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_DEVICE, default=IH_DEVICE_INTESISHOME): vol.In( + [IH_DEVICE_AIRCONWITHME, IH_DEVICE_INTESISHOME] + ), + } +) + +MAP_IH_TO_HVAC_MODE = { + "auto": HVAC_MODE_HEAT_COOL, + "cool": HVAC_MODE_COOL, + "dry": HVAC_MODE_DRY, + "fan": HVAC_MODE_FAN_ONLY, + "heat": HVAC_MODE_HEAT, + "off": HVAC_MODE_OFF, +} + +MAP_HVAC_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_HVAC_MODE.items()} + +MAP_STATE_ICONS = { + HVAC_MODE_COOL: "mdi:snowflake", + HVAC_MODE_DRY: "mdi:water-off", + HVAC_MODE_FAN_ONLY: "mdi:fan", + HVAC_MODE_HEAT: "mdi:white-balance-sunny", + HVAC_MODE_HEAT_COOL: "mdi:cached", +} + +IH_HVAC_MODES = [ + HVAC_MODE_HEAT_COOL, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, +] + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Create the IntesisHome climate devices.""" + ih_user = config[CONF_USERNAME] + ih_pass = config[CONF_PASSWORD] + device_type = config[CONF_DEVICE] + + controller = IntesisHome( + ih_user, + ih_pass, + hass.loop, + websession=async_get_clientsession(hass), + device_type=device_type, + ) + try: + await controller.poll_status() + except IHAuthenticationError: + _LOGGER.error("Invalid username or password") + return + except IHConnectionError: + _LOGGER.error("Error connecting to the %s server", device_type) + raise PlatformNotReady + + ih_devices = controller.get_devices() + if ih_devices: + async_add_entities( + [ + IntesisAC(ih_device_id, device, controller) + for ih_device_id, device in ih_devices.items() + ], + True, + ) + else: + _LOGGER.error( + "Error getting device list from %s API: %s", + device_type, + controller.error_message, + ) + await controller.stop() + + +class IntesisAC(ClimateDevice): + """Represents an Intesishome air conditioning device.""" + + def __init__(self, ih_device_id, ih_device, controller): + """Initialize the thermostat.""" + self._controller = controller + self._device_id = ih_device_id + self._ih_device = ih_device + self._device_name = ih_device.get("name") + self._device_type = controller.device_type + self._connected = None + self._setpoint_step = 1 + self._current_temp = None + self._max_temp = None + self._min_temp = None + self._target_temp = None + self._outdoor_temp = None + self._run_hours = None + self._rssi = None + self._swing = None + self._swing_list = None + self._vvane = None + self._hvane = None + self._power = False + self._fan_speed = None + self._hvac_mode = None + self._fan_modes = controller.get_fan_speed_list(ih_device_id) + self._support = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE + self._swing_list = [SWING_OFF] + + if ih_device.get("config_vertical_vanes"): + self._swing_list.append(SWING_VERTICAL) + if ih_device.get("config_horizontal_vanes"): + self._swing_list.append(SWING_HORIZONTAL) + if len(self._swing_list) == 3: + self._swing_list.append(SWING_BOTH) + self._support |= SUPPORT_SWING_MODE + elif len(self._swing_list) == 2: + self._support |= SUPPORT_SWING_MODE + + async def async_added_to_hass(self): + """Subscribe to event updates.""" + _LOGGER.debug("Added climate device with state: %s", repr(self._ih_device)) + await self._controller.add_update_callback(self.async_update_callback) + try: + await self._controller.connect() + except IHConnectionError as ex: + _LOGGER.error("Exception connecting to IntesisHome: %s", ex) + + @property + def name(self): + """Return the name of the AC device.""" + return self._device_name + + @property + def temperature_unit(self): + """Intesishome API uses celsius on the backend.""" + return TEMP_CELSIUS + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attrs = {} + if len(self._swing_list) > 1: + attrs["vertical_vane"] = self._vvane + attrs["horizontal_vane"] = self._hvane + if self._outdoor_temp: + attrs["outdoor_temp"] = self._outdoor_temp + return attrs + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._device_id + + @property + def target_temperature_step(self) -> float: + """Return whether setpoint should be whole or half degree precision.""" + return self._setpoint_step + + async def async_set_temperature(self, **kwargs): + """Set new target temperature.""" + temperature = kwargs.get(ATTR_TEMPERATURE) + hvac_mode = kwargs.get(ATTR_HVAC_MODE) + + if hvac_mode: + await self.async_set_hvac_mode(hvac_mode) + + if temperature: + _LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature) + await self._controller.set_temperature(self._device_id, temperature) + self._target_temp = temperature + + # Write updated temperature to HA state to avoid flapping (API confirmation is slow) + self.async_write_ha_state() + + async def async_set_hvac_mode(self, hvac_mode): + """Set operation mode.""" + _LOGGER.debug("Setting %s to %s mode", self._device_type, hvac_mode) + if hvac_mode == HVAC_MODE_OFF: + self._power = False + await self._controller.set_power_off(self._device_id) + # Write changes to HA, API can be slow to push changes + self.async_write_ha_state() + return + + # First check device is turned on + if not self._controller.is_on(self._device_id): + self._power = True + await self._controller.set_power_on(self._device_id) + + # Set the mode + await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode]) + + # Send the temperature again in case changing modes has changed it + if self._target_temp: + await self._controller.set_temperature(self._device_id, self._target_temp) + + # Updates can take longer than 2 seconds, so update locally + self._hvac_mode = hvac_mode + self.async_write_ha_state() + + async def async_set_fan_mode(self, fan_mode): + """Set fan mode (from quiet, low, medium, high, auto).""" + await self._controller.set_fan_speed(self._device_id, fan_mode) + + # Updates can take longer than 2 seconds, so update locally + self._fan_speed = fan_mode + self.async_write_ha_state() + + async def async_set_swing_mode(self, swing_mode): + """Set the vertical vane.""" + if swing_mode == SWING_OFF: + await self._controller.set_vertical_vane(self._device_id, "auto/stop") + await self._controller.set_horizontal_vane(self._device_id, "auto/stop") + elif swing_mode == SWING_BOTH: + await self._controller.set_vertical_vane(self._device_id, "swing") + await self._controller.set_horizontal_vane(self._device_id, "swing") + elif swing_mode == SWING_HORIZONTAL: + await self._controller.set_vertical_vane(self._device_id, "auto/stop") + await self._controller.set_horizontal_vane(self._device_id, "swing") + elif swing_mode == SWING_VERTICAL: + await self._controller.set_vertical_vane(self._device_id, "swing") + await self._controller.set_horizontal_vane(self._device_id, "auto/stop") + self._swing = swing_mode + + async def async_update(self): + """Copy values from controller dictionary to climate device.""" + # Update values from controller's device dictionary + self._connected = self._controller.is_connected + self._current_temp = self._controller.get_temperature(self._device_id) + self._fan_speed = self._controller.get_fan_speed(self._device_id) + self._power = self._controller.is_on(self._device_id) + self._min_temp = self._controller.get_min_setpoint(self._device_id) + self._max_temp = self._controller.get_max_setpoint(self._device_id) + self._rssi = self._controller.get_rssi(self._device_id) + self._run_hours = self._controller.get_run_hours(self._device_id) + self._target_temp = self._controller.get_setpoint(self._device_id) + self._outdoor_temp = self._controller.get_outdoor_temperature(self._device_id) + + # Operation mode + mode = self._controller.get_mode(self._device_id) + self._hvac_mode = MAP_IH_TO_HVAC_MODE.get(mode) + + # Swing mode + # Climate module only supports one swing setting. + self._vvane = self._controller.get_vertical_swing(self._device_id) + self._hvane = self._controller.get_horizontal_swing(self._device_id) + + if self._vvane == "swing" and self._hvane == "swing": + self._swing = SWING_BOTH + elif self._vvane == "swing": + self._swing = SWING_VERTICAL + elif self._hvane == "swing": + self._swing = SWING_HORIZONTAL + else: + self._swing = SWING_OFF + + async def async_will_remove_from_hass(self): + """Shutdown the controller when the device is being removed.""" + await self._controller.stop() + + @property + def icon(self): + """Return the icon for the current state.""" + icon = None + if self._power: + icon = MAP_STATE_ICONS.get(self._hvac_mode) + return icon + + async def async_update_callback(self, device_id=None): + """Let HA know there has been an update from the controller.""" + # Track changes in connection state + if not self._controller.is_connected and self._connected: + # Connection has dropped + self._connected = False + reconnect_minutes = 1 + randrange(10) + _LOGGER.error( + "Connection to %s API was lost. Reconnecting in %i minutes", + self._device_type, + reconnect_minutes, + ) + # Schedule reconnection + async_call_later( + self.hass, reconnect_minutes * 60, self._controller.connect() + ) + + if self._controller.is_connected and not self._connected: + # Connection has been restored + self._connected = True + _LOGGER.debug("Connection to %s API was restored", self._device_type) + + if not device_id or self._device_id == device_id: + # Update all devices if no device_id was specified + _LOGGER.debug( + "%s API sent a status update for device %s", + self._device_type, + device_id, + ) + self.async_schedule_update_ha_state(True) + + @property + def min_temp(self): + """Return the minimum temperature for the current mode of operation.""" + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature for the current mode of operation.""" + return self._max_temp + + @property + def should_poll(self): + """Poll for updates if pyIntesisHome doesn't have a socket open.""" + return False + + @property + def hvac_modes(self): + """List of available operation modes.""" + return IH_HVAC_MODES + + @property + def fan_mode(self): + """Return whether the fan is on.""" + return self._fan_speed + + @property + def swing_mode(self): + """Return current swing mode.""" + return self._swing + + @property + def fan_modes(self): + """List of available fan modes.""" + return self._fan_modes + + @property + def swing_modes(self): + """List of available swing positions.""" + return self._swing_list + + @property + def available(self) -> bool: + """If the device hasn't been able to connect, mark as unavailable.""" + return self._connected or self._connected is None + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._current_temp + + @property + def hvac_mode(self): + """Return the current mode of operation if unit is on.""" + if self._power: + return self._hvac_mode + return HVAC_MODE_OFF + + @property + def target_temperature(self): + """Return the current setpoint temperature if unit is on.""" + return self._target_temp + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json new file mode 100644 index 00000000000..025d08ac548 --- /dev/null +++ b/homeassistant/components/intesishome/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "intesishome", + "name": "IntesisHome", + "documentation": "https://www.home-assistant.io/integrations/intesishome", + "dependencies": [], + "codeowners": ["@jnimmo"], + "requirements": ["pyintesishome==1.5"] +} diff --git a/homeassistant/components/ios/.translations/ko.json b/homeassistant/components/ios/.translations/ko.json index 1496dab0555..283594a45b5 100644 --- a/homeassistant/components/ios/.translations/ko.json +++ b/homeassistant/components/ios/.translations/ko.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Home Assistant iOS \ucef4\ud3ec\ub10c\ud2b8\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Home Assistant iOS" } }, diff --git a/homeassistant/components/ios/config_flow.py b/homeassistant/components/ios/config_flow.py index 511e350aae3..9eaca389ba1 100644 --- a/homeassistant/components/ios/config_flow.py +++ b/homeassistant/components/ios/config_flow.py @@ -1,8 +1,8 @@ """Config flow for iOS.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries -from .const import DOMAIN +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN config_entry_flow.register_discovery_flow( DOMAIN, "Home Assistant iOS", lambda *_: True, config_entries.CONN_CLASS_CLOUD_PUSH diff --git a/homeassistant/components/ios/manifest.json b/homeassistant/components/ios/manifest.json index 6e011a43ded..d55b1ef5c74 100644 --- a/homeassistant/components/ios/manifest.json +++ b/homeassistant/components/ios/manifest.json @@ -1,15 +1,9 @@ { "domain": "ios", - "name": "Ios", + "name": "Apple iOS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ios", "requirements": [], - "dependencies": [ - "device_tracker", - "http", - "zeroconf" - ], - "codeowners": [ - "@robbiet480" - ] + "dependencies": ["device_tracker", "http", "zeroconf"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/ios/notify.py b/homeassistant/components/ios/notify.py index 80dbad5336d..63ed6a6ee26 100644 --- a/homeassistant/components/ios/notify.py +++ b/homeassistant/components/ios/notify.py @@ -92,9 +92,9 @@ class iOSNotificationService(BaseNotificationService): if req.status_code != 201: fallback_error = req.json().get("errorMessage", "Unknown error") - fallback_message = ( - "Internal server error, " "please try again later: " "{}" - ).format(fallback_error) + fallback_message = "Internal server error, please try again later: {}".format( + fallback_error + ) message = req.json().get("message", fallback_message) if req.status_code == 429: _LOGGER.warning(message) diff --git a/homeassistant/components/iota/manifest.json b/homeassistant/components/iota/manifest.json index 018ea4ac167..b49553dabd9 100644 --- a/homeassistant/components/iota/manifest.json +++ b/homeassistant/components/iota/manifest.json @@ -1,10 +1,8 @@ { "domain": "iota", - "name": "Iota", + "name": "IOTA", "documentation": "https://www.home-assistant.io/integrations/iota", - "requirements": [ - "pyota==2.0.5" - ], + "requirements": ["pyota==2.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/iperf3/manifest.json b/homeassistant/components/iperf3/manifest.json index 6b7cadfd5de..d98472f8236 100644 --- a/homeassistant/components/iperf3/manifest.json +++ b/homeassistant/components/iperf3/manifest.json @@ -2,11 +2,7 @@ "domain": "iperf3", "name": "Iperf3", "documentation": "https://www.home-assistant.io/integrations/iperf3", - "requirements": [ - "iperf3==0.1.11" - ], + "requirements": ["iperf3==0.1.11"], "dependencies": [], - "codeowners": [ - "@rohankapoorcom" - ] + "codeowners": ["@rohankapoorcom"] } diff --git a/homeassistant/components/ipma/.translations/da.json b/homeassistant/components/ipma/.translations/da.json index 080c41429ba..017aff4d0ec 100644 --- a/homeassistant/components/ipma/.translations/da.json +++ b/homeassistant/components/ipma/.translations/da.json @@ -11,7 +11,7 @@ "name": "Navn" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", - "title": "Beliggenhed" + "title": "Lokalitet" } }, "title": "Portugisisk vejrservice (IPMA)" diff --git a/homeassistant/components/ipma/__init__.py b/homeassistant/components/ipma/__init__.py index 702e12a8a63..a00941624f5 100644 --- a/homeassistant/components/ipma/__init__.py +++ b/homeassistant/components/ipma/__init__.py @@ -1,5 +1,6 @@ """Component for the Portuguese weather service - IPMA.""" from homeassistant.core import Config, HomeAssistant + from .config_flow import IpmaFlowHandler # noqa: F401 from .const import DOMAIN # noqa: F401 diff --git a/homeassistant/components/ipma/manifest.json b/homeassistant/components/ipma/manifest.json index 47759759a56..d01bf3e8da4 100644 --- a/homeassistant/components/ipma/manifest.json +++ b/homeassistant/components/ipma/manifest.json @@ -1,13 +1,9 @@ { "domain": "ipma", - "name": "Ipma", + "name": "Instituto Português do Mar e Atmosfera (IPMA)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ipma", - "requirements": [ - "pyipma==1.2.1" - ], + "requirements": ["pyipma==1.2.1"], "dependencies": [], - "codeowners": [ - "@dgomes" - ] + "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 9f1836c7389..c088d76d165 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -1,22 +1,23 @@ """Support for IPMA weather service.""" -import logging from datetime import timedelta +import logging import async_timeout +from pyipma import Station import voluptuous as vol from homeassistant.components.weather import ( - WeatherEntity, - PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + PLATFORM_SCHEMA, + WeatherEntity, ) -from homeassistant.const import CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -84,7 +85,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async def async_get_station(hass, latitude, longitude): """Retrieve weather station, station name to be used as the entity name.""" - from pyipma import Station websession = async_get_clientsession(hass) with async_timeout.timeout(10): diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index e3add21c3a4..512153fe1c2 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -4,8 +4,7 @@ from datetime import timedelta import logging from pyiqvia import Client -from pyiqvia.errors import IQVIAError, InvalidZipError - +from pyiqvia.errors import InvalidZipError, IQVIAError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -233,7 +232,7 @@ class IQVIAEntity(Entity): @property def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return f"{self._zip_code}_{self._type}" @property diff --git a/homeassistant/components/iqvia/config_flow.py b/homeassistant/components/iqvia/config_flow.py index f6776a833ee..abec8eff09a 100644 --- a/homeassistant/components/iqvia/config_flow.py +++ b/homeassistant/components/iqvia/config_flow.py @@ -1,10 +1,10 @@ """Config flow to configure the IQVIA component.""" from collections import OrderedDict -import voluptuous as vol from pyiqvia import Client from pyiqvia.errors import InvalidZipError +import voluptuous as vol from homeassistant import config_entries from homeassistant.core import callback diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 90aa89f06d1..21c31bbff08 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -9,8 +9,8 @@ from homeassistant.components.iqvia import ( DOMAIN, SENSORS, TYPE_ALLERGY_FORECAST, - TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_INDEX, + TYPE_ALLERGY_OUTLOOK, TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW, TYPE_ASTHMA_FORECAST, diff --git a/homeassistant/components/irish_rail_transport/manifest.json b/homeassistant/components/irish_rail_transport/manifest.json index da15d87ab02..2861a9dbbb1 100644 --- a/homeassistant/components/irish_rail_transport/manifest.json +++ b/homeassistant/components/irish_rail_transport/manifest.json @@ -1,12 +1,8 @@ { "domain": "irish_rail_transport", - "name": "Irish rail transport", + "name": "Irish Rail Transport", "documentation": "https://www.home-assistant.io/integrations/irish_rail_transport", - "requirements": [ - "pyirishrail==0.0.2" - ], + "requirements": ["pyirishrail==0.0.2"], "dependencies": [], - "codeowners": [ - "@ttroy50" - ] + "codeowners": ["@ttroy50"] } diff --git a/homeassistant/components/islamic_prayer_times/manifest.json b/homeassistant/components/islamic_prayer_times/manifest.json index 035b61d0f2d..b9245bf0812 100644 --- a/homeassistant/components/islamic_prayer_times/manifest.json +++ b/homeassistant/components/islamic_prayer_times/manifest.json @@ -1,10 +1,8 @@ { "domain": "islamic_prayer_times", - "name": "Islamic prayer times", + "name": "Islamic Prayer Times", "documentation": "https://www.home-assistant.io/integrations/islamic_prayer_times", - "requirements": [ - "prayer_times_calculator==0.0.3" - ], + "requirements": ["prayer_times_calculator==0.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 88cbd2cb431..3f7de535407 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -1,15 +1,16 @@ """Platform to retrieve Islamic prayer times information for Home Assistant.""" -import logging from datetime import datetime, timedelta +import logging +from prayer_times_calculator import PrayerTimesCalculator 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 DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -148,7 +149,6 @@ class IslamicPrayerTimesData: def get_new_prayer_times(self): """Fetch prayer times for today.""" - from prayer_times_calculator import PrayerTimesCalculator today = datetime.today().strftime("%Y-%m-%d") diff --git a/homeassistant/components/iss/manifest.json b/homeassistant/components/iss/manifest.json index 72521e67af9..d8324e490c3 100644 --- a/homeassistant/components/iss/manifest.json +++ b/homeassistant/components/iss/manifest.json @@ -1,10 +1,8 @@ { "domain": "iss", - "name": "Iss", + "name": "International Space Station (ISS)", "documentation": "https://www.home-assistant.io/integrations/iss", - "requirements": [ - "pyiss==1.0.1" - ], + "requirements": ["pyiss==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 96796e37a6a..c1474334a8e 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -56,13 +56,13 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -# Do not use the Hass consts for the states here - we're matching exact API -# responses, not using them for Hass states +# Do not use the Home Assistant consts for the states here - we're matching +# exact API responses, not using them for Home Assistant states NODE_FILTERS = { "binary_sensor": { "uom": [], "states": [], - "node_def_id": ["BinaryAlarm"], + "node_def_id": ["BinaryAlarm", "BinaryAlarm_ADV"], "insteon_type": ["16."], # Does a startswith() match; include the dot }, "sensor": { @@ -112,6 +112,8 @@ NODE_FILTERS = { "BallastRelayLampSwitch_ADV", "RemoteLinc2", "RemoteLinc2_ADV", + "KeypadDimmer", + "KeypadDimmer_ADV", ], "insteon_type": ["1."], }, @@ -155,7 +157,7 @@ SUPPORTED_DOMAINS = [ ] SUPPORTED_PROGRAM_DOMAINS = ["binary_sensor", "lock", "fan", "cover", "switch"] -# ISY Scenes are more like Switches than Hass Scenes +# ISY Scenes are more like Switches than Home Assistant Scenes # (they can turn off, and report their state) SCENE_DOMAIN = "switch" diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index eed5f1a81a0..7e69feb2f70 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -100,8 +100,8 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): Often times, a single device is represented by multiple nodes in the ISY, allowing for different nuances in how those devices report their on and - off events. This class turns those multiple nodes in to a single Hass - entity and handles both ways that ISY binary sensors can work. + off events. This class turns those multiple nodes in to a single Home + Assistant entity and handles both ways that ISY binary sensors can work. """ def __init__(self, node) -> None: @@ -165,7 +165,7 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): """Handle an "On" control event from the "negative" node.""" if event == "DON": _LOGGER.debug( - "Sensor %s turning Off via the Negative node " "sending a DON command", + "Sensor %s turning Off via the Negative node sending a DON command", self.name, ) self._computed_state = False @@ -181,7 +181,7 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): """ if event == "DON": _LOGGER.debug( - "Sensor %s turning On via the Primary node " "sending a DON command", + "Sensor %s turning On via the Primary node sending a DON command", self.name, ) self._computed_state = True @@ -189,7 +189,7 @@ class ISYBinarySensorDevice(ISYDevice, BinarySensorDevice): self._heartbeat() if event == "DOF": _LOGGER.debug( - "Sensor %s turning Off via the Primary node " "sending a DOF command", + "Sensor %s turning Off via the Primary node sending a DOF command", self.name, ) self._computed_state = False diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 759e1b78e8e..25793bfc0c0 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -1,10 +1,8 @@ { "domain": "isy994", - "name": "Isy994", + "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", - "requirements": [ - "PyISY==1.1.2" - ], + "requirements": ["PyISY==1.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/itach/manifest.json b/homeassistant/components/itach/manifest.json index 86bb362f8dc..748bfe0a817 100644 --- a/homeassistant/components/itach/manifest.json +++ b/homeassistant/components/itach/manifest.json @@ -1,10 +1,8 @@ { "domain": "itach", - "name": "Itach", + "name": "Global Caché iTach TCP/IP to IR", "documentation": "https://www.home-assistant.io/integrations/itach", - "requirements": [ - "pyitachip2ir==0.0.7" - ], + "requirements": ["pyitachip2ir==0.0.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/itunes/manifest.json b/homeassistant/components/itunes/manifest.json index ec47deabc23..d7e9938eec8 100644 --- a/homeassistant/components/itunes/manifest.json +++ b/homeassistant/components/itunes/manifest.json @@ -1,6 +1,6 @@ { "domain": "itunes", - "name": "Itunes", + "name": "Apple iTunes", "documentation": "https://www.home-assistant.io/integrations/itunes", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/itunes/media_player.py b/homeassistant/components/itunes/media_player.py index aebe16ffa26..327cbf5e9ac 100644 --- a/homeassistant/components/itunes/media_player.py +++ b/homeassistant/components/itunes/media_player.py @@ -4,7 +4,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -14,11 +14,11 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( CONF_HOST, @@ -110,7 +110,7 @@ class Itunes: def _command(self, named_command): """Make a request for a controlling command.""" - return self._request("PUT", "/" + named_command) + return self._request("PUT", f"/{named_command}") def now_playing(self): """Return the current state.""" @@ -168,7 +168,7 @@ class Itunes: def artwork_url(self): """Return a URL of the current track's album art.""" - return self._base_url + "/artwork" + return f"{self._base_url}/artwork" def airplay_devices(self): """Return a list of AirPlay devices.""" @@ -176,17 +176,17 @@ class Itunes: def airplay_device(self, device_id): """Return an AirPlay device.""" - return self._request("GET", "/airplay_devices/" + device_id) + return self._request("GET", f"/airplay_devices/{device_id}") def toggle_airplay_device(self, device_id, toggle): """Toggle airplay device on or off, id, toggle True or False.""" command = "on" if toggle else "off" - path = "/airplay_devices/" + device_id + "/" + command + path = f"/airplay_devices/{device_id}/{command}" return self._request("PUT", path) def set_volume_airplay_device(self, device_id, level): """Set volume, returns current state of device, id,level 0-100.""" - path = "/airplay_devices/" + device_id + "/volume" + path = f"/airplay_devices/{device_id}/volume" return self._request("PUT", path, {"level": level}) @@ -431,7 +431,7 @@ class AirPlayDevice(MediaPlayerDevice): if "name" in state_hash: name = state_hash.get("name", "") - self.device_name = (name + " AirTunes Speaker").strip() + self.device_name = f"{name} AirTunes Speaker".strip() if "kind" in state_hash: self.kind = state_hash.get("kind", None) diff --git a/homeassistant/components/izone/.translations/ko.json b/homeassistant/components/izone/.translations/ko.json index 69b8ce8a31e..91593b26511 100644 --- a/homeassistant/components/izone/.translations/ko.json +++ b/homeassistant/components/izone/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "iZone \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "iZone \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "iZone" } }, diff --git a/homeassistant/components/izone/__init__.py b/homeassistant/components/izone/__init__.py index 6fecbc1f3a6..0e5dcddbc48 100644 --- a/homeassistant/components/izone/__init__.py +++ b/homeassistant/components/izone/__init__.py @@ -13,7 +13,7 @@ from homeassistant.const import CONF_EXCLUDE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .const import IZONE, DATA_CONFIG +from .const import DATA_CONFIG, IZONE from .discovery import async_start_discovery_service, async_stop_discovery_service _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/izone/climate.py b/homeassistant/components/izone/climate.py index c932c66627b..b80dfc2542f 100644 --- a/homeassistant/components/izone/climate.py +++ b/homeassistant/components/izone/climate.py @@ -1,22 +1,21 @@ """Support for the iZone HVAC.""" import logging -from typing import Optional, List +from typing import List, Optional -from pizone import Zone, Controller +from pizone import Controller, Zone -from homeassistant.core import callback from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, - FAN_LOW, - FAN_MEDIUM, - FAN_HIGH, - FAN_AUTO, PRESET_ECO, PRESET_NONE, SUPPORT_FAN_MODE, @@ -25,23 +24,24 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_EXCLUDE, PRECISION_HALVES, TEMP_CELSIUS, - CONF_EXCLUDE, ) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( + DATA_CONFIG, DATA_DISCOVERY_SERVICE, - IZONE, - DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_RECONNECTED, DISPATCH_CONTROLLER_UPDATE, DISPATCH_ZONE_UPDATE, - DATA_CONFIG, + IZONE, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index eb57a36a2bb..add1bb47a54 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -1,7 +1,7 @@ """Config flow for izone.""" -import logging import asyncio +import logging from async_timeout import timeout @@ -9,14 +9,13 @@ from homeassistant import config_entries from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import IZONE, TIMEOUT_DISCOVERY, DISPATCH_CONTROLLER_DISCOVERED - +from .const import DISPATCH_CONTROLLER_DISCOVERED, IZONE, TIMEOUT_DISCOVERY +from .discovery import async_start_discovery_service, async_stop_discovery_service _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass): - from .discovery import async_start_discovery_service, async_stop_discovery_service controller_ready = asyncio.Event() async_dispatcher_connect( diff --git a/homeassistant/components/izone/discovery.py b/homeassistant/components/izone/discovery.py index 3630c28605b..c49144f1db9 100644 --- a/homeassistant/components/izone/discovery.py +++ b/homeassistant/components/izone/discovery.py @@ -1,17 +1,18 @@ """Internal discovery service for iZone AC.""" import logging + import pizone from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType from .const import ( DATA_DISCOVERY_SERVICE, - DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_DISCONNECTED, + DISPATCH_CONTROLLER_DISCOVERED, DISPATCH_CONTROLLER_RECONNECTED, DISPATCH_CONTROLLER_UPDATE, DISPATCH_ZONE_UPDATE, diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index a3aa3ad3fa8..b8bb5b55b79 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -1,9 +1,9 @@ { "domain": "izone", - "name": "izone", + "name": "iZone", "documentation": "https://www.home-assistant.io/integrations/izone", - "requirements": [ "python-izone==1.1.1" ], + "requirements": ["python-izone==1.1.1"], "dependencies": [], - "codeowners": [ "@Swamp-Ig" ], + "codeowners": ["@Swamp-Ig"], "config_flow": true } diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index bbe0c1d24fd..21c19da7b35 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -1,13 +1,12 @@ """The jewish_calendar component.""" import logging -import voluptuous as vol import hdate +import voluptuous as vol 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 - +from homeassistant.helpers.discovery import async_load_platform _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index b343963153d..8e1781f310b 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -1,12 +1,8 @@ { "domain": "jewish_calendar", - "name": "Jewish calendar", + "name": "Jewish Calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", - "requirements": [ - "hdate==0.9.3" - ], + "requirements": ["hdate==0.9.3"], "dependencies": [], - "codeowners": [ - "@tsvi" - ] + "codeowners": ["@tsvi"] } diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index a2c2e4b11b6..07f02e069d8 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -1,10 +1,8 @@ { "domain": "joaoapps_join", - "name": "Joaoapps join", + "name": "Joaoapps Join", "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", - "requirements": [ - "python-join-api==0.0.4" - ], + "requirements": ["python-join-api==0.0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index fac59cb3b8d..6f6836a1683 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -1,12 +1,8 @@ { "domain": "juicenet", - "name": "Juicenet", + "name": "JuiceNet", "documentation": "https://www.home-assistant.io/integrations/juicenet", - "requirements": [ - "python-juicenet==0.1.6" - ], + "requirements": ["python-juicenet==0.1.6"], "dependencies": [], - "codeowners": [ - "@jesserockz" - ] + "codeowners": ["@jesserockz"] } diff --git a/homeassistant/components/kaiterra/manifest.json b/homeassistant/components/kaiterra/manifest.json index eb3626a315b..562b988ee38 100644 --- a/homeassistant/components/kaiterra/manifest.json +++ b/homeassistant/components/kaiterra/manifest.json @@ -5,4 +5,4 @@ "requirements": ["kaiterra-async-client==0.0.2"], "codeowners": ["@Michsior14"], "dependencies": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/kankun/switch.py b/homeassistant/components/kankun/switch.py index 63f289862f6..4f7ba5c8b06 100644 --- a/homeassistant/components/kankun/switch.py +++ b/homeassistant/components/kankun/switch.py @@ -4,15 +4,15 @@ import logging import requests import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HOST, CONF_NAME, - CONF_PORT, - CONF_PATH, - CONF_USERNAME, CONF_PASSWORD, + CONF_PATH, + CONF_PORT, CONF_SWITCHES, + CONF_USERNAME, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py index 5a9a49a005a..466b5321245 100644 --- a/homeassistant/components/keba/__init__.py +++ b/homeassistant/components/keba/__init__.py @@ -66,7 +66,7 @@ async def async_setup(hass, config): _LOGGER.error("Could not find a charging station at %s", host) return False - # Set failsafe mode at start up of home assistant + # Set failsafe mode at start up of Home Assistant failsafe = config[DOMAIN][CONF_FS] timeout = config[DOMAIN][CONF_FS_TIMEOUT] if failsafe else 0 fallback = config[DOMAIN][CONF_FS_FALLBACK] if failsafe else 0 @@ -106,13 +106,14 @@ class KebaHandler(KebaKeContact): """Representation of a KEBA charging station connection.""" def __init__(self, hass, host, rfid, refresh_interval): - """Constructor.""" + """Initialize charging station connection.""" super().__init__(host, self.hass_callback) self._update_listeners = [] self._hass = hass self.rfid = rfid - self.device_name = "keba_wallbox_" + self.device_name = "keba" # correct device name will be set in setup() + self.device_id = "keba_wallbox_" # correct device id will be set in setup() # Ensure at least MAX_POLLING_INTERVAL seconds delay self._refresh_interval = max(MAX_POLLING_INTERVAL, refresh_interval) @@ -147,8 +148,12 @@ class KebaHandler(KebaKeContact): # Request initial values and extract serial number await self.request_data() - if self.get_value("Serial") is not None: - self.device_name = f"keba_wallbox_{self.get_value('Serial')}" + if ( + self.get_value("Serial") is not None + and self.get_value("Product") is not None + ): + self.device_id = f"keba_wallbox_{self.get_value('Serial')}" + self.device_name = self.get_value("Product") return True return False @@ -179,7 +184,7 @@ class KebaHandler(KebaKeContact): """Set energy target in async way.""" try: energy = param["energy"] - await self.set_energy(energy) + await self.set_energy(float(energy)) self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning("Energy value is not correct. %s", ex) @@ -188,7 +193,7 @@ class KebaHandler(KebaKeContact): """Set current maximum in async way.""" try: current = param["current"] - await self.set_current(current) + await self.set_current(float(current)) # No fast polling as this function might be called regularly except (KeyError, ValueError) as ex: _LOGGER.warning("Current value is not correct. %s", ex) @@ -216,10 +221,10 @@ class KebaHandler(KebaKeContact): async def async_set_failsafe(self, param=None): """Set failsafe mode in async way.""" try: - timout = param[CONF_FS_TIMEOUT] + timeout = param[CONF_FS_TIMEOUT] fallback = param[CONF_FS_FALLBACK] persist = param[CONF_FS_PERSIST] - await self.set_failsafe(timout, fallback, persist) + await self.set_failsafe(int(timeout), float(fallback), bool(persist)) self._set_fast_polling() except (KeyError, ValueError) as ex: _LOGGER.warning( diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py index 8c0503a2020..5cced416bc3 100644 --- a/homeassistant/components/keba/binary_sensor.py +++ b/homeassistant/components/keba/binary_sensor.py @@ -1,12 +1,12 @@ """Support for KEBA charging station binary sensors.""" import logging -from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_PLUG, DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_PLUG, DEVICE_CLASS_POWER, DEVICE_CLASS_SAFETY, + BinarySensorDevice, ) from . import DOMAIN @@ -22,10 +22,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] sensors = [ - KebaBinarySensor(keba, "Online", "Wallbox", DEVICE_CLASS_CONNECTIVITY), - KebaBinarySensor(keba, "Plug", "Plug", DEVICE_CLASS_PLUG), - KebaBinarySensor(keba, "State", "Charging state", DEVICE_CLASS_POWER), - KebaBinarySensor(keba, "Tmo FS", "Failsafe Mode", DEVICE_CLASS_SAFETY), + KebaBinarySensor( + keba, "Online", "Status", "device_state", DEVICE_CLASS_CONNECTIVITY + ), + KebaBinarySensor(keba, "Plug", "Plug", "plug_state", DEVICE_CLASS_PLUG), + KebaBinarySensor( + keba, "State", "Charging State", "charging_state", DEVICE_CLASS_POWER + ), + KebaBinarySensor( + keba, "Tmo FS", "Failsafe Mode", "failsafe_mode_state", DEVICE_CLASS_SAFETY + ), ] async_add_entities(sensors) @@ -33,11 +39,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KebaBinarySensor(BinarySensorDevice): """Representation of a binary sensor of a KEBA charging station.""" - def __init__(self, keba, key, sensor_name, device_class): + def __init__(self, keba, key, name, entity_type, device_class): """Initialize the KEBA Sensor.""" self._key = key self._keba = keba - self._name = sensor_name + self._name = name + self._entity_type = entity_type self._device_class = device_class self._is_on = None self._attributes = {} @@ -50,12 +57,12 @@ class KebaBinarySensor(BinarySensorDevice): @property def unique_id(self): """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def device_class(self): diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py index 3a65e44cd6f..f69fbdddf20 100644 --- a/homeassistant/components/keba/lock.py +++ b/homeassistant/components/keba/lock.py @@ -15,17 +15,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] - sensors = [KebaLock(keba, "Authentication")] + sensors = [KebaLock(keba, "Authentication", "authentication")] async_add_entities(sensors) class KebaLock(LockDevice): """The entity class for KEBA charging stations switch.""" - def __init__(self, keba, name): + def __init__(self, keba, name, entity_type): """Initialize the KEBA switch.""" self._keba = keba self._name = name + self._entity_type = entity_type self._state = True @property @@ -35,13 +36,13 @@ class KebaLock(LockDevice): @property def unique_id(self): - """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + """Return the unique ID of the lock.""" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def is_locked(self): diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json index 422a79cd0be..1f845ff8895 100644 --- a/homeassistant/components/keba/manifest.json +++ b/homeassistant/components/keba/manifest.json @@ -2,9 +2,7 @@ "domain": "keba", "name": "Keba Charging Station", "documentation": "https://www.home-assistant.io/integrations/keba", - "requirements": ["keba-kecontact==0.2.0"], + "requirements": ["keba-kecontact==1.0.0"], "dependencies": [], - "codeowners": [ - "@dannerph" - ] + "codeowners": ["@dannerph"] } diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py index f46b2f0cf90..d9e6118ff32 100644 --- a/homeassistant/components/keba/sensor.py +++ b/homeassistant/components/keba/sensor.py @@ -1,9 +1,8 @@ """Support for KEBA charging station sensors.""" import logging -from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR from homeassistant.helpers.entity import Entity -from homeassistant.const import DEVICE_CLASS_POWER from . import DOMAIN @@ -18,15 +17,40 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= keba = hass.data[DOMAIN] sensors = [ - KebaSensor(keba, "Curr user", "Max current", "mdi:flash", "A"), + KebaSensor(keba, "Curr user", "Max Current", "max_current", "mdi:flash", "A"), KebaSensor( - keba, "Setenergy", "Energy target", "mdi:gauge", ENERGY_KILO_WATT_HOUR + keba, + "Setenergy", + "Energy Target", + "energy_target", + "mdi:gauge", + ENERGY_KILO_WATT_HOUR, ), - KebaSensor(keba, "P", "Charging power", "mdi:flash", "kW", DEVICE_CLASS_POWER), KebaSensor( - keba, "E pres", "Session energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR + keba, + "P", + "Charging Power", + "charging_power", + "mdi:flash", + "kW", + DEVICE_CLASS_POWER, + ), + KebaSensor( + keba, + "E pres", + "Session Energy", + "session_energy", + "mdi:gauge", + ENERGY_KILO_WATT_HOUR, + ), + KebaSensor( + keba, + "E total", + "Total Energy", + "total_energy", + "mdi:gauge", + ENERGY_KILO_WATT_HOUR, ), - KebaSensor(keba, "E total", "Total Energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR), ] async_add_entities(sensors) @@ -34,14 +58,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class KebaSensor(Entity): """The entity class for KEBA charging stations sensors.""" - def __init__(self, keba, key, name, icon, unit, device_class=None): + def __init__(self, keba, key, name, entity_type, icon, unit, device_class=None): """Initialize the KEBA Sensor.""" - self._key = key self._keba = keba + self._key = key self._name = name - self._device_class = device_class + self._entity_type = entity_type self._icon = icon self._unit = unit + self._device_class = device_class + self._state = None self._attributes = {} @@ -53,12 +79,12 @@ class KebaSensor(Entity): @property def unique_id(self): """Return the unique ID of the binary sensor.""" - return f"{self._keba.device_name}_{self._name}" + return f"{self._keba.device_id}_{self._entity_type}" @property def name(self): """Return the name of the device.""" - return self._name + return f"{self._keba.device_name} {self._name}" @property def device_class(self): diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index df78c98aa3c..a4f81bcf2be 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -1,12 +1,8 @@ { "domain": "keenetic_ndms2", - "name": "Keenetic ndms2", + "name": "Keenetic NDMS2 Routers", "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", - "requirements": [ - "ndms2_client==0.0.11" - ], + "requirements": ["ndms2_client==0.0.11"], "dependencies": [], - "codeowners": [ - "@foxel" - ] + "codeowners": ["@foxel"] } diff --git a/homeassistant/components/kef/__init__.py b/homeassistant/components/kef/__init__.py new file mode 100644 index 00000000000..a55c8ca3210 --- /dev/null +++ b/homeassistant/components/kef/__init__.py @@ -0,0 +1 @@ +"""The KEF Wireless Speakers component.""" diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json new file mode 100644 index 00000000000..b950b144cf9 --- /dev/null +++ b/homeassistant/components/kef/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "kef", + "name": "KEF", + "documentation": "https://www.home-assistant.io/integrations/kef", + "dependencies": [], + "codeowners": ["@basnijholt"], + "requirements": ["aiokef==0.2.5", "getmac==0.8.1"] +} diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py new file mode 100644 index 00000000000..177b2fccd13 --- /dev/null +++ b/homeassistant/components/kef/media_player.py @@ -0,0 +1,272 @@ +"""Platform for the KEF Wireless Speakers.""" + +from datetime import timedelta +from functools import partial +import ipaddress +import logging + +from aiokef.aiokef import AsyncKefSpeaker +from getmac import get_mac_address +import voluptuous as vol + +from homeassistant.components.media_player import ( + PLATFORM_SCHEMA, + SUPPORT_SELECT_SOURCE, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, + SUPPORT_VOLUME_STEP, + MediaPlayerDevice, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_TYPE, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "KEF" +DEFAULT_PORT = 50001 +DEFAULT_MAX_VOLUME = 0.5 +DEFAULT_VOLUME_STEP = 0.05 +DEFAULT_INVERSE_SPEAKER_MODE = False + +DOMAIN = "kef" + +SCAN_INTERVAL = timedelta(seconds=30) + +SOURCES = {"LSX": ["Wifi", "Bluetooth", "Aux", "Opt"]} +SOURCES["LS50"] = SOURCES["LSX"] + ["Usb"] + +SUPPORT_KEF = ( + SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_VOLUME_MUTE + | SUPPORT_SELECT_SOURCE + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON +) + +CONF_MAX_VOLUME = "maximum_volume" +CONF_VOLUME_STEP = "volume_step" +CONF_INVERSE_SPEAKER_MODE = "inverse_speaker_mode" +CONF_STANDBY_TIME = "standby_time" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_TYPE): vol.In(["LS50", "LSX"]), + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MAX_VOLUME, default=DEFAULT_MAX_VOLUME): cv.small_float, + vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): cv.small_float, + vol.Optional( + CONF_INVERSE_SPEAKER_MODE, default=DEFAULT_INVERSE_SPEAKER_MODE + ): cv.boolean, + vol.Optional(CONF_STANDBY_TIME): vol.In([20, 60]), + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEF platform.""" + if DOMAIN not in hass.data: + hass.data[DOMAIN] = {} + + host = config[CONF_HOST] + speaker_type = config[CONF_TYPE] + port = config[CONF_PORT] + name = config[CONF_NAME] + maximum_volume = config[CONF_MAX_VOLUME] + volume_step = config[CONF_VOLUME_STEP] + inverse_speaker_mode = config[CONF_INVERSE_SPEAKER_MODE] + standby_time = config.get(CONF_STANDBY_TIME) + + sources = SOURCES[speaker_type] + + _LOGGER.debug( + "Setting up %s with host: %s, port: %s, name: %s, sources: %s", + DOMAIN, + host, + port, + name, + sources, + ) + + try: + if ipaddress.ip_address(host).version == 6: + mode = "ip6" + else: + mode = "ip" + except ValueError: + mode = "hostname" + mac = await hass.async_add_executor_job(partial(get_mac_address, **{mode: host})) + unique_id = f"kef-{mac}" if mac is not None else None + + media_player = KefMediaPlayer( + name, + host, + port, + maximum_volume, + volume_step, + standby_time, + inverse_speaker_mode, + sources, + ioloop=hass.loop, + unique_id=unique_id, + ) + + if host in hass.data[DOMAIN]: + _LOGGER.debug("%s is already configured", host) + else: + hass.data[DOMAIN][host] = media_player + async_add_entities([media_player], update_before_add=True) + + +class KefMediaPlayer(MediaPlayerDevice): + """Kef Player Object.""" + + def __init__( + self, + name, + host, + port, + maximum_volume, + volume_step, + standby_time, + inverse_speaker_mode, + sources, + ioloop, + unique_id, + ): + """Initialize the media player.""" + self._name = name + self._sources = sources + self._speaker = AsyncKefSpeaker( + host, + port, + volume_step, + maximum_volume, + standby_time, + inverse_speaker_mode, + ioloop=ioloop, + ) + self._unique_id = unique_id + + self._state = None + self._muted = None + self._source = None + self._volume = None + self._is_online = None + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def state(self): + """Return the state of the device.""" + return self._state + + async def async_update(self): + """Update latest state.""" + _LOGGER.debug("Running async_update") + try: + self._is_online = await self._speaker.is_online() + if self._is_online: + ( + self._volume, + self._muted, + ) = await self._speaker.get_volume_and_is_muted() + state = await self._speaker.get_state() + self._source = state.source + self._state = STATE_ON if state.is_on else STATE_OFF + else: + self._muted = None + self._source = None + self._volume = None + self._state = STATE_OFF + except (ConnectionRefusedError, ConnectionError, TimeoutError) as err: + _LOGGER.debug("Error in `update`: %s", err) + self._state = None + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def is_volume_muted(self): + """Boolean if volume is currently muted.""" + return self._muted + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return SUPPORT_KEF + + @property + def source(self): + """Name of the current input source.""" + return self._source + + @property + def source_list(self): + """List of available input sources.""" + return self._sources + + @property + def available(self): + """Return if the speaker is reachable online.""" + return self._is_online + + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + + @property + def icon(self): + """Return the device's icon.""" + return "mdi:speaker-wireless" + + async def async_turn_off(self): + """Turn the media player off.""" + await self._speaker.turn_off() + + async def async_turn_on(self): + """Turn the media player on.""" + await self._speaker.turn_on() + + async def async_volume_up(self): + """Volume up the media player.""" + await self._speaker.increase_volume() + + async def async_volume_down(self): + """Volume down the media player.""" + await self._speaker.decrease_volume() + + async def async_set_volume_level(self, volume): + """Set volume level, range 0..1.""" + await self._speaker.set_volume(volume) + + async def async_mute_volume(self, mute): + """Mute (True) or unmute (False) media player.""" + if mute: + await self._speaker.mute() + else: + await self._speaker.unmute() + + async def async_select_source(self, source: str): + """Select input source.""" + if source in self.source_list: + await self._speaker.set_source(source) + else: + raise ValueError(f"Unknown input source: {source}.") diff --git a/homeassistant/components/keyboard/manifest.json b/homeassistant/components/keyboard/manifest.json index a3cf7a51ed7..ca9f705ec46 100644 --- a/homeassistant/components/keyboard/manifest.json +++ b/homeassistant/components/keyboard/manifest.json @@ -2,9 +2,7 @@ "domain": "keyboard", "name": "Keyboard", "documentation": "https://www.home-assistant.io/integrations/keyboard", - "requirements": [ - "pyuserinput==0.1.11" - ], + "requirements": ["pyuserinput==0.1.11"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/keyboard/services.yaml b/homeassistant/components/keyboard/services.yaml index a9896c3d6cf..8e49cdd6a12 100644 --- a/homeassistant/components/keyboard/services.yaml +++ b/homeassistant/components/keyboard/services.yaml @@ -1,17 +1,17 @@ volume_up: - description: Simulates a key press of the "Volume Up" button on HomeAssistant's host machine. + description: Simulates a key press of the "Volume Up" button on Home Assistant's host machine. volume_down: - description: Simulates a key press of the "Volume Down" button on HomeAssistant's host machine. + description: Simulates a key press of the "Volume Down" button on Home Assistant's host machine. volume_mute: - description: Simulates a key press of the "Volume Mute" button on HomeAssistant's host machine. + description: Simulates a key press of the "Volume Mute" button on Home Assistant's host machine. media_play_pause: - description: Simulates a key press of the "Media Play/Pause" button on HomeAssistant's host machine. + description: Simulates a key press of the "Media Play/Pause" button on Home Assistant's host machine. media_next_track: - description: Simulates a key press of the "Media Next Track" button on HomeAssistant's host machine. + description: Simulates a key press of the "Media Next Track" button on Home Assistant's host machine. media_prev_track: - description: Simulates a key press of the "Media Previous Track" button on HomeAssistant's host machine. + description: Simulates a key press of the "Media Previous Track" button on Home Assistant's host machine. diff --git a/homeassistant/components/keyboard_remote/__init__.py b/homeassistant/components/keyboard_remote/__init__.py index d4ed6128cbe..310bd0189bd 100644 --- a/homeassistant/components/keyboard_remote/__init__.py +++ b/homeassistant/components/keyboard_remote/__init__.py @@ -1,13 +1,15 @@ """Receive signals from a keyboard and use it as a remote control.""" # pylint: disable=import-error -import logging import asyncio +import logging +import os -from evdev import InputDevice, categorize, ecodes, list_devices import aionotify +from evdev import InputDevice, categorize, ecodes, list_devices import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -118,9 +120,11 @@ class KeyboardRemote: # add initial devices (do this AFTER starting watcher in order to # avoid race conditions leading to missing device connections) initial_start_monitoring = set() - descriptors = list_devices(DEVINPUT) + descriptors = await self.hass.async_add_executor_job(list_devices, DEVINPUT) for descriptor in descriptors: - dev, handler = self.get_device_handler(descriptor) + dev, handler = await self.hass.async_add_executor_job( + self.get_device_handler, descriptor + ) if handler is None: continue @@ -164,6 +168,15 @@ class KeyboardRemote: handler = self.handlers_by_descriptor[descriptor] elif dev.name in self.handlers_by_name: handler = self.handlers_by_name[dev.name] + else: + # check for symlinked paths matching descriptor + for test_descriptor, test_handler in self.handlers_by_descriptor.items(): + if test_handler.dev is not None: + fullpath = test_handler.dev.path + else: + fullpath = os.path.realpath(test_descriptor) + if fullpath == descriptor: + handler = test_handler return (dev, handler) @@ -185,7 +198,9 @@ class KeyboardRemote: (event.flags & aionotify.Flags.CREATE) or (event.flags & aionotify.Flags.ATTRIB) ) and not descriptor_active: - dev, handler = self.get_device_handler(descriptor) + dev, handler = await self.hass.async_add_executor_job( + self.get_device_handler, descriptor + ) if handler is None: continue self.active_handlers_by_descriptor[descriptor] = handler @@ -241,7 +256,7 @@ class KeyboardRemote: """Stop event monitoring task and issue event.""" if self.monitor_task is not None: try: - self.dev.ungrab() + await self.hass.async_add_executor_job(self.dev.ungrab) except OSError: pass # monitoring of the device form the event loop and closing of the @@ -271,7 +286,7 @@ class KeyboardRemote: try: _LOGGER.debug("Start device monitoring") - dev.grab() + await self.hass.async_add_executor_job(dev.grab) async for event in dev.async_read_loop(): if event.type is ecodes.EV_KEY: if event.value in self.key_values: diff --git a/homeassistant/components/keyboard_remote/manifest.json b/homeassistant/components/keyboard_remote/manifest.json index 25b8bfa682a..12779dc6333 100644 --- a/homeassistant/components/keyboard_remote/manifest.json +++ b/homeassistant/components/keyboard_remote/manifest.json @@ -1,10 +1,8 @@ { "domain": "keyboard_remote", - "name": "Keyboard remote", + "name": "Keyboard Remote", "documentation": "https://www.home-assistant.io/integrations/keyboard_remote", - "requirements": [ - "evdev==1.1.2", "aionotify==0.2.0" - ], + "requirements": ["evdev==1.1.2", "aionotify==0.2.0"], "dependencies": [], "codeowners": ["@bendavid"] } diff --git a/homeassistant/components/kira/manifest.json b/homeassistant/components/kira/manifest.json index 78542bb7b66..38f629d8b83 100644 --- a/homeassistant/components/kira/manifest.json +++ b/homeassistant/components/kira/manifest.json @@ -2,9 +2,7 @@ "domain": "kira", "name": "Kira", "documentation": "https://www.home-assistant.io/integrations/kira", - "requirements": [ - "pykira==0.1.1" - ], + "requirements": ["pykira==0.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/kiwi/manifest.json b/homeassistant/components/kiwi/manifest.json index 888c6533013..ce754fc4c04 100644 --- a/homeassistant/components/kiwi/manifest.json +++ b/homeassistant/components/kiwi/manifest.json @@ -1,10 +1,8 @@ { "domain": "kiwi", - "name": "Kiwi", + "name": "KIWI", "documentation": "https://www.home-assistant.io/integrations/kiwi", - "requirements": [ - "kiwiki-client==0.1.1" - ], + "requirements": ["kiwiki-client==0.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 00d5d18f013..5640106eefa 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -102,7 +102,7 @@ async def async_setup(hass, config): except XKNXException as ex: _LOGGER.warning("Can't connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( - "Can't connect to KNX interface:
" "{0}".format(ex), title="KNX" + "Can't connect to KNX interface:
{0}".format(ex), title="KNX" ) for component, discovery_type in ( @@ -322,7 +322,7 @@ class KNXExposeTime: class KNXExposeSensor: - """Object to Expose HASS entity to KNX bus.""" + """Object to Expose Home Assistant entity to KNX bus.""" def __init__(self, hass, xknx, expose_type, entity_id, address): """Initialize of Expose class.""" diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 976d1286c9f..d0b8d8c1163 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -189,7 +189,7 @@ class KNXCover(CoverDevice): await self.device.set_angle(tilt_position) def start_auto_updater(self): - """Start the autoupdater to update HASS while cover is moving.""" + """Start the autoupdater to update Home Assistant while cover is moving.""" if self._unsubscribe_auto_updater is None: self._unsubscribe_auto_updater = async_track_utc_time_change( self.hass, self.auto_updater_hook diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index f99ec2f22c0..16f55b5292d 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -1,12 +1,8 @@ { "domain": "knx", - "name": "Knx", + "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": [ - "xknx==0.11.2" - ], + "requirements": ["xknx==0.11.2"], "dependencies": [], - "codeowners": [ - "@Julius2342" - ] -} \ No newline at end of file + "codeowners": ["@Julius2342"] +} diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index 5bbffc5df1d..1f2d3cb5cd0 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -1,13 +1,13 @@ """The kodi component.""" import asyncio + import voluptuous as vol -from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM -from homeassistant.helpers import config_validation as cv from homeassistant.components.kodi.const import DOMAIN from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN - +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM +from homeassistant.helpers import config_validation as cv SERVICE_ADD_MEDIA = "add_to_playlist" SERVICE_CALL_METHOD = "call_method" diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index ef138bb2ee2..80bcda0c1de 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,12 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": [ - "jsonrpc-async==0.6", - "jsonrpc-websocket==0.6" - ], + "requirements": ["jsonrpc-async==0.6", "jsonrpc-websocket==0.6"], "dependencies": [], - "codeowners": [ - "@armills" - ] + "codeowners": ["@armills"] } diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9b2ba01e90a..13aa18d01ad 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -7,15 +7,14 @@ import socket import urllib import aiohttp -import jsonrpc_base import jsonrpc_async +import jsonrpc_base import jsonrpc_websocket - import voluptuous as vol from homeassistant.components.kodi import SERVICE_CALL_METHOD from homeassistant.components.kodi.const import DOMAIN -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -52,12 +51,11 @@ from homeassistant.const import ( STATE_PLAYING, ) from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import script +from homeassistant.helpers import config_validation as cv, script from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.template import Template -from homeassistant.util.yaml import dump import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump _LOGGER = logging.getLogger(__name__) @@ -813,7 +811,7 @@ class KodiDevice(MediaPlayerDevice): except jsonrpc_base.jsonrpc.TransportError: result = None _LOGGER.warning( - "TransportError trying to run API method " "%s.%s(%s)", + "TransportError trying to run API method %s.%s(%s)", self.entity_id, method, kwargs, @@ -965,7 +963,7 @@ class KodiDevice(MediaPlayerDevice): @staticmethod def _find(key_word, words): key_word = key_word.split(" ") - patt = [re.compile("(^| )" + k + "( |$)", re.IGNORECASE) for k in key_word] + patt = [re.compile(f"(^| ){k}( |$)", re.IGNORECASE) for k in key_word] out = [[i, 0] for i in range(len(words))] for i in range(len(words)): diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 1072cf1b732..6f370ffad98 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -3,9 +3,15 @@ import logging import aiohttp import jsonrpc_async - import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( ATTR_ICON, CONF_HOST, @@ -17,14 +23,6 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) DEFAULT_PORT = 8080 diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 624d359e154..28e62c322ad 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -488,7 +488,7 @@ class KonnectedView(HomeAssistantView): device = data[CONF_DEVICES][device_id] if not device: return self.json_message( - "Device " + device_id + " not configured", status_code=HTTP_NOT_FOUND + f"Device {device_id} not configured", status_code=HTTP_NOT_FOUND ) try: diff --git a/homeassistant/components/konnected/manifest.json b/homeassistant/components/konnected/manifest.json index 397373499ae..feb6a4589cb 100644 --- a/homeassistant/components/konnected/manifest.json +++ b/homeassistant/components/konnected/manifest.json @@ -2,13 +2,7 @@ "domain": "konnected", "name": "Konnected", "documentation": "https://www.home-assistant.io/integrations/konnected", - "requirements": [ - "konnected==0.1.5" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@heythisisnate" - ] + "requirements": ["konnected==0.1.5"], + "dependencies": ["http"], + "codeowners": ["@heythisisnate"] } diff --git a/homeassistant/components/kwb/manifest.json b/homeassistant/components/kwb/manifest.json index f79b0f5b352..c13aee18ef4 100644 --- a/homeassistant/components/kwb/manifest.json +++ b/homeassistant/components/kwb/manifest.json @@ -1,10 +1,8 @@ { "domain": "kwb", - "name": "Kwb", + "name": "KWB Easyfire", "documentation": "https://www.home-assistant.io/integrations/kwb", - "requirements": [ - "pykwb==0.0.8" - ], + "requirements": ["pykwb==0.0.8"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index c30a147342b..28262b1e42d 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -1,10 +1,8 @@ { "domain": "lacrosse", - "name": "Lacrosse", + "name": "LaCrosse", "documentation": "https://www.home-assistant.io/integrations/lacrosse", - "requirements": [ - "pylacrosse==0.4.0" - ], + "requirements": ["pylacrosse==0.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/lametric/manifest.json b/homeassistant/components/lametric/manifest.json index 72e12e78ba4..8b81fb888fa 100644 --- a/homeassistant/components/lametric/manifest.json +++ b/homeassistant/components/lametric/manifest.json @@ -1,12 +1,8 @@ { "domain": "lametric", - "name": "Lametric", + "name": "LaMetric", "documentation": "https://www.home-assistant.io/integrations/lametric", - "requirements": [ - "lmnotify==0.0.4" - ], + "requirements": ["lmnotify==0.0.4"], "dependencies": [], - "codeowners": [ - "@robbiet480" - ] + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index b8dd610b1a0..052eb3bceac 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -113,7 +113,7 @@ class LaMetricNotificationService(BaseNotificationService): self._devices = lmn.get_devices() except RequestsConnectionError: _LOGGER.warning( - "Problem connecting to LaMetric, " "using cached devices instead" + "Problem connecting to LaMetric, using cached devices instead" ) for dev in self._devices: if targets is None or dev["name"] in targets: diff --git a/homeassistant/components/lannouncer/manifest.json b/homeassistant/components/lannouncer/manifest.json index 47bdd1ee0ae..e803f2e56d2 100644 --- a/homeassistant/components/lannouncer/manifest.json +++ b/homeassistant/components/lannouncer/manifest.json @@ -1,6 +1,6 @@ { "domain": "lannouncer", - "name": "Lannouncer", + "name": "LANnouncer", "documentation": "https://www.home-assistant.io/integrations/lannouncer", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/lannouncer/notify.py b/homeassistant/components/lannouncer/notify.py index 9512a75047b..9421eb16f51 100644 --- a/homeassistant/components/lannouncer/notify.py +++ b/homeassistant/components/lannouncer/notify.py @@ -5,14 +5,13 @@ from urllib.parse import urlencode import voluptuous as vol -from homeassistant.const import CONF_HOST, CONF_PORT -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv ATTR_METHOD = "method" ATTR_METHOD_DEFAULT = "speak" diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index 78ecfd4efb0..a72e8929cbc 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -1,10 +1,8 @@ { "domain": "lastfm", - "name": "Lastfm", + "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", - "requirements": [ - "pylast==3.1.0" - ], + "requirements": ["pylast==3.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/launch_library/manifest.json b/homeassistant/components/launch_library/manifest.json index 5bf63f2b099..b5fc7b36713 100644 --- a/homeassistant/components/launch_library/manifest.json +++ b/homeassistant/components/launch_library/manifest.json @@ -1,12 +1,8 @@ { "domain": "launch_library", - "name": "Launch library", + "name": "Launch Library", "documentation": "https://www.home-assistant.io/integrations/launch_library", - "requirements": [ - "pylaunches==0.2.0" - ], + "requirements": ["pylaunches==0.2.0"], "dependencies": [], - "codeowners": [ - "@ludeeus" - ] + "codeowners": ["@ludeeus"] } diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index f7170340f1b..14f25be70b0 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -4,7 +4,6 @@ import logging import pypck import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP from homeassistant.const import ( CONF_ADDRESS, @@ -22,6 +21,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py index 236035b0400..f4545817c9f 100644 --- a/homeassistant/components/lcn/helpers.py +++ b/homeassistant/components/lcn/helpers.py @@ -9,7 +9,7 @@ from .const import DEFAULT_NAME # Regex for address validation PATTERN_ADDRESS = re.compile( - "^((?P\\w+)\\.)?s?(?P\\d+)" "\\.(?Pm|g)?(?P\\d+)$" + "^((?P\\w+)\\.)?s?(?P\\d+)\\.(?Pm|g)?(?P\\d+)$" ) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index dcafe908d5c..80a15ef6bd6 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -1,12 +1,8 @@ { "domain": "lcn", - "name": "Lcn", + "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": [ - "pypck==0.6.3" - ], + "requirements": ["pypck==0.6.3"], "dependencies": [], - "codeowners": [ - "@alengwenus" - ] + "codeowners": ["@alengwenus"] } diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py index aba29e55176..c35a0cc00bf 100644 --- a/homeassistant/components/lcn/services.py +++ b/homeassistant/components/lcn/services.py @@ -2,13 +2,13 @@ import pypck import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_ADDRESS, CONF_BRIGHTNESS, CONF_STATE, CONF_UNIT_OF_MEASUREMENT, ) +import homeassistant.helpers.config_validation as cv from .const import ( CONF_CONNECTIONS, @@ -305,7 +305,7 @@ class SendKeys(LcnServiceCall): hit = pypck.lcn_defs.SendKeyCommand.HIT if pypck.lcn_defs.SendKeyCommand[call.data[CONF_STATE]] != hit: raise ValueError( - "Only hit command is allowed when sending" " deferred keys." + "Only hit command is allowed when sending deferred keys." ) delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) address_connection.send_keys_hit_deferred(keys, delay_time, delay_unit) @@ -344,7 +344,7 @@ class LockKeys(LcnServiceCall): if delay_time != 0: if table_id != 0: raise ValueError( - "Only table A is allowed when locking keys" " for a specific time." + "Only table A is allowed when locking keys for a specific time." ) delay_unit = pypck.lcn_defs.TimeUnit.parse(call.data[CONF_TIME_UNIT]) address_connection.lock_keys_tab_a_temporary(delay_time, delay_unit, states) diff --git a/homeassistant/components/lg_netcast/manifest.json b/homeassistant/components/lg_netcast/manifest.json index 3f3d9d85c52..87c73b772b8 100644 --- a/homeassistant/components/lg_netcast/manifest.json +++ b/homeassistant/components/lg_netcast/manifest.json @@ -1,10 +1,8 @@ { "domain": "lg_netcast", - "name": "Lg netcast", + "name": "LG Netcast", "documentation": "https://www.home-assistant.io/integrations/lg_netcast", - "requirements": [ - "pylgnetcast-homeassistant==0.2.0.dev0" - ], + "requirements": ["pylgnetcast-homeassistant==0.2.0.dev0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/lg_soundbar/manifest.json b/homeassistant/components/lg_soundbar/manifest.json index 603f755fac1..9f93f4e8f3f 100644 --- a/homeassistant/components/lg_soundbar/manifest.json +++ b/homeassistant/components/lg_soundbar/manifest.json @@ -1,10 +1,8 @@ { "domain": "lg_soundbar", - "name": "Lg soundbar", + "name": "LG Soundbars", "documentation": "https://www.home-assistant.io/integrations/lg_soundbar", - "requirements": [ - "temescal==0.1" - ], + "requirements": ["temescal==0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/life360/.translations/da.json b/homeassistant/components/life360/.translations/da.json index 933fce4a4e8..32acc488dc6 100644 --- a/homeassistant/components/life360/.translations/da.json +++ b/homeassistant/components/life360/.translations/da.json @@ -20,7 +20,7 @@ "username": "Brugernavn" }, "description": "Hvis du vil angive avancerede indstillinger skal du se [Life360 dokumentation]({docs_url}).\nDu \u00f8nsker m\u00e5ske at g\u00f8re dette f\u00f8r du tilf\u00f8jer konti.", - "title": "Life360 kontooplysninger" + "title": "Life360-kontooplysninger" } }, "title": "Life360" diff --git a/homeassistant/components/life360/device_tracker.py b/homeassistant/components/life360/device_tracker.py index ddd562ebfac..b7b0415a1b3 100644 --- a/homeassistant/components/life360/device_tracker.py +++ b/homeassistant/components/life360/device_tracker.py @@ -162,7 +162,7 @@ class Life360Scanner: msg = f"{key}: {err_msg}" if _errs >= self._error_threshold: if _errs == self._max_errs: - msg = "Suppressing further errors until OK: " + msg + msg = f"Suppressing further errors until OK: {msg}" _LOGGER.error(msg) elif _errs >= self._warning_threshold: _LOGGER.warning(msg) @@ -195,7 +195,10 @@ class Life360Scanner: ) reported = False - self._dev_data[dev_id] = last_seen or prev_seen, reported + # Don't remember last_seen unless it's really an update. + if not last_seen or prev_seen and last_seen <= prev_seen: + last_seen = prev_seen + self._dev_data[dev_id] = last_seen, reported return prev_seen @@ -218,7 +221,17 @@ class Life360Scanner: return # Only update when we truly have an update. - if not last_seen or prev_seen and last_seen <= prev_seen: + if not last_seen: + _LOGGER.warning("%s: Ignoring update because timestamp is missing", dev_id) + return + if prev_seen and last_seen < prev_seen: + _LOGGER.warning( + "%s: Ignoring update because timestamp is older than last timestamp", + dev_id, + ) + _LOGGER.debug("%s < %s", last_seen, prev_seen) + return + if last_seen == prev_seen: return lat = loc.get("latitude") diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index a890ec39375..016b9e13d63 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -4,10 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/life360", "dependencies": [], - "codeowners": [ - "@pnbruckner" - ], - "requirements": [ - "life360==4.1.1" - ] + "codeowners": ["@pnbruckner"], + "requirements": ["life360==4.1.1"] } diff --git a/homeassistant/components/lifx/.translations/da.json b/homeassistant/components/lifx/.translations/da.json index ffd8e20ce42..99143f38c98 100644 --- a/homeassistant/components/lifx/.translations/da.json +++ b/homeassistant/components/lifx/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Ingen LIFX enheder kunne findes p\u00e5 netv\u00e6rket.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af LIFX." + "no_devices_found": "Der blev ikke fundet nogen LIFX-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af LIFX er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index aa63be04f0d..4e845a07854 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -562,7 +562,7 @@ class LIFXLight(Light): """Return the name of the currently running effect.""" effect = self.effects_conductor.effect(self.bulb) if effect: - return "lifx_effect_" + effect.name + return f"lifx_effect_{effect.name}" return None async def update_hass(self, now=None): diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index a8c2755aefb..327eb1d4abd 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -1,16 +1,11 @@ { "domain": "lifx", - "name": "Lifx", + "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": [ - "aiolifx==0.6.7", - "aiolifx_effects==0.2.2" - ], + "requirements": ["aiolifx==0.6.7", "aiolifx_effects==0.2.2"], "homekit": { - "models": [ - "LIFX" - ] + "models": ["LIFX"] }, "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lifx_cloud/manifest.json b/homeassistant/components/lifx_cloud/manifest.json index f6aa8ccb7c5..b2f169a2811 100644 --- a/homeassistant/components/lifx_cloud/manifest.json +++ b/homeassistant/components/lifx_cloud/manifest.json @@ -1,6 +1,6 @@ { "domain": "lifx_cloud", - "name": "Lifx cloud", + "name": "LIFX Cloud", "documentation": "https://www.home-assistant.io/integrations/lifx_cloud", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/lifx_cloud/scene.py b/homeassistant/components/lifx_cloud/scene.py index ac4e0201fb8..4068ff20fe2 100644 --- a/homeassistant/components/lifx_cloud/scene.py +++ b/homeassistant/components/lifx_cloud/scene.py @@ -8,7 +8,7 @@ import async_timeout import voluptuous as vol from homeassistant.components.scene import Scene -from homeassistant.const import CONF_TOKEN, CONF_TIMEOUT, CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, CONF_TIMEOUT, CONF_TOKEN from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/lifx_legacy/manifest.json b/homeassistant/components/lifx_legacy/manifest.json index de5f5ff04de..67e510b2ba5 100644 --- a/homeassistant/components/lifx_legacy/manifest.json +++ b/homeassistant/components/lifx_legacy/manifest.json @@ -1,10 +1,8 @@ { "domain": "lifx_legacy", - "name": "Lifx legacy", + "name": "LIFX Legacy", "documentation": "https://www.home-assistant.io/integrations/lifx_legacy", - "requirements": [ - "liffylights==0.9.4" - ], + "requirements": ["liffylights==0.9.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index 14a747f6eff..eefa1e8bb6e 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,8 +1,17 @@ { "device_automation": { + "action_type": { + "toggle": "Skift {entity_name}", + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er fra", + "is_on": "{entity_name} er til" + }, "trigger_type": { - "turned_off": "{entity_name} slukket", - "turned_on": "{entity_name} t\u00e6ndt" + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte" } } } \ No newline at end of file diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index e055f67421e..b923fdb210e 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -6,12 +6,12 @@ "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" + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f01258e2ab4..791f7328cf8 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -9,7 +9,6 @@ from typing import Dict, Optional, Tuple import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_CONTROL -from homeassistant.components.group import ENTITY_ID_FORMAT as GROUP_ENTITY_ID_FORMAT from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, @@ -17,7 +16,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.exceptions import UnknownUser, Unauthorized +from homeassistant.exceptions import Unauthorized, UnknownUser import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, @@ -29,15 +28,11 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.loader import bind_hass import homeassistant.util.color as color_util - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "light" SCAN_INTERVAL = timedelta(seconds=30) -GROUP_NAME_ALL_LIGHTS = "all lights" -ENTITY_ID_ALL_LIGHTS = GROUP_ENTITY_ID_FORMAT.format("all_lights") - ENTITY_ID_FORMAT = DOMAIN + ".{}" # Bitfield of features supported by the light entity @@ -132,9 +127,8 @@ _LOGGER = logging.getLogger(__name__) @bind_hass -def is_on(hass, entity_id=None): +def is_on(hass, entity_id): """Return if the lights are on based on the statemachine.""" - entity_id = entity_id or ENTITY_ID_ALL_LIGHTS return hass.states.is_state(entity_id, STATE_ON) @@ -184,7 +178,7 @@ def preprocess_turn_off(params): async def async_setup(hass, config): """Expose light control via state machine and services.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LIGHTS + _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -337,7 +331,7 @@ class Profiles: name = entity_id + ".default" if name in cls._all: return name - name = ENTITY_ID_ALL_LIGHTS + ".default" + name = "group.all_lights.default" if name in cls._all: return name return None diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 9d8ef6bceaf..c436ce7886a 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -1,13 +1,14 @@ """Provides device actions for lights.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, Context from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import TemplateVarsType, ConfigType -from . import DOMAIN +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import DOMAIN ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index e87ae3bf945..d27953749f6 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -1,14 +1,15 @@ """Provides device conditions for lights.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import ConfigType +from homeassistant.core import HomeAssistant from homeassistant.helpers.condition import ConditionCheckerType -from . import DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 432d24d3c14..066d1f4c020 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -1,14 +1,15 @@ """Provides device trigger for lights.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import DOMAIN +from . import DOMAIN TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/light/intent.py b/homeassistant/components/light/intent.py index 93b9748fc4a..ea8899c44fc 100644 --- a/homeassistant/components/light/intent.py +++ b/homeassistant/components/light/intent.py @@ -3,20 +3,19 @@ import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -import homeassistant.util.color as color_util import homeassistant.helpers.config_validation as cv +import homeassistant.util.color as color_util from . import ( - ATTR_ENTITY_ID, - SUPPORT_COLOR, - ATTR_RGB_COLOR, ATTR_BRIGHTNESS_PCT, - SUPPORT_BRIGHTNESS, + ATTR_ENTITY_ID, + ATTR_RGB_COLOR, DOMAIN, SERVICE_TURN_ON, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, ) - INTENT_SET = "HassLightSet" diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index 88e7585f802..e0a9652a10c 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -3,8 +3,7 @@ "name": "Light", "documentation": "https://www.home-assistant.io/integrations/light", "requirements": [], - "dependencies": [ - "group" - ], - "codeowners": [] + "dependencies": ["group"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 90d14c2a19f..59a4b0306d0 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -6,16 +6,15 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from . import ( - DOMAIN, ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, @@ -29,6 +28,7 @@ from . import ( ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + DOMAIN, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/lightwave/manifest.json b/homeassistant/components/lightwave/manifest.json index 4b2456f0df5..9fea812d321 100644 --- a/homeassistant/components/lightwave/manifest.json +++ b/homeassistant/components/lightwave/manifest.json @@ -2,9 +2,7 @@ "domain": "lightwave", "name": "Lightwave", "documentation": "https://www.home-assistant.io/integrations/lightwave", - "requirements": [ - "lightwave==0.15" - ], + "requirements": ["lightwave==0.17"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/limitlessled/manifest.json b/homeassistant/components/limitlessled/manifest.json index 5eff655e806..6dd3101fabf 100644 --- a/homeassistant/components/limitlessled/manifest.json +++ b/homeassistant/components/limitlessled/manifest.json @@ -1,10 +1,8 @@ { "domain": "limitlessled", - "name": "Limitlessled", + "name": "LimitlessLED", "documentation": "https://www.home-assistant.io/integrations/limitlessled", - "requirements": [ - "limitlessled==1.1.3" - ], + "requirements": ["limitlessled==1.1.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/linksys_smart/device_tracker.py b/homeassistant/components/linksys_smart/device_tracker.py index 1af84a4c4ab..a2a8e317133 100644 --- a/homeassistant/components/linksys_smart/device_tracker.py +++ b/homeassistant/components/linksys_smart/device_tracker.py @@ -4,13 +4,13 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv DEFAULT_TIMEOUT = 10 diff --git a/homeassistant/components/linksys_smart/manifest.json b/homeassistant/components/linksys_smart/manifest.json index 28ed3f52036..32c6c1822ea 100644 --- a/homeassistant/components/linksys_smart/manifest.json +++ b/homeassistant/components/linksys_smart/manifest.json @@ -1,6 +1,6 @@ { "domain": "linksys_smart", - "name": "Linksys smart", + "name": "Linksys Smart Wi-Fi", "documentation": "https://www.home-assistant.io/integrations/linksys_smart", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/linky/.translations/fr.json b/homeassistant/components/linky/.translations/fr.json index af12c2b654d..6ff99c41a16 100644 --- a/homeassistant/components/linky/.translations/fr.json +++ b/homeassistant/components/linky/.translations/fr.json @@ -8,7 +8,7 @@ "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" + "wrong_login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe" }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/pt.json b/homeassistant/components/linky/.translations/pt.json index daf1ce75181..67e742c5813 100644 --- a/homeassistant/components/linky/.translations/pt.json +++ b/homeassistant/components/linky/.translations/pt.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Palavra-passe", - "username": "" + "username": "O email" } } } diff --git a/homeassistant/components/linky/config_flow.py b/homeassistant/components/linky/config_flow.py index 3b882eed2ad..8a2d307ceab 100644 --- a/homeassistant/components/linky/config_flow.py +++ b/homeassistant/components/linky/config_flow.py @@ -1,7 +1,6 @@ """Config flow to configure the Linky integration.""" import logging -import voluptuous as vol from pylinky.client import LinkyClient from pylinky.exceptions import ( PyLinkyAccessException, @@ -9,6 +8,7 @@ from pylinky.exceptions import ( PyLinkyException, PyLinkyWrongLoginException, ) +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME diff --git a/homeassistant/components/linky/manifest.json b/homeassistant/components/linky/manifest.json index a2505427f45..e93d124dbda 100644 --- a/homeassistant/components/linky/manifest.json +++ b/homeassistant/components/linky/manifest.json @@ -1,13 +1,9 @@ { "domain": "linky", - "name": "Linky", + "name": "Enedis Linky", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/linky", - "requirements": [ - "pylinky==0.4.0" - ], + "requirements": ["pylinky==0.4.0"], "dependencies": [], - "codeowners": [ - "@Quentame" - ] + "codeowners": ["@Quentame"] } diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index 489e66c2b12..4b5f9ab6cad 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -1,10 +1,9 @@ """Support for Linky.""" +from datetime import timedelta import json import logging -from datetime import timedelta -from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient -from pylinky.client import PyLinkyException +from pylinky.client import DAILY, MONTHLY, YEARLY, LinkyClient, PyLinkyException from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( diff --git a/homeassistant/components/linode/manifest.json b/homeassistant/components/linode/manifest.json index 064f7e1ccf0..5f486a44fa0 100644 --- a/homeassistant/components/linode/manifest.json +++ b/homeassistant/components/linode/manifest.json @@ -2,9 +2,7 @@ "domain": "linode", "name": "Linode", "documentation": "https://www.home-assistant.io/integrations/linode", - "requirements": [ - "linode-api==4.1.9b1" - ], + "requirements": ["linode-api==4.1.9b1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/linux_battery/manifest.json b/homeassistant/components/linux_battery/manifest.json index 3730f01622f..3bed6ee598a 100644 --- a/homeassistant/components/linux_battery/manifest.json +++ b/homeassistant/components/linux_battery/manifest.json @@ -1,12 +1,8 @@ { "domain": "linux_battery", - "name": "Linux battery", + "name": "Linux Battery", "documentation": "https://www.home-assistant.io/integrations/linux_battery", - "requirements": [ - "batinfo==0.4.2" - ], + "requirements": ["batinfo==0.4.2"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/lirc/manifest.json b/homeassistant/components/lirc/manifest.json index b15799b54e3..45a659b4ba2 100644 --- a/homeassistant/components/lirc/manifest.json +++ b/homeassistant/components/lirc/manifest.json @@ -1,10 +1,8 @@ { "domain": "lirc", - "name": "Lirc", + "name": "LIRC", "documentation": "https://www.home-assistant.io/integrations/lirc", - "requirements": [ - "python-lirc==1.2.3" - ], + "requirements": ["python-lirc==1.2.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/litejet/manifest.json b/homeassistant/components/litejet/manifest.json index 988e2bd1ed4..eb80539a2ad 100644 --- a/homeassistant/components/litejet/manifest.json +++ b/homeassistant/components/litejet/manifest.json @@ -1,10 +1,8 @@ { "domain": "litejet", - "name": "Litejet", + "name": "LiteJet", "documentation": "https://www.home-assistant.io/integrations/litejet", - "requirements": [ - "pylitejet==0.1" - ], + "requirements": ["pylitejet==0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/liveboxplaytv/manifest.json b/homeassistant/components/liveboxplaytv/manifest.json index bcb2b53f081..a05ff27ca90 100644 --- a/homeassistant/components/liveboxplaytv/manifest.json +++ b/homeassistant/components/liveboxplaytv/manifest.json @@ -1,13 +1,8 @@ { "domain": "liveboxplaytv", - "name": "Liveboxplaytv", + "name": "Orange Livebox Play TV", "documentation": "https://www.home-assistant.io/integrations/liveboxplaytv", - "requirements": [ - "liveboxplaytv==2.0.2", - "pyteleloisirs==3.5" - ], + "requirements": ["liveboxplaytv==2.0.3", "pyteleloisirs==3.6"], "dependencies": [], - "codeowners": [ - "@pschmitt" - ] + "codeowners": ["@pschmitt"] } diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index 996b4f33b50..66fb383d677 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -7,7 +7,7 @@ import pyteleloisirs import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, diff --git a/homeassistant/components/llamalab_automate/manifest.json b/homeassistant/components/llamalab_automate/manifest.json index 2f46e6e790c..6fd6c30401f 100644 --- a/homeassistant/components/llamalab_automate/manifest.json +++ b/homeassistant/components/llamalab_automate/manifest.json @@ -1,6 +1,6 @@ { "domain": "llamalab_automate", - "name": "Llamalab automate", + "name": "LlamaLab Automate", "documentation": "https://www.home-assistant.io/integrations/llamalab_automate", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/llamalab_automate/notify.py b/homeassistant/components/llamalab_automate/notify.py index ab6a7032208..5a3d4e0df38 100644 --- a/homeassistant/components/llamalab_automate/notify.py +++ b/homeassistant/components/llamalab_automate/notify.py @@ -4,11 +4,10 @@ import logging import requests import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_API_KEY, CONF_DEVICE from homeassistant.helpers import config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://llamalab.com/automate/cloud/message" diff --git a/homeassistant/components/local_file/camera.py b/homeassistant/components/local_file/camera.py index 9bd476cfb27..1d06efeb708 100644 --- a/homeassistant/components/local_file/camera.py +++ b/homeassistant/components/local_file/camera.py @@ -5,12 +5,12 @@ import os import voluptuous as vol -from homeassistant.const import CONF_NAME, ATTR_ENTITY_ID from homeassistant.components.camera import ( - Camera, CAMERA_SERVICE_SCHEMA, PLATFORM_SCHEMA, + Camera, ) +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.helpers import config_validation as cv from .const import ( diff --git a/homeassistant/components/local_file/manifest.json b/homeassistant/components/local_file/manifest.json index ded748be8dc..f4773fac863 100644 --- a/homeassistant/components/local_file/manifest.json +++ b/homeassistant/components/local_file/manifest.json @@ -1,6 +1,6 @@ { "domain": "local_file", - "name": "Local file", + "name": "Local File", "documentation": "https://www.home-assistant.io/integrations/local_file", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/local_ip/.translations/ca.json b/homeassistant/components/local_ip/.translations/ca.json new file mode 100644 index 00000000000..b2b7ee89c16 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integraci\u00f3 ja configurada amb un sensor amb aquest nom" + }, + "step": { + "user": { + "data": { + "name": "Nom del sensor" + }, + "title": "Adre\u00e7a IP local" + } + }, + "title": "Adre\u00e7a IP local" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/da.json b/homeassistant/components/local_ip/.translations/da.json new file mode 100644 index 00000000000..c0396ccb182 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integration er allerede konfigureret med en eksisterende sensor med det navn" + }, + "step": { + "user": { + "data": { + "name": "Sensornavn" + }, + "title": "Lokal IP-adresse" + } + }, + "title": "Lokal IP-adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/de.json b/homeassistant/components/local_ip/.translations/de.json new file mode 100644 index 00000000000..c7a993bdef9 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integration ist bereits mit einem vorhandenen Sensor mit diesem Namen konfiguriert" + }, + "step": { + "user": { + "data": { + "name": "Sensorname" + }, + "title": "Lokale IP-Adresse" + } + }, + "title": "Lokale IP-Adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/en.json b/homeassistant/components/local_ip/.translations/en.json new file mode 100644 index 00000000000..869bb5a23d5 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integration is already configured with an existing sensor with that name" + }, + "step": { + "user": { + "data": { + "name": "Sensor Name" + }, + "title": "Local IP Address" + } + }, + "title": "Local IP Address" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/es.json b/homeassistant/components/local_ip/.translations/es.json new file mode 100644 index 00000000000..a18c809de85 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3n ya est\u00e1 configurada con un sensor existente con ese nombre" + }, + "step": { + "user": { + "data": { + "name": "Nombre del sensor" + }, + "title": "Direcci\u00f3n IP local" + } + }, + "title": "Direcci\u00f3n IP local" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/fr.json b/homeassistant/components/local_ip/.translations/fr.json new file mode 100644 index 00000000000..0d3c61c385b --- /dev/null +++ b/homeassistant/components/local_ip/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "L'int\u00e9gration est d\u00e9j\u00e0 configur\u00e9e avec un capteur existant portant ce nom" + }, + "step": { + "user": { + "data": { + "name": "Nom du capteur" + }, + "title": "Adresse IP locale" + } + }, + "title": "Adresse IP locale" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/it.json b/homeassistant/components/local_ip/.translations/it.json new file mode 100644 index 00000000000..a33e892c6ec --- /dev/null +++ b/homeassistant/components/local_ip/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione \u00e8 gi\u00e0 configurata con un sensore esistente con questo nome" + }, + "step": { + "user": { + "data": { + "name": "Nome del sensore" + }, + "title": "Indirizzo IP locale" + } + }, + "title": "Indirizzo IP locale" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/ko.json b/homeassistant/components/local_ip/.translations/ko.json new file mode 100644 index 00000000000..a00a130bfca --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uac00 \uc774\ubbf8 \ud574\ub2f9 \uc774\ub984\uc758 \uc13c\uc11c\ub85c \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "name": "\uc13c\uc11c \uc774\ub984" + }, + "title": "\ub85c\uceec IP \uc8fc\uc18c" + } + }, + "title": "\ub85c\uceec IP \uc8fc\uc18c" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/lb.json b/homeassistant/components/local_ip/.translations/lb.json new file mode 100644 index 00000000000..aa249f184ce --- /dev/null +++ b/homeassistant/components/local_ip/.translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integratioun ass scho konfigur\u00e9iert mat engem Sensor mat deem Numm" + }, + "step": { + "user": { + "data": { + "name": "Numm vum Sensor" + }, + "title": "Lokal IP Adresse" + } + }, + "title": "Lokal IP Adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/nl.json b/homeassistant/components/local_ip/.translations/nl.json new file mode 100644 index 00000000000..fdffd97427b --- /dev/null +++ b/homeassistant/components/local_ip/.translations/nl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Sensor Naam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/no.json b/homeassistant/components/local_ip/.translations/no.json new file mode 100644 index 00000000000..2427a7ae50d --- /dev/null +++ b/homeassistant/components/local_ip/.translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integrasjonen er allerede konfigurert med en eksisterende sensor med det navnet" + }, + "step": { + "user": { + "data": { + "name": "Sensornavn" + }, + "title": "Lokal IP-adresse" + } + }, + "title": "Lokal IP-adresse" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/pl.json b/homeassistant/components/local_ip/.translations/pl.json new file mode 100644 index 00000000000..a4032eeebd1 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integracja jest ju\u017c skonfigurowana z istniej\u0105cym sensorem o tej nazwie" + }, + "step": { + "user": { + "data": { + "name": "Nazwa sensora" + }, + "title": "Lokalny adres IP" + } + }, + "title": "Lokalny adres IP" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/pt-BR.json b/homeassistant/components/local_ip/.translations/pt-BR.json new file mode 100644 index 00000000000..3aea4075456 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "A integra\u00e7\u00e3o j\u00e1 est\u00e1 configurada com um sensor existente com esse nome" + }, + "step": { + "user": { + "data": { + "name": "Nome do sensor" + }, + "title": "Endere\u00e7o IP local" + } + }, + "title": "Endere\u00e7o IP local" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/ru.json b/homeassistant/components/local_ip/.translations/ru.json new file mode 100644 index 00000000000..1613d974449 --- /dev/null +++ b/homeassistant/components/local_ip/.translations/ru.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0434\u0430\u0442\u0447\u0438\u043a\u0430." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0430" + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" + } + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0439 IP-\u0430\u0434\u0440\u0435\u0441" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/sl.json b/homeassistant/components/local_ip/.translations/sl.json new file mode 100644 index 00000000000..23894b20c9c --- /dev/null +++ b/homeassistant/components/local_ip/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Integracija je \u017ee konfigurirana z obstoje\u010dim senzorjem s tem imenom" + }, + "step": { + "user": { + "data": { + "name": "Ime tipala" + }, + "title": "Lokalni IP naslov" + } + }, + "title": "Lokalni IP naslov" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/.translations/zh-Hant.json b/homeassistant/components/local_ip/.translations/zh-Hant.json new file mode 100644 index 00000000000..ec50980b6ea --- /dev/null +++ b/homeassistant/components/local_ip/.translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u6574\u5408\u5df2\u7d93\u8a2d\u5b9a\u4e26\u6709\u73fe\u6709\u50b3\u611f\u5668\u4f7f\u7528\u76f8\u540c\u540d\u7a31" + }, + "step": { + "user": { + "data": { + "name": "\u50b3\u611f\u5668\u540d\u7a31" + }, + "title": "\u672c\u5730 IP \u4f4d\u5740" + } + }, + "title": "\u672c\u5730 IP \u4f4d\u5740" + } +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/__init__.py b/homeassistant/components/local_ip/__init__.py new file mode 100644 index 00000000000..c93b7a5a81b --- /dev/null +++ b/homeassistant/components/local_ip/__init__.py @@ -0,0 +1,42 @@ +"""Get the local IP address of the Home Assistant instance.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv + +DOMAIN = "local_ip" +PLATFORM = "sensor" + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Optional(CONF_NAME, default=DOMAIN): cv.string})}, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up local_ip from configuration.yaml.""" + conf = config.get(DOMAIN) + if conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, data=conf, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Set up local_ip from a config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, PLATFORM) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) diff --git a/homeassistant/components/local_ip/config_flow.py b/homeassistant/components/local_ip/config_flow.py new file mode 100644 index 00000000000..58a666a68f3 --- /dev/null +++ b/homeassistant/components/local_ip/config_flow.py @@ -0,0 +1,34 @@ +"""Config flow for local_ip.""" +import voluptuous as vol + +from homeassistant import config_entries + +from . import DOMAIN + + +class SimpleConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for local_ip.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + if any( + user_input["name"] == entry.data["name"] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + return self.async_create_entry(title=user_input["name"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required("name", default=DOMAIN): str}), + errors={}, + ) + + async def async_step_import(self, import_info): + """Handle import from config file.""" + return await self.async_step_user(import_info) diff --git a/homeassistant/components/local_ip/manifest.json b/homeassistant/components/local_ip/manifest.json new file mode 100644 index 00000000000..4e97c32afa0 --- /dev/null +++ b/homeassistant/components/local_ip/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "local_ip", + "name": "Local IP Address", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/local_ip", + "dependencies": [], + "codeowners": ["@issacg"], + "requirements": [] +} diff --git a/homeassistant/components/local_ip/sensor.py b/homeassistant/components/local_ip/sensor.py new file mode 100644 index 00000000000..274a11faec6 --- /dev/null +++ b/homeassistant/components/local_ip/sensor.py @@ -0,0 +1,34 @@ +"""Sensor platform for local_ip.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity +from homeassistant.util import get_local_ip + + +async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): + """Set up the platform from config_entry.""" + name = config_entry.data["name"] + async_add_entities([IPSensor(name)], True) + + +class IPSensor(Entity): + """A simple sensor.""" + + def __init__(self, name: str): + """Initialize the sensor.""" + self._state = None + self._name = name + + @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 + + def update(self): + """Fetch new state data for the sensor.""" + self._state = get_local_ip() diff --git a/homeassistant/components/local_ip/strings.json b/homeassistant/components/local_ip/strings.json new file mode 100644 index 00000000000..43a88be3325 --- /dev/null +++ b/homeassistant/components/local_ip/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "title": "Local IP Address", + "step": { + "user": { + "title": "Local IP Address", + "data": { + "name": "Sensor Name" + } + } + }, + "abort": { + "already_configured": "Integration is already configured with an existing sensor with that name" + } + } +} diff --git a/homeassistant/components/locative/.translations/da.json b/homeassistant/components/locative/.translations/da.json index 8211d52fa5d..3752b23bbe3 100644 --- a/homeassistant/components/locative/.translations/da.json +++ b/homeassistant/components/locative/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Geofency meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Geofency-meddelelser.", "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { diff --git a/homeassistant/components/locative/.translations/ko.json b/homeassistant/components/locative/.translations/ko.json index 0649ed557c4..c53f538799f 100644 --- a/homeassistant/components/locative/.translations/ko.json +++ b/homeassistant/components/locative/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Locative Webhook \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Locative Webhook \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Locative Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index ed8bcb6e7e5..ea36aa9f7fb 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -2,21 +2,21 @@ import logging from typing import Dict -import voluptuous as vol from aiohttp import web +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( - HTTP_UNPROCESSABLE_ENTITY, + ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, - STATE_NOT_HOME, CONF_WEBHOOK_ID, - ATTR_ID, HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, + STATE_NOT_HOME, ) from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/locative/config_flow.py b/homeassistant/components/locative/config_flow.py index b4fb43d5e4e..a1ac8263416 100644 --- a/homeassistant/components/locative/config_flow.py +++ b/homeassistant/components/locative/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Locative.""" from homeassistant.helpers import config_entry_flow -from .const import DOMAIN +from .const import DOMAIN config_entry_flow.register_webhook_flow( DOMAIN, diff --git a/homeassistant/components/locative/device_tracker.py b/homeassistant/components/locative/device_tracker.py index c92847930a0..ef247954171 100644 --- a/homeassistant/components/locative/device_tracker.py +++ b/homeassistant/components/locative/device_tracker.py @@ -1,9 +1,9 @@ """Support for the Locative platform.""" import logging -from homeassistant.core import callback from homeassistant.components.device_tracker import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN as LT_DOMAIN, TRACKER_UPDATE diff --git a/homeassistant/components/locative/manifest.json b/homeassistant/components/locative/manifest.json index 46a2d4de20e..ab37ce2e4e6 100644 --- a/homeassistant/components/locative/manifest.json +++ b/homeassistant/components/locative/manifest.json @@ -4,8 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/locative", "requirements": [], - "dependencies": [ - "webhook" - ], + "dependencies": ["webhook"], "codeowners": [] } diff --git a/homeassistant/components/lock/.translations/da.json b/homeassistant/components/lock/.translations/da.json index de4f603ac43..e2f2588349c 100644 --- a/homeassistant/components/lock/.translations/da.json +++ b/homeassistant/components/lock/.translations/da.json @@ -2,11 +2,16 @@ "device_automation": { "action_type": { "lock": "L\u00e5s {entity_name}", - "open": "\u00c5ben {entity_name}" + "open": "\u00c5bn {entity_name}", + "unlock": "L\u00e5s {entity_name} op" }, "condition_type": { "is_locked": "{entity_name} er l\u00e5st", "is_unlocked": "{entity_name} er l\u00e5st op" + }, + "trigger_type": { + "locked": "{entity_name} blev l\u00e5st", + "unlocked": "{entity_name} l\u00e5st op" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/hu.json b/homeassistant/components/lock/.translations/hu.json new file mode 100644 index 00000000000..20b1663558b --- /dev/null +++ b/homeassistant/components/lock/.translations/hu.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "lock": "{entity_name} z\u00e1r\u00e1sa", + "open": "{entity_name} nyit\u00e1sa", + "unlock": "{entity_name} nyit\u00e1sa" + }, + "condition_type": { + "is_locked": "{entity_name} z\u00e1rva", + "is_unlocked": "{entity_name} nyitva" + }, + "trigger_type": { + "locked": "{entity_name} be lett z\u00e1rva", + "unlocked": "{entity_name} ki lett nyitva" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ko.json b/homeassistant/components/lock/.translations/ko.json index 6abd9cd60e6..fb202f73b37 100644 --- a/homeassistant/components/lock/.translations/ko.json +++ b/homeassistant/components/lock/.translations/ko.json @@ -6,8 +6,12 @@ "unlock": "{entity_name} \uc7a0\uae08 \ud574\uc81c" }, "condition_type": { - "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", - "is_unlocked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc73c\uba74", + "is_unlocked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74" + }, + "trigger_type": { + "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c", + "unlocked": "{entity_name} \uc774(\uac00) \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index c443a5219d7..c788a7c3e8c 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -5,26 +5,24 @@ import logging import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, - PLATFORM_SCHEMA, - PLATFORM_SCHEMA_BASE, -) -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, STATE_LOCKED, STATE_UNLOCKED, - SERVICE_LOCK, - SERVICE_UNLOCK, - SERVICE_OPEN, ) -from homeassistant.components import group - +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, + make_entity_service_schema, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs @@ -33,11 +31,8 @@ ATTR_CHANGED_BY = "changed_by" DOMAIN = "lock" SCAN_INTERVAL = timedelta(seconds=30) -ENTITY_ID_ALL_LOCKS = group.ENTITY_ID_FORMAT.format("all_locks") ENTITY_ID_FORMAT = DOMAIN + ".{}" -GROUP_NAME_ALL_LOCKS = "all locks" - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) LOCK_SERVICE_SCHEMA = make_entity_service_schema({vol.Optional(ATTR_CODE): cv.string}) @@ -51,16 +46,15 @@ PROP_TO_ATTR = {"changed_by": ATTR_CHANGED_BY, "code_format": ATTR_CODE_FORMAT} @bind_hass -def is_locked(hass, entity_id=None): +def is_locked(hass, entity_id): """Return if the lock is locked based on the statemachine.""" - entity_id = entity_id or ENTITY_ID_ALL_LOCKS return hass.states.is_state(entity_id, STATE_LOCKED) async def async_setup(hass, config): """Track states and offer events for locks.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_LOCKS + _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py index c678bfc17cf..efdb5e352cf 100644 --- a/homeassistant/components/lock/device_action.py +++ b/homeassistant/components/lock/device_action.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, SERVICE_LOCK, SERVICE_OPEN, SERVICE_UNLOCK, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv + from . import DOMAIN, SUPPORT_OPEN ACTION_TYPES = {"lock", "unlock", "open"} diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py index 328da6ad450..44791320669 100644 --- a/homeassistant/components/lock/device_condition.py +++ b/homeassistant/components/lock/device_condition.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" from typing import List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, STATE_LOCKED, STATE_UNLOCKED, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_locked", "is_unlocked"} diff --git a/homeassistant/components/lock/device_trigger.py b/homeassistant/components/lock/device_trigger.py index 8732cca29f0..9db2822a591 100644 --- a/homeassistant/components/lock/device_trigger.py +++ b/homeassistant/components/lock/device_trigger.py @@ -1,21 +1,23 @@ """Provides device automations for Lock.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, STATE_LOCKED, STATE_UNLOCKED, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN TRIGGER_TYPES = {"locked", "unlocked"} diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index 0a76628a5b5..ab05166d15f 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -3,8 +3,7 @@ "name": "Lock", "documentation": "https://www.home-assistant.io/integrations/lock", "requirements": [], - "dependencies": [ - "group" - ], - "codeowners": [] + "dependencies": ["group"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py index dc1bee85839..b8b469f943f 100644 --- a/homeassistant/components/lock/reproduce_state.py +++ b/homeassistant/components/lock/reproduce_state.py @@ -5,10 +5,10 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_LOCKED, - STATE_UNLOCKED, SERVICE_LOCK, SERVICE_UNLOCK, + STATE_LOCKED, + STATE_UNLOCKED, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/lockitron/lock.py b/homeassistant/components/lockitron/lock.py index b993f644ecd..5840c7f5537 100644 --- a/homeassistant/components/lockitron/lock.py +++ b/homeassistant/components/lockitron/lock.py @@ -4,9 +4,9 @@ import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import CONF_ACCESS_TOKEN, CONF_ID +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 8675f778a26..ac45a636bf7 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -195,7 +195,7 @@ def humanify(hass, events): Will try to group events if possible: - if 2+ sensor updates in GROUP_BY_MINUTES, show last - - if home assistant stop and start happen in same minute call it restarted + - if Home Assistant stop and start happen in same minute call it restarted """ domain_prefixes = tuple(f"{dom}." for dom in CONTINUOUS_DOMAINS) diff --git a/homeassistant/components/logentries/__init__.py b/homeassistant/components/logentries/__init__.py index 3601ee275b8..55d1ab7aae6 100644 --- a/homeassistant/components/logentries/__init__.py +++ b/homeassistant/components/logentries/__init__.py @@ -1,13 +1,13 @@ """Support for sending data to Logentries webhook endpoint.""" import json import logging -import requests +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_TOKEN, EVENT_STATE_CHANGED from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index a2ed19f92b1..8043469d43b 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,6 +1,6 @@ """Support for settting the level of logging for components.""" -import logging from collections import OrderedDict +import logging import voluptuous as vol diff --git a/homeassistant/components/logger/manifest.json b/homeassistant/components/logger/manifest.json index ac201fb00a1..45f5cc934b2 100644 --- a/homeassistant/components/logger/manifest.json +++ b/homeassistant/components/logger/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/logger", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/logi_circle/.translations/da.json b/homeassistant/components/logi_circle/.translations/da.json index 9de8d707ad4..1f2a96fe5b4 100644 --- a/homeassistant/components/logi_circle/.translations/da.json +++ b/homeassistant/components/logi_circle/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_setup": "Du kan kun konfigurere en enkelt Logi Circle konto.", - "external_error": "Der opstod en undtagelse fra et andet flow.", + "already_setup": "Du kan kun konfigurere en enkelt Logi Circle-konto.", + "external_error": "Undtagelse skete fra et andet flow.", "external_setup": "Logi Circle er konfigureret med succes fra et andet flow.", "no_flows": "Du skal konfigurere Logi Circle f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/logi_circle/)." }, @@ -24,7 +24,7 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg via hvilken godkendelsesudbyder du vil godkende med Logi Circle.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" } }, "title": "Logi Circle" diff --git a/homeassistant/components/logi_circle/config_flow.py b/homeassistant/components/logi_circle/config_flow.py index ce8460233d6..bc585153b64 100644 --- a/homeassistant/components/logi_circle/config_flow.py +++ b/homeassistant/components/logi_circle/config_flow.py @@ -207,5 +207,5 @@ class LogiCircleAuthCallbackView(HomeAssistantView): ) return self.json_message("Authorisation code saved") return self.json_message( - "Authorisation code missing " "from query string", status_code=400 + "Authorisation code missing from query string", status_code=400 ) diff --git a/homeassistant/components/logi_circle/manifest.json b/homeassistant/components/logi_circle/manifest.json index 22502956e06..bd6dc8a8d27 100644 --- a/homeassistant/components/logi_circle/manifest.json +++ b/homeassistant/components/logi_circle/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/logi_circle", "requirements": ["logi_circle==0.2.2"], - "dependencies": ["ffmpeg"], + "dependencies": ["ffmpeg", "http"], "codeowners": ["@evanjd"] } diff --git a/homeassistant/components/london_air/manifest.json b/homeassistant/components/london_air/manifest.json index cca4a54bda8..c5b9caffa88 100644 --- a/homeassistant/components/london_air/manifest.json +++ b/homeassistant/components/london_air/manifest.json @@ -1,6 +1,6 @@ { "domain": "london_air", - "name": "London air", + "name": "London Air", "documentation": "https://www.home-assistant.io/integrations/london_air", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/london_underground/manifest.json b/homeassistant/components/london_underground/manifest.json index e66d5c1820d..a81034e0718 100644 --- a/homeassistant/components/london_underground/manifest.json +++ b/homeassistant/components/london_underground/manifest.json @@ -1,10 +1,8 @@ { "domain": "london_underground", - "name": "London underground", + "name": "London Underground", "documentation": "https://www.home-assistant.io/integrations/london_underground", - "requirements": [ - "london-tube-status==0.2" - ], + "requirements": ["london-tube-status==0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/loopenergy/manifest.json b/homeassistant/components/loopenergy/manifest.json index 41e3d0dd6b0..88b679fb071 100644 --- a/homeassistant/components/loopenergy/manifest.json +++ b/homeassistant/components/loopenergy/manifest.json @@ -1,10 +1,8 @@ { "domain": "loopenergy", - "name": "Loopenergy", + "name": "Loop Energy", "documentation": "https://www.home-assistant.io/integrations/loopenergy", - "requirements": [ - "pyloopenergy==0.1.3" - ], + "requirements": ["pyloopenergy==0.1.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index dedf3b6a58a..6c99356907f 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -36,23 +36,6 @@ EVENT_LOVELACE_UPDATED = "lovelace_updated" LOVELACE_CONFIG_FILE = "ui-lovelace.yaml" -WS_TYPE_GET_LOVELACE_UI = "lovelace/config" -WS_TYPE_SAVE_CONFIG = "lovelace/config/save" - -SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_GET_LOVELACE_UI, - vol.Optional("force", default=False): bool, - } -) - -SCHEMA_SAVE_CONFIG = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_SAVE_CONFIG, - vol.Required("config"): vol.Any(str, dict), - } -) - class ConfigNotFound(HomeAssistantError): """When no config available.""" @@ -72,12 +55,12 @@ async def async_setup(hass, config): else: hass.data[DOMAIN] = LovelaceStorage(hass) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_LOVELACE_UI, websocket_lovelace_config, SCHEMA_GET_LOVELACE_UI - ) + hass.components.websocket_api.async_register_command(websocket_lovelace_config) + + hass.components.websocket_api.async_register_command(websocket_lovelace_save_config) hass.components.websocket_api.async_register_command( - WS_TYPE_SAVE_CONFIG, websocket_lovelace_save_config, SCHEMA_SAVE_CONFIG + websocket_lovelace_delete_config ) hass.components.system_health.async_register_info(DOMAIN, system_health_info) @@ -124,6 +107,10 @@ class LovelaceStorage: self._hass.bus.async_fire(EVENT_LOVELACE_UPDATED) await self._store.async_save(self._data) + async def async_delete(self): + """Delete config.""" + await self.async_save(None) + async def _load(self): """Load the config.""" data = await self._store.async_load() @@ -185,6 +172,10 @@ class LovelaceYAML: """Save config.""" raise HomeAssistantError("Not supported") + async def async_delete(self): + """Delete config.""" + raise HomeAssistantError("Not supported") + def handle_yaml_errors(func): """Handle error with WebSocket calls.""" @@ -212,6 +203,9 @@ def handle_yaml_errors(func): @websocket_api.async_response +@websocket_api.websocket_command( + {"type": "lovelace/config", vol.Optional("force", default=False): bool} +) @handle_yaml_errors async def websocket_lovelace_config(hass, connection, msg): """Send Lovelace UI config over WebSocket configuration.""" @@ -219,12 +213,23 @@ async def websocket_lovelace_config(hass, connection, msg): @websocket_api.async_response +@websocket_api.websocket_command( + {"type": "lovelace/config/save", "config": vol.Any(str, dict)} +) @handle_yaml_errors async def websocket_lovelace_save_config(hass, connection, msg): """Save Lovelace UI configuration.""" await hass.data[DOMAIN].async_save(msg["config"]) +@websocket_api.async_response +@websocket_api.websocket_command({"type": "lovelace/config/delete"}) +@handle_yaml_errors +async def websocket_lovelace_delete_config(hass, connection, msg): + """Delete Lovelace UI configuration.""" + await hass.data[DOMAIN].async_delete() + + async def system_health_info(hass): """Get info for the info page.""" return await hass.data[DOMAIN].async_get_info() diff --git a/homeassistant/components/lovelace/manifest.json b/homeassistant/components/lovelace/manifest.json index 72be440d54c..aa0d706976a 100644 --- a/homeassistant/components/lovelace/manifest.json +++ b/homeassistant/components/lovelace/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/lovelace", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/frontend" - ] + "codeowners": ["@home-assistant/frontend"] } diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index d7cf72ebaf5..7fefe89c1df 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -1,13 +1,8 @@ { "domain": "luci", - "name": "Luci", + "name": "OpenWRT (luci)", "documentation": "https://www.home-assistant.io/integrations/luci", - "requirements": [ - "openwrt-luci-rpc==1.1.2" - ], + "requirements": ["openwrt-luci-rpc==1.1.2"], "dependencies": [], - "codeowners": [ - "@fbradyirl", - "@mzdrale" - ] + "codeowners": ["@fbradyirl", "@mzdrale"] } diff --git a/homeassistant/components/luftdaten/.translations/da.json b/homeassistant/components/luftdaten/.translations/da.json index d43fc1128ae..3a5f5e7b409 100644 --- a/homeassistant/components/luftdaten/.translations/da.json +++ b/homeassistant/components/luftdaten/.translations/da.json @@ -9,7 +9,7 @@ "user": { "data": { "show_on_map": "Vis p\u00e5 kort", - "station_id": "Luftdaten Sensor ID" + "station_id": "Luftdaten sensor-id" }, "title": "Definer Luftdaten" } diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 3dca82404c0..d797fbbf4ba 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -34,6 +34,7 @@ SENSOR_HUMIDITY = "humidity" SENSOR_PM10 = "P1" SENSOR_PM2_5 = "P2" SENSOR_PRESSURE = "pressure" +SENSOR_PRESSURE_AT_SEALEVEL = "pressure_at_sealevel" SENSOR_TEMPERATURE = "temperature" TOPIC_UPDATE = f"{DOMAIN}_data_update" @@ -44,6 +45,7 @@ SENSORS = { SENSOR_TEMPERATURE: ["Temperature", "mdi:thermometer", TEMP_CELSIUS], SENSOR_HUMIDITY: ["Humidity", "mdi:water-percent", "%"], SENSOR_PRESSURE: ["Pressure", "mdi:arrow-down-bold", "Pa"], + SENSOR_PRESSURE_AT_SEALEVEL: ["Pressure at sealevel", "mdi:mdi-download", "Pa"], SENSOR_PM10: ["PM10", "mdi:thought-bubble", VOLUME_MICROGRAMS_PER_CUBIC_METER], SENSOR_PM2_5: [ "PM2.5", diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index 112b58ba65d..13fa67a8b6b 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -3,11 +3,8 @@ "name": "Luftdaten", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/luftdaten", - "requirements": [ - "luftdaten==0.6.3" - ], + "requirements": ["luftdaten==0.6.3"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "gold" } diff --git a/homeassistant/components/lupusec/manifest.json b/homeassistant/components/lupusec/manifest.json index 2fea92afd0e..391de3cfc55 100644 --- a/homeassistant/components/lupusec/manifest.json +++ b/homeassistant/components/lupusec/manifest.json @@ -1,10 +1,8 @@ { "domain": "lupusec", - "name": "Lupusec", + "name": "Lupus Electronics LUPUSEC", "documentation": "https://www.home-assistant.io/integrations/lupusec", - "requirements": [ - "lupupy==0.0.18" - ], + "requirements": ["lupupy==0.0.18"], "dependencies": [], "codeowners": ["@majuss"] } diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index cae2fc5cfdd..938132259d9 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -19,12 +19,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def to_lutron_level(level): - """Convert the given HASS light level (0-255) to Lutron (0.0-100.0).""" + """Convert the given Home Assistant light level (0-255) to Lutron (0.0-100.0).""" return float((level * 100) / 255) def to_hass_level(level): - """Convert the given Lutron (0.0-100.0) light level to HASS (0-255).""" + """Convert the given Lutron (0.0-100.0) light level to Home Assistant (0-255).""" return int((level * 255) / 100) diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index f8439e3c1ec..9eb4fdeaa45 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -2,11 +2,7 @@ "domain": "lutron", "name": "Lutron", "documentation": "https://www.home-assistant.io/integrations/lutron", - "requirements": [ - "pylutron==0.2.5" - ], + "requirements": ["pylutron==0.2.5"], "dependencies": [], - "codeowners": [ - "@JonGilmore" - ] + "codeowners": ["@JonGilmore"] } diff --git a/homeassistant/components/lutron_caseta/.translations/da.json b/homeassistant/components/lutron_caseta/.translations/da.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/da.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/de.json b/homeassistant/components/lutron_caseta/.translations/de.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/de.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/ko.json b/homeassistant/components/lutron_caseta/.translations/ko.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/ko.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/.translations/nl.json b/homeassistant/components/lutron_caseta/.translations/nl.json new file mode 100644 index 00000000000..cfc3c290afe --- /dev/null +++ b/homeassistant/components/lutron_caseta/.translations/nl.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Lutron Cas\u00e9ta" + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index e9df5ad1d46..3dd8c8fac2e 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -1,10 +1,8 @@ { "domain": "lutron_caseta", - "name": "Lutron caseta", + "name": "Lutron Caseta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": [ - "pylutron-caseta==0.5.1" - ], + "requirements": ["pylutron-caseta==0.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/lw12wifi/manifest.json b/homeassistant/components/lw12wifi/manifest.json index 7f830cda25b..014dde12fcb 100644 --- a/homeassistant/components/lw12wifi/manifest.json +++ b/homeassistant/components/lw12wifi/manifest.json @@ -1,10 +1,8 @@ { "domain": "lw12wifi", - "name": "Lw12wifi", + "name": "LAGUTE LW-12", "documentation": "https://www.home-assistant.io/integrations/lw12wifi", - "requirements": [ - "lw12==0.9.2" - ], + "requirements": ["lw12==0.9.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/lyft/manifest.json b/homeassistant/components/lyft/manifest.json index e8b593b3145..ec9fb422d21 100644 --- a/homeassistant/components/lyft/manifest.json +++ b/homeassistant/components/lyft/manifest.json @@ -2,9 +2,7 @@ "domain": "lyft", "name": "Lyft", "documentation": "https://www.home-assistant.io/integrations/lyft", - "requirements": [ - "lyft_rides==0.2" - ], + "requirements": ["lyft_rides==0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/magicseaweed/manifest.json b/homeassistant/components/magicseaweed/manifest.json index 41795c117a9..ccd684e1f35 100644 --- a/homeassistant/components/magicseaweed/manifest.json +++ b/homeassistant/components/magicseaweed/manifest.json @@ -2,9 +2,7 @@ "domain": "magicseaweed", "name": "Magicseaweed", "documentation": "https://www.home-assistant.io/integrations/magicseaweed", - "requirements": [ - "magicseaweed==1.0.3" - ], + "requirements": ["magicseaweed==1.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 1252036e1b2..0381d932328 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -16,7 +16,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_prepare_setup_platform - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mailbox/manifest.json b/homeassistant/components/mailbox/manifest.json index 2883c9caf34..5202569d198 100644 --- a/homeassistant/components/mailbox/manifest.json +++ b/homeassistant/components/mailbox/manifest.json @@ -3,8 +3,6 @@ "name": "Mailbox", "documentation": "https://www.home-assistant.io/integrations/mailbox", "requirements": [], - "dependencies": [ - "http" - ], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/mailgun/.translations/da.json b/homeassistant/components/mailgun/.translations/da.json index 0e25974031d..f9152633706 100644 --- a/homeassistant/components/mailgun/.translations/da.json +++ b/homeassistant/components/mailgun/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Mailgun meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Mailgun-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 [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\n Se [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [Webhooks med Mailgun]({mailgun_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/json\n\nSe [dokumentationen] ({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/.translations/ko.json b/homeassistant/components/mailgun/.translations/ko.json index 4ca5b155e73..8f1f021caf6 100644 --- a/homeassistant/components/mailgun/.translations/ko.json +++ b/homeassistant/components/mailgun/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Mailgun \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Mailgun \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Mailgun Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/mailgun/manifest.json b/homeassistant/components/mailgun/manifest.json index c0bd0823b8f..45e809bac1a 100644 --- a/homeassistant/components/mailgun/manifest.json +++ b/homeassistant/components/mailgun/manifest.json @@ -3,11 +3,7 @@ "name": "Mailgun", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mailgun", - "requirements": [ - "pymailgunner==1.4" - ], - "dependencies": [ - "webhook" - ], + "requirements": ["pymailgunner==1.4"], + "dependencies": ["webhook"], "codeowners": [] } diff --git a/homeassistant/components/manual/manifest.json b/homeassistant/components/manual/manifest.json index 12d5297c010..29d6cbdf871 100644 --- a/homeassistant/components/manual/manifest.json +++ b/homeassistant/components/manual/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/manual", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/manual_mqtt/manifest.json b/homeassistant/components/manual_mqtt/manifest.json index d9ec004e6a8..1ae597fecd9 100644 --- a/homeassistant/components/manual_mqtt/manifest.json +++ b/homeassistant/components/manual_mqtt/manifest.json @@ -1,10 +1,8 @@ { "domain": "manual_mqtt", - "name": "Manual mqtt", + "name": "Manual MQTT", "documentation": "https://www.home-assistant.io/integrations/manual_mqtt", "requirements": [], - "dependencies": [ - "mqtt" - ], + "dependencies": ["mqtt"], "codeowners": [] } diff --git a/homeassistant/components/map/manifest.json b/homeassistant/components/map/manifest.json index e64d18c92e1..108ca8f1772 100644 --- a/homeassistant/components/map/manifest.json +++ b/homeassistant/components/map/manifest.json @@ -3,8 +3,7 @@ "name": "Map", "documentation": "https://www.home-assistant.io/integrations/map", "requirements": [], - "dependencies": [ - "frontend" - ], - "codeowners": [] + "dependencies": ["frontend"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/marytts/manifest.json b/homeassistant/components/marytts/manifest.json index 0188f405e14..59517e4f1bb 100644 --- a/homeassistant/components/marytts/manifest.json +++ b/homeassistant/components/marytts/manifest.json @@ -1,6 +1,6 @@ { "domain": "marytts", - "name": "Marytts", + "name": "MaryTTS", "documentation": "https://www.home-assistant.io/integrations/marytts", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index cacdf9dd502..c6430546bf2 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -2,11 +2,7 @@ "domain": "mastodon", "name": "Mastodon", "documentation": "https://www.home-assistant.io/integrations/mastodon", - "requirements": [ - "Mastodon.py==1.5.0" - ], + "requirements": ["Mastodon.py==1.5.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index 71735bd7e51..f8a57572d04 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -1,4 +1,4 @@ -"""The matrix bot component.""" +"""The Matrix bot component.""" from functools import partial import logging import os @@ -19,6 +19,8 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json +from .const import DOMAIN, SERVICE_SEND_MESSAGE + _LOGGER = logging.getLogger(__name__) SESSION_FILE = ".matrix.conf" @@ -31,10 +33,7 @@ CONF_EXPRESSION = "expression" EVENT_MATRIX_COMMAND = "matrix_command" -DOMAIN = "matrix" - COMMAND_SCHEMA = vol.All( - # Basic Schema vol.Schema( { vol.Exclusive(CONF_WORD, "trigger"): cv.string, @@ -43,7 +42,6 @@ COMMAND_SCHEMA = vol.All( vol.Optional(CONF_ROOMS, default=[]): vol.All(cv.ensure_list, [cv.string]), } ), - # Make sure it's either a word or an expression command cv.has_at_least_one_key(CONF_WORD, CONF_EXPRESSION), ) @@ -65,7 +63,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_SEND_MESSAGE = "send_message" SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( { @@ -77,7 +74,6 @@ SERVICE_SCHEMA_SEND_MESSAGE = vol.Schema( def setup(hass, config): """Set up the Matrix bot component.""" - config = config[DOMAIN] try: @@ -138,11 +134,11 @@ class MatrixBot: # so we only do it once per room. self._aliases_fetched_for = set() - # word commands are stored dict-of-dict: First dict indexes by room ID + # Word commands are stored dict-of-dict: First dict indexes by room ID # / alias, second dict indexes by the word self._word_commands = {} - # regular expression commands are stored as a list of commands per + # Regular expression commands are stored as a list of commands per # room, i.e., a dict-of-list self._expression_commands = {} @@ -184,7 +180,7 @@ class MatrixBot: self.hass.bus.listen_once(EVENT_HOMEASSISTANT_START, handle_startup) def _handle_room_message(self, room_id, room, event): - """Handle a message sent to a room.""" + """Handle a message sent to a Matrix room.""" if event["content"]["msgtype"] != "m.text": return @@ -194,7 +190,7 @@ class MatrixBot: _LOGGER.debug("Handling message: %s", event["content"]["body"]) if event["content"]["body"][0] == "!": - # Could trigger a single-word command. + # Could trigger a single-word command pieces = event["content"]["body"].split(" ") cmd = pieces[0][1:] @@ -248,8 +244,7 @@ class MatrixBot: return room def _join_rooms(self): - """Join the rooms that we listen for commands in.""" - + """Join the Matrix rooms that we listen for commands in.""" for room_id in self._listening_rooms: try: room = self._join_or_get_room(room_id) @@ -285,8 +280,7 @@ class MatrixBot: save_json(self._session_filepath, self._auth_tokens) def _login(self): - """Login to the matrix homeserver and return the client instance.""" - + """Login to the Matrix homeserver and return the client instance.""" # Attempt to generate a valid client using either of the two possible # login methods: client = None @@ -299,13 +293,12 @@ class MatrixBot: except MatrixRequestError as ex: _LOGGER.warning( - "Login by token failed, falling back to password. " - "login_by_token raised: (%d) %s", + "Login by token failed, falling back to password: %d, %s", ex.code, ex.content, ) - # If we still don't have a client try password. + # If we still don't have a client try password if not client: try: client = self._login_by_password() @@ -313,20 +306,17 @@ class MatrixBot: except MatrixRequestError as ex: _LOGGER.error( - "Login failed, both token and username/password invalid " - "login_by_password raised: (%d) %s", + "Login failed, both token and username/password invalid: %d, %s", ex.code, ex.content, ) - - # re-raise the error so _setup can catch it. + # Re-raise the error so _setup can catch it raise return client def _login_by_token(self): """Login using authentication token and return the client.""" - return MatrixClient( base_url=self._homeserver, token=self._auth_tokens[self._mx_id], @@ -336,7 +326,6 @@ class MatrixBot: def _login_by_password(self): """Login using password authentication and return the client.""" - _client = MatrixClient( base_url=self._homeserver, valid_cert_check=self._verify_tls ) @@ -348,7 +337,7 @@ class MatrixBot: return _client def _send_message(self, message, target_rooms): - """Send the message to the matrix server.""" + """Send the message to the Matrix server.""" for target_room in target_rooms: try: @@ -356,7 +345,7 @@ class MatrixBot: _LOGGER.debug(room.send_text(message)) except MatrixRequestError as ex: _LOGGER.error( - "Unable to deliver message to room '%s': (%d): %s", + "Unable to deliver message to room '%s': %d, %s", target_room, ex.code, ex.content, diff --git a/homeassistant/components/matrix/const.py b/homeassistant/components/matrix/const.py new file mode 100644 index 00000000000..6b082bde121 --- /dev/null +++ b/homeassistant/components/matrix/const.py @@ -0,0 +1,4 @@ +"""Constants for the Matrix integration.""" +DOMAIN = "matrix" + +SERVICE_SEND_MESSAGE = "send_message" diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index a467518c04e..f4a92d7e104 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -2,11 +2,7 @@ "domain": "matrix", "name": "Matrix", "documentation": "https://www.home-assistant.io/integrations/matrix", - "requirements": [ - "matrix-client==0.2.0" - ], + "requirements": ["matrix-client==0.3.2"], "dependencies": [], - "codeowners": [ - "@tinloaf" - ] + "codeowners": ["@tinloaf"] } diff --git a/homeassistant/components/matrix/notify.py b/homeassistant/components/matrix/notify.py index 06e9712becc..9f1f0eb992a 100644 --- a/homeassistant/components/matrix/notify.py +++ b/homeassistant/components/matrix/notify.py @@ -11,32 +11,33 @@ from homeassistant.components.notify import ( ) import homeassistant.helpers.config_validation as cv +from .const import DOMAIN, SERVICE_SEND_MESSAGE + _LOGGER = logging.getLogger(__name__) CONF_DEFAULT_ROOM = "default_room" -DOMAIN = "matrix" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_DEFAULT_ROOM): cv.string}) def get_service(hass, config, discovery_info=None): """Get the Matrix notification service.""" - return MatrixNotificationService(config.get(CONF_DEFAULT_ROOM)) + return MatrixNotificationService(config[CONF_DEFAULT_ROOM]) class MatrixNotificationService(BaseNotificationService): - """Send Notifications to a Matrix Room.""" + """Send notifications to a Matrix room.""" def __init__(self, default_room): - """Set up the notification service.""" + """Set up the Matrix notification service.""" self._default_room = default_room def send_message(self, message="", **kwargs): - """Send the message to the matrix server.""" + """Send the message to the Matrix server.""" target_rooms = kwargs.get(ATTR_TARGET) or [self._default_room] service_data = {ATTR_TARGET: target_rooms, ATTR_MESSAGE: message} return self.hass.services.call( - DOMAIN, "send_message", service_data=service_data + DOMAIN, SERVICE_SEND_MESSAGE, service_data=service_data ) diff --git a/homeassistant/components/matrix/services.yaml b/homeassistant/components/matrix/services.yaml index 1cf83de2c33..03c441a39ec 100644 --- a/homeassistant/components/matrix/services.yaml +++ b/homeassistant/components/matrix/services.yaml @@ -2,8 +2,8 @@ send_message: description: Send message to target room(s) fields: message: - description: The message to be sent + description: The message to be sent. example: 'This is a message I am sending to matrix' target: - description: A list of room(s) to send the message to + description: A list of room(s) to send the message to. example: '#hasstest:matrix.org' \ No newline at end of file diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index ff4b219ec21..a6a63ddc0fd 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -155,7 +155,7 @@ class MaxCubeClimate(ClimateDevice): @staticmethod def map_temperature_max_hass(temperature): - """Map Temperature from MAX! to HASS.""" + """Map Temperature from MAX! to Home Assistant.""" if temperature is None: return 0.0 diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index 1f5b1eef935..b3ac6591f76 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -1,10 +1,8 @@ { "domain": "maxcube", - "name": "Maxcube", + "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", - "requirements": [ - "maxcube-api==0.1.0" - ], + "requirements": ["maxcube-api==0.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index 088052c469e..e95b91389cd 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -1,13 +1,13 @@ """Support for binary sensor using I2C MCP23017 chip.""" import logging -import voluptuous as vol +import adafruit_mcp230xx # pylint: disable=import-error import board # pylint: disable=import-error import busio # pylint: disable=import-error -import adafruit_mcp230xx # pylint: disable=import-error import digitalio # pylint: disable=import-error +import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 13c36424dd6..ebf796fe3db 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -6,7 +6,7 @@ "RPi.GPIO==0.7.0", "adafruit-blinka==1.2.1", "adafruit-circuitpython-mcp230xx==1.1.2" - ], + ], "dependencies": [], "codeowners": ["@jardiamj"] } diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index 399ed17c44b..8506106b705 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -1,16 +1,16 @@ """Support for switch sensor using I2C MCP23017 chip.""" import logging -import voluptuous as vol +import adafruit_mcp230xx # pylint: disable=import-error import board # pylint: disable=import-error import busio # pylint: disable=import-error -import adafruit_mcp230xx # pylint: disable=import-error import digitalio # pylint: disable=import-error +import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 16f491f0cae..4781b2c7693 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -1,12 +1,9 @@ { "domain": "media_extractor", - "name": "Media extractor", + "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": [ - "youtube_dl==2019.11.28" - ], - "dependencies": [ - "media_player" - ], - "codeowners": [] -} \ No newline at end of file + "requirements": ["youtube_dl==2020.01.01"], + "dependencies": ["media_player"], + "codeowners": [], + "quality_scale": "internal" +} diff --git a/homeassistant/components/media_player/.translations/da.json b/homeassistant/components/media_player/.translations/da.json new file mode 100644 index 00000000000..a53bbed07d0 --- /dev/null +++ b/homeassistant/components/media_player/.translations/da.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} er inaktiv", + "is_off": "{entity_name} er slukket", + "is_on": "{entity_name} er t\u00e6ndt", + "is_paused": "{entity_name} er sat p\u00e5 pause", + "is_playing": "{entity_name} afspiller" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/de.json b/homeassistant/components/media_player/.translations/de.json index 42fdc22f377..7efad821a9e 100644 --- a/homeassistant/components/media_player/.translations/de.json +++ b/homeassistant/components/media_player/.translations/de.json @@ -1,9 +1,11 @@ { "device_automation": { "condition_type": { + "is_idle": "{entity_name} ist unt\u00e4tig", "is_off": "{entity_name} ist ausgeschaltet", "is_on": "{entity_name} ist eingeschaltet", - "is_paused": "{entity_name} ist pausiert" + "is_paused": "{entity_name} ist pausiert", + "is_playing": "{entity_name} spielt" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/ko.json b/homeassistant/components/media_player/.translations/ko.json index 7542154448f..49367eaf617 100644 --- a/homeassistant/components/media_player/.translations/ko.json +++ b/homeassistant/components/media_player/.translations/ko.json @@ -1,11 +1,11 @@ { "device_automation": { "condition_type": { - "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734\uc0c1\ud0dc\uc785\ub2c8\ub2e4", - "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", - "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd\uc911\uc785\ub2c8\ub2e4" + "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734\uc0c1\ud0dc\uc774\uba74", + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5b4 \uc788\uc73c\uba74", + "is_playing": "{entity_name} \uc774(\uac00) \uc7ac\uc0dd \uc911\uc774\uba74" } } } \ No newline at end of file diff --git a/homeassistant/components/media_player/.translations/sv.json b/homeassistant/components/media_player/.translations/sv.json new file mode 100644 index 00000000000..b7f62a138df --- /dev/null +++ b/homeassistant/components/media_player/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u00e4r inaktiv", + "is_off": "{entity_name} \u00e4r avst\u00e4ngd", + "is_on": "{entity_name} \u00e4r p\u00e5", + "is_paused": "{entity_name} \u00e4r pausad", + "is_playing": "{entity_name} spelar" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index a6cd4dda4ea..83c117d6c05 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -97,7 +97,6 @@ from .const import ( SUPPORT_VOLUME_STEP, ) - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -150,9 +149,7 @@ ATTR_TO_PROPERTY = [ ATTR_APP_ID, ATTR_APP_NAME, ATTR_INPUT_SOURCE, - ATTR_INPUT_SOURCE_LIST, ATTR_SOUND_MODE, - ATTR_SOUND_MODE_LIST, ATTR_MEDIA_SHUFFLE, ] @@ -785,6 +782,24 @@ class MediaPlayerDevice(Entity): return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token, image_hash) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + supported_features = self.supported_features + data = {} + + if supported_features & SUPPORT_SELECT_SOURCE: + source_list = self.source_list + if source_list: + data[ATTR_INPUT_SOURCE_LIST] = source_list + + if supported_features & SUPPORT_SELECT_SOUND_MODE: + sound_mode_list = self.sound_mode_list + if sound_mode_list: + data[ATTR_SOUND_MODE_LIST] = sound_mode_list + + return data + @property def state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/media_player/device_condition.py b/homeassistant/components/media_player/device_condition.py index a67a084a94f..a8091a6aed8 100644 --- a/homeassistant/components/media_player/device_condition.py +++ b/homeassistant/components/media_player/device_condition.py @@ -1,24 +1,26 @@ """Provides device automations for Media player.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, + STATE_IDLE, STATE_OFF, STATE_ON, - STATE_IDLE, STATE_PAUSED, STATE_PLAYING, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + from . import DOMAIN CONDITION_TYPES = {"is_on", "is_off", "is_idle", "is_paused", "is_playing"} diff --git a/homeassistant/components/media_player/manifest.json b/homeassistant/components/media_player/manifest.json index 4df8ad8442a..5c9a5cde0e4 100644 --- a/homeassistant/components/media_player/manifest.json +++ b/homeassistant/components/media_player/manifest.json @@ -1,10 +1,9 @@ { "domain": "media_player", - "name": "Media player", + "name": "Media Player", "documentation": "https://www.home-assistant.io/integrations/media_player", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index dac08afe471..dc9078d3ffd 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -21,21 +21,20 @@ from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType from .const import ( + ATTR_INPUT_SOURCE, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + ATTR_MEDIA_ENQUEUE, + ATTR_MEDIA_SEEK_POSITION, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, - ATTR_MEDIA_SEEK_POSITION, - ATTR_INPUT_SOURCE, ATTR_SOUND_MODE, - ATTR_MEDIA_CONTENT_TYPE, - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_ENQUEUE, - SERVICE_PLAY_MEDIA, - SERVICE_SELECT_SOURCE, - SERVICE_SELECT_SOUND_MODE, DOMAIN, + SERVICE_PLAY_MEDIA, + SERVICE_SELECT_SOUND_MODE, + SERVICE_SELECT_SOURCE, ) - # mypy: allow-untyped-defs diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index ad6b8a78957..19ef1cb14c0 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -125,7 +125,7 @@ select_source: fields: entity_id: description: Name(s) of entities to change source on. - example: 'media_player.media_player.txnr535_0009b0d81f82' + example: 'media_player.txnr535_0009b0d81f82' source: description: Name of the source to switch to. Platform dependent. example: 'video1' diff --git a/homeassistant/components/mediaroom/manifest.json b/homeassistant/components/mediaroom/manifest.json index 0e42cd7e535..31a028db61c 100644 --- a/homeassistant/components/mediaroom/manifest.json +++ b/homeassistant/components/mediaroom/manifest.json @@ -2,11 +2,7 @@ "domain": "mediaroom", "name": "Mediaroom", "documentation": "https://www.home-assistant.io/integrations/mediaroom", - "requirements": [ - "pymediaroom==0.6.4" - ], + "requirements": ["pymediaroom==0.6.4"], "dependencies": [], - "codeowners": [ - "@dgomes" - ] + "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/mediaroom/media_player.py b/homeassistant/components/mediaroom/media_player.py index 8e02ee56a75..539138783ee 100644 --- a/homeassistant/components/mediaroom/media_player.py +++ b/homeassistant/components/mediaroom/media_player.py @@ -1,9 +1,10 @@ """Support for the Mediaroom Set-up-box.""" import logging +from pymediaroom import PyMediaroomError, Remote, State, install_mediaroom_protocol import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -99,7 +100,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([new_stb]) if not config[CONF_OPTIMISTIC]: - from pymediaroom import install_mediaroom_protocol already_installed = hass.data.get(DISCOVERY_MEDIAROOM, None) if not already_installed: @@ -123,7 +123,6 @@ class MediaroomDevice(MediaPlayerDevice): def set_state(self, mediaroom_state): """Map pymediaroom state to HA state.""" - from pymediaroom import State state_map = { State.OFF: STATE_OFF, @@ -139,7 +138,6 @@ class MediaroomDevice(MediaPlayerDevice): def __init__(self, host, device_id, optimistic=False, timeout=DEFAULT_TIMEOUT): """Initialize the device.""" - from pymediaroom import Remote self.host = host self.stb = Remote(host) @@ -184,7 +182,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_play_media(self, media_type, media_id, **kwargs): """Play media.""" - from pymediaroom import PyMediaroomError _LOGGER.debug( "STB(%s) Play media: %s (%s)", self.stb.stb_ip, media_id, media_type @@ -237,7 +234,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_turn_on(self): """Turn on the receiver.""" - from pymediaroom import PyMediaroomError try: self.set_state(await self.stb.turn_on()) @@ -250,7 +246,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_turn_off(self): """Turn off the receiver.""" - from pymediaroom import PyMediaroomError try: self.set_state(await self.stb.turn_off()) @@ -263,7 +258,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_play(self): """Send play command.""" - from pymediaroom import PyMediaroomError try: _LOGGER.debug("media_play()") @@ -277,7 +271,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_pause(self): """Send pause command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("PlayPause") @@ -290,7 +283,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_stop(self): """Send stop command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("Stop") @@ -303,7 +295,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_previous_track(self): """Send Program Down command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("ProgDown") @@ -316,7 +307,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_media_next_track(self): """Send Program Up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("ProgUp") @@ -329,7 +319,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_volume_up(self): """Send volume up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("VolUp") @@ -340,7 +329,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_volume_down(self): """Send volume up command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("VolDown") @@ -350,7 +338,6 @@ class MediaroomDevice(MediaPlayerDevice): async def async_mute_volume(self, mute): """Send mute command.""" - from pymediaroom import PyMediaroomError try: await self.stb.send_cmd("Mute") diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 38f4977c96a..4b033811f43 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -3,6 +3,10 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -12,7 +16,6 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS from . import DATA_MELISSA @@ -29,7 +32,7 @@ OP_MODES = [ HVAC_MODE_OFF, ] -FAN_MODES = [HVAC_MODE_AUTO, SPEED_HIGH, SPEED_MEDIUM, SPEED_LOW] +FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -200,11 +203,11 @@ class MelissaClimate(ClimateDevice): if fan == self._api.FAN_AUTO: return HVAC_MODE_AUTO if fan == self._api.FAN_LOW: - return SPEED_LOW + return FAN_LOW if fan == self._api.FAN_MEDIUM: - return SPEED_MEDIUM + return FAN_MEDIUM if fan == self._api.FAN_HIGH: - return SPEED_HIGH + return FAN_HIGH _LOGGER.warning("Fan mode %s could not be mapped to hass", fan) return None @@ -224,10 +227,10 @@ class MelissaClimate(ClimateDevice): """Translate hass fan modes to melissa modes.""" if fan == HVAC_MODE_AUTO: return self._api.FAN_AUTO - if fan == SPEED_LOW: + if fan == FAN_LOW: return self._api.FAN_LOW - if fan == SPEED_MEDIUM: + if fan == FAN_MEDIUM: return self._api.FAN_MEDIUM - if fan == SPEED_HIGH: + if fan == FAN_HIGH: return self._api.FAN_HIGH _LOGGER.warning("Melissa have no setting for %s fan mode", fan) diff --git a/homeassistant/components/melissa/manifest.json b/homeassistant/components/melissa/manifest.json index 64760338f35..bd69a1cc0b2 100644 --- a/homeassistant/components/melissa/manifest.json +++ b/homeassistant/components/melissa/manifest.json @@ -2,11 +2,7 @@ "domain": "melissa", "name": "Melissa", "documentation": "https://www.home-assistant.io/integrations/melissa", - "requirements": [ - "py-melissa-climate==2.0.0" - ], + "requirements": ["py-melissa-climate==2.0.0"], "dependencies": [], - "codeowners": [ - "@kennedyshead" - ] + "codeowners": ["@kennedyshead"] } diff --git a/homeassistant/components/meraki/device_tracker.py b/homeassistant/components/meraki/device_tracker.py index 3f50caa2c8b..1aa1485922e 100644 --- a/homeassistant/components/meraki/device_tracker.py +++ b/homeassistant/components/meraki/device_tracker.py @@ -5,15 +5,16 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.meraki/ """ -import logging import json +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv + +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER +from homeassistant.components.http import HomeAssistantView from homeassistant.const import HTTP_BAD_REQUEST, HTTP_UNPROCESSABLE_ENTITY from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_ROUTER +import homeassistant.helpers.config_validation as cv CONF_VALIDATOR = "validator" CONF_SECRET = "secret" diff --git a/homeassistant/components/message_bird/manifest.json b/homeassistant/components/message_bird/manifest.json index 79428f951f1..fb87d06edfb 100644 --- a/homeassistant/components/message_bird/manifest.json +++ b/homeassistant/components/message_bird/manifest.json @@ -1,10 +1,8 @@ { "domain": "message_bird", - "name": "Message bird", + "name": "MessageBird", "documentation": "https://www.home-assistant.io/integrations/message_bird", - "requirements": [ - "messagebird==1.2.0" - ], + "requirements": ["messagebird==1.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/met/.translations/da.json b/homeassistant/components/met/.translations/da.json index 9d44f1b2b6c..e36a6511aa3 100644 --- a/homeassistant/components/met/.translations/da.json +++ b/homeassistant/components/met/.translations/da.json @@ -12,7 +12,7 @@ "name": "Navn" }, "description": "Meteorologisk institutt", - "title": "Placering" + "title": "Lokalitet" } }, "title": "Met.no" diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 305038c3e6a..7ef3c5f8796 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,5 +1,6 @@ """The met component.""" from homeassistant.core import Config, HomeAssistant + from .config_flow import MetFlowHandler # noqa: F401 from .const import DOMAIN # noqa: F401 diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index c7ff4973c7d..759f7f6fc89 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -6,7 +6,7 @@ from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, C from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, HOME_LOCATION_NAME, CONF_TRACK_HOME +from .const import CONF_TRACK_HOME, DOMAIN, HOME_LOCATION_NAME @callback diff --git a/homeassistant/components/met/manifest.json b/homeassistant/components/met/manifest.json index 2652e33b76c..62aeaf21de7 100644 --- a/homeassistant/components/met/manifest.json +++ b/homeassistant/components/met/manifest.json @@ -1,13 +1,9 @@ { "domain": "met", - "name": "Met", + "name": "Meteorologisk institutt (Met.no)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/met", - "requirements": [ - "pyMetno==0.4.6" - ], + "requirements": ["pyMetno==0.4.6"], "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 2f9ddc5a67c..d99573a985e 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -5,16 +5,16 @@ from random import randrange import metno import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity from homeassistant.const import ( CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, - TEMP_CELSIUS, EVENT_CORE_CONFIG_UPDATE, + TEMP_CELSIUS, ) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_call_later diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json index ba043bf2a71..41a003ea4f7 100644 --- a/homeassistant/components/meteo_france/manifest.json +++ b/homeassistant/components/meteo_france/manifest.json @@ -1,14 +1,8 @@ { "domain": "meteo_france", - "name": "Meteo france", + "name": "Météo-France", "documentation": "https://www.home-assistant.io/integrations/meteo_france", - "requirements": [ - "meteofrance==0.3.7", - "vigilancemeteo==3.0.0" - ], + "requirements": ["meteofrance==0.3.7", "vigilancemeteo==3.0.0"], "dependencies": [], - "codeowners": [ - "@victorcerutti", - "@oncleben31" - ] -} \ No newline at end of file + "codeowners": ["@victorcerutti", "@oncleben31"] +} diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json index ee14ce7d26d..5fc7e7c137c 100644 --- a/homeassistant/components/meteoalarm/manifest.json +++ b/homeassistant/components/meteoalarm/manifest.json @@ -1,10 +1,8 @@ { "domain": "meteoalarm", - "name": "meteoalarm", + "name": "MeteoAlarm", "documentation": "https://www.home-assistant.io/integrations/meteoalarm", - "requirements": [ - "meteoalertapi==0.1.6" - ], + "requirements": ["meteoalertapi==0.1.6"], "dependencies": [], "codeowners": ["@rolfberkenbosch"] } diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index f5624e33edb..9c16b7687a2 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -1,10 +1,8 @@ { "domain": "metoffice", - "name": "Metoffice", + "name": "Met Office", "documentation": "https://www.home-assistant.io/integrations/metoffice", - "requirements": [ - "datapoint==0.4.3" - ], + "requirements": ["datapoint==0.9.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mfi/manifest.json b/homeassistant/components/mfi/manifest.json index c08b4e4c88a..4e5ab31b1d5 100644 --- a/homeassistant/components/mfi/manifest.json +++ b/homeassistant/components/mfi/manifest.json @@ -1,10 +1,8 @@ { "domain": "mfi", - "name": "Mfi", + "name": "Ubiquiti mFi mPort", "documentation": "https://www.home-assistant.io/integrations/mfi", - "requirements": [ - "mficlient==0.3.0" - ], + "requirements": ["mficlient==0.3.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mfi/switch.py b/homeassistant/components/mfi/switch.py index 00cb23a102e..18809f08d4f 100644 --- a/homeassistant/components/mfi/switch.py +++ b/homeassistant/components/mfi/switch.py @@ -21,7 +21,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = True DEFAULT_VERIFY_SSL = True -SWITCH_MODELS = ["Outlet", "Output 5v", "Output 12v", "Output 24v"] +SWITCH_MODELS = ["Outlet", "Output 5v", "Output 12v", "Output 24v", "Dimmer Switch"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/mhz19/manifest.json b/homeassistant/components/mhz19/manifest.json index 5bffcf8e92c..7f160a2e9a4 100644 --- a/homeassistant/components/mhz19/manifest.json +++ b/homeassistant/components/mhz19/manifest.json @@ -1,10 +1,8 @@ { "domain": "mhz19", - "name": "Mhz19", + "name": "MH-Z19 CO2 Sensor", "documentation": "https://www.home-assistant.io/integrations/mhz19", - "requirements": [ - "pmsensor==0.4" - ], + "requirements": ["pmsensor==0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 5834897ee90..d0947357a51 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -1,10 +1,8 @@ { "domain": "microsoft", - "name": "Microsoft", + "name": "Microsoft Text-to-Speech (TTS)", "documentation": "https://www.home-assistant.io/integrations/microsoft", - "requirements": [ - "pycsspeechtts==1.0.3" - ], + "requirements": ["pycsspeechtts==1.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 5d0c50e536a..780f4d6dd48 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -8,7 +8,7 @@ from aiohttp.hdrs import CONTENT_TYPE import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_TIMEOUT, ATTR_NAME +from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -239,7 +239,7 @@ class MicrosoftFaceGroupEntity(Entity): class MicrosoftFace: - """Microsoft Face api for HomeAssistant.""" + """Microsoft Face api for Home Assistant.""" def __init__(self, hass, server_loc, api_key, timeout, entities): """Initialize Microsoft Face api.""" diff --git a/homeassistant/components/microsoft_face/manifest.json b/homeassistant/components/microsoft_face/manifest.json index 1d51dca42cd..d4ff56d325b 100644 --- a/homeassistant/components/microsoft_face/manifest.json +++ b/homeassistant/components/microsoft_face/manifest.json @@ -1,10 +1,8 @@ { "domain": "microsoft_face", - "name": "Microsoft face", + "name": "Microsoft Face", "documentation": "https://www.home-assistant.io/integrations/microsoft_face", "requirements": [], - "dependencies": [ - "camera" - ], + "dependencies": ["camera"], "codeowners": [] } diff --git a/homeassistant/components/microsoft_face_detect/manifest.json b/homeassistant/components/microsoft_face_detect/manifest.json index 12d73623e75..7e784fe988b 100644 --- a/homeassistant/components/microsoft_face_detect/manifest.json +++ b/homeassistant/components/microsoft_face_detect/manifest.json @@ -1,6 +1,6 @@ { "domain": "microsoft_face_detect", - "name": "Microsoft face detect", + "name": "Microsoft Face Detect", "documentation": "https://www.home-assistant.io/integrations/microsoft_face_detect", "requirements": [], "dependencies": ["microsoft_face"], diff --git a/homeassistant/components/microsoft_face_identify/manifest.json b/homeassistant/components/microsoft_face_identify/manifest.json index a52aca1ac0a..dea16ed5afc 100644 --- a/homeassistant/components/microsoft_face_identify/manifest.json +++ b/homeassistant/components/microsoft_face_identify/manifest.json @@ -1,6 +1,6 @@ { "domain": "microsoft_face_identify", - "name": "Microsoft face identify", + "name": "Microsoft Face Identify", "documentation": "https://www.home-assistant.io/integrations/microsoft_face_identify", "requirements": [], "dependencies": ["microsoft_face"], diff --git a/homeassistant/components/miflora/manifest.json b/homeassistant/components/miflora/manifest.json index 54fa59135b3..22633979128 100644 --- a/homeassistant/components/miflora/manifest.json +++ b/homeassistant/components/miflora/manifest.json @@ -1,14 +1,8 @@ { "domain": "miflora", - "name": "Miflora", + "name": "Mi Flora", "documentation": "https://www.home-assistant.io/integrations/miflora", - "requirements": [ - "bluepy==1.1.4", - "miflora==0.4.0" - ], + "requirements": ["bluepy==1.3.0", "miflora==0.6.0"], "dependencies": [], - "codeowners": [ - "@danielhiversen", - "@ChristianKuehnel" - ] + "codeowners": ["@danielhiversen", "@ChristianKuehnel"] } diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index aacd3c65b3e..9b533288d86 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -2,34 +2,35 @@ import logging import ssl -import voluptuous as vol import librouteros from librouteros.login import login_plain, login_token +import voluptuous as vol +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.const import ( CONF_HOST, + CONF_METHOD, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, CONF_SSL, - CONF_METHOD, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform -from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER + from .const import ( - NAME, + CONF_ARP_PING, + CONF_ENCODING, + CONF_LOGIN_METHOD, + CONF_TRACK_DEVICES, + DEFAULT_ENCODING, DOMAIN, HOSTS, + IDENTITY, + MIKROTIK_SERVICES, MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN, - DEFAULT_ENCODING, - IDENTITY, - CONF_TRACK_DEVICES, - CONF_ENCODING, - CONF_ARP_PING, - CONF_LOGIN_METHOD, - MIKROTIK_SERVICES, + NAME, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 6c3fb559cba..92fcfac4ae4 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -5,18 +5,19 @@ from homeassistant.components.device_tracker import ( DOMAIN as DEVICE_TRACKER, DeviceScanner, ) -from homeassistant.util import slugify from homeassistant.const import CONF_METHOD +from homeassistant.util import slugify + from .const import ( - HOSTS, - MIKROTIK, - CONF_ARP_PING, - MIKROTIK_SERVICES, - CAPSMAN, - WIRELESS, - DHCP, ARP, ATTR_DEVICE_TRACKER, + CAPSMAN, + CONF_ARP_PING, + DHCP, + HOSTS, + MIKROTIK, + MIKROTIK_SERVICES, + WIRELESS, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index 9a05f5a9f87..1f5bcf8163f 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -1,10 +1,8 @@ { "domain": "mikrotik", - "name": "Mikrotik", + "name": "MikroTik", "documentation": "https://www.home-assistant.io/integrations/mikrotik", - "requirements": [ - "librouteros==2.3.0" - ], + "requirements": ["librouteros==2.3.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mill/climate.py b/homeassistant/components/mill/climate.py index b08015fe548..875d217247c 100644 --- a/homeassistant/components/mill/climate.py +++ b/homeassistant/components/mill/climate.py @@ -6,11 +6,11 @@ import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + FAN_ON, HVAC_MODE_HEAT, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, - FAN_ON, ) from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/mill/manifest.json b/homeassistant/components/mill/manifest.json index 85e70c78ede..f5ca1835a67 100644 --- a/homeassistant/components/mill/manifest.json +++ b/homeassistant/components/mill/manifest.json @@ -2,11 +2,7 @@ "domain": "mill", "name": "Mill", "documentation": "https://www.home-assistant.io/integrations/mill", - "requirements": [ - "millheater==0.3.4" - ], + "requirements": ["millheater==0.3.4"], "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/min_max/manifest.json b/homeassistant/components/min_max/manifest.json index ed899a0438f..db3a896a1c3 100644 --- a/homeassistant/components/min_max/manifest.json +++ b/homeassistant/components/min_max/manifest.json @@ -1,10 +1,9 @@ { "domain": "min_max", - "name": "Min max", + "name": "Min/Max", "documentation": "https://www.home-assistant.io/integrations/min_max", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index 519bba464de..80beaf1f798 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -3,16 +3,16 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - STATE_UNKNOWN, - STATE_UNAVAILABLE, - CONF_TYPE, ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, + CONF_TYPE, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change @@ -153,7 +153,7 @@ class MinMaxSensor(Entity): self.last = float(new_state.state) except ValueError: _LOGGER.warning( - "Unable to store state. " "Only numerical states are supported" + "Unable to store state. Only numerical states are supported" ) hass.async_add_job(self.async_update_ha_state, True) diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py index d411d913082..24e7049dd18 100644 --- a/homeassistant/components/minio/__init__.py +++ b/homeassistant/components/minio/__init__.py @@ -1,8 +1,8 @@ """Minio component.""" import logging import os -import threading from queue import Queue +import threading from typing import List import voluptuous as vol @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv -from .minio_helper import create_minio_client, MinioEventThread +from .minio_helper import MinioEventThread, create_minio_client _LOGGER = logging.getLogger(__name__) @@ -170,7 +170,7 @@ def get_minio_endpoint(host: str, port: int) -> str: class QueueListener(threading.Thread): - """Forward events from queue into HASS event bus.""" + """Forward events from queue into Home Assistant event bus.""" def __init__(self, hass): """Create queue.""" @@ -179,7 +179,7 @@ class QueueListener(threading.Thread): self._queue = Queue() def run(self): - """Listen to queue events, and forward them to HASS event bus.""" + """Listen to queue events, and forward them to Home Assistant event bus.""" _LOGGER.info("Running QueueListener") while True: event = self._queue.get() diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json index bc373633503..35f2c56d3da 100644 --- a/homeassistant/components/minio/manifest.json +++ b/homeassistant/components/minio/manifest.json @@ -2,11 +2,7 @@ "domain": "minio", "name": "Minio", "documentation": "https://www.home-assistant.io/integrations/minio", - "requirements": [ - "minio==4.0.9" - ], + "requirements": ["minio==4.0.9"], "dependencies": [], - "codeowners": [ - "@tkislan" - ] + "codeowners": ["@tkislan"] } diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py index bd7b15d27d4..2aaba9d4085 100644 --- a/homeassistant/components/minio/minio_helper.py +++ b/homeassistant/components/minio/minio_helper.py @@ -1,11 +1,11 @@ """Minio helper methods.""" -import time from collections.abc import Iterable import json import logging +from queue import Queue import re import threading -from queue import Queue +import time from typing import Iterator, List from urllib.parse import unquote diff --git a/homeassistant/components/mitemp_bt/manifest.json b/homeassistant/components/mitemp_bt/manifest.json index 0f8da91ffd0..c87f4988adf 100644 --- a/homeassistant/components/mitemp_bt/manifest.json +++ b/homeassistant/components/mitemp_bt/manifest.json @@ -1,10 +1,8 @@ { "domain": "mitemp_bt", - "name": "Mitemp bt", + "name": "Xiaomi Mijia BLE Temperature and Humidity Sensor", "documentation": "https://www.home-assistant.io/integrations/mitemp_bt", - "requirements": [ - "mitemp_bt==0.0.3" - ], + "requirements": ["mitemp_bt==0.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index b7cfa20a163..ab0409694d1 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -1,7 +1,7 @@ """Support for IP Cameras.""" import asyncio -import logging from contextlib import closing +import logging import aiohttp import async_timeout @@ -9,21 +9,21 @@ import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import ( - CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, CONF_AUTHENTICATION, + CONF_NAME, + CONF_PASSWORD, + CONF_USERNAME, + CONF_VERIFY_SSL, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, - CONF_VERIFY_SSL, -) -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.helpers.aiohttp_client import ( - async_get_clientsession, - async_aiohttp_proxy_web, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import ( + async_aiohttp_proxy_web, + async_get_clientsession, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mjpeg/manifest.json b/homeassistant/components/mjpeg/manifest.json index 93f01ac2e3a..6de13808991 100644 --- a/homeassistant/components/mjpeg/manifest.json +++ b/homeassistant/components/mjpeg/manifest.json @@ -1,6 +1,6 @@ { "domain": "mjpeg", - "name": "Mjpeg", + "name": "MJPEG IP Camera", "documentation": "https://www.home-assistant.io/integrations/mjpeg", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/mobile_app/.translations/da.json b/homeassistant/components/mobile_app/.translations/da.json index 551e9957254..54dc85e7255 100644 --- a/homeassistant/components/mobile_app/.translations/da.json +++ b/homeassistant/components/mobile_app/.translations/da.json @@ -1,14 +1,14 @@ { "config": { "abort": { - "install_app": "\u00c5bn Mobile App for at konfigurere integrationen med Home Assistant. Se [dokumentationen]({apps_url}) for at f\u00e5 en liste over kompatible apps." + "install_app": "\u00c5bn mobilappen for at konfigurere integrationen med Home Assistant. Se [dokumentationen]({apps_url}) for at f\u00e5 vist en liste over kompatible apps." }, "step": { "confirm": { - "description": "Vil du konfigurere Mobile App komponenten?", - "title": "Mobile App" + "description": "Vil du konfigurere mobilapp-komponenten?", + "title": "Mobilapp" } }, - "title": "Mobile App" + "title": "Mobilapp" } } \ No newline at end of file diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index bc9c6167da8..6fc4b342298 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -1,7 +1,11 @@ """Config flow for Mobile App.""" -from homeassistant import config_entries +import uuid -from .const import ATTR_DEVICE_NAME, DOMAIN +from homeassistant import config_entries +from homeassistant.components import person +from homeassistant.helpers import entity_registry + +from .const import ATTR_APP_ID, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, CONF_USER_ID, DOMAIN @config_entries.HANDLERS.register(DOMAIN) @@ -23,6 +27,26 @@ class MobileAppFlowHandler(config_entries.ConfigFlow): async def async_step_registration(self, user_input=None): """Handle a flow initialized during registration.""" + if ATTR_DEVICE_ID in user_input: + # Unique ID is combi of app + device ID. + await self.async_set_unique_id( + f"{user_input[ATTR_APP_ID]}-{user_input[ATTR_DEVICE_ID]}" + ) + else: + user_input[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") + + # Register device tracker entity and add to person registering app + ent_reg = await entity_registry.async_get_registry(self.hass) + devt_entry = ent_reg.async_get_or_create( + "device_tracker", + DOMAIN, + user_input[ATTR_DEVICE_ID], + suggested_object_id=user_input[ATTR_DEVICE_NAME], + ) + await person.async_add_user_device_tracker( + self.hass, user_input[CONF_USER_ID], devt_entry.entity_id + ) + return self.async_create_entry( title=user_input[ATTR_DEVICE_NAME], data=user_input ) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index 318076d5fd9..720cf7106e7 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -1,19 +1,4 @@ """Constants for mobile_app.""" -import voluptuous as vol - -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES as BINARY_SENSOR_CLASSES, -) -from homeassistant.components.device_tracker import ( - ATTR_BATTERY, - ATTR_GPS, - ATTR_GPS_ACCURACY, - ATTR_LOCATION_NAME, -) -from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES -from homeassistant.const import ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA -from homeassistant.helpers import config_validation as cv - DOMAIN = "mobile_app" STORAGE_KEY = DOMAIN @@ -71,100 +56,6 @@ ERR_ENCRYPTION_REQUIRED = "encryption_required" ERR_SENSOR_NOT_REGISTERED = "not_registered" ERR_SENSOR_DUPLICATE_UNIQUE_ID = "duplicate_unique_id" -WEBHOOK_TYPE_CALL_SERVICE = "call_service" -WEBHOOK_TYPE_FIRE_EVENT = "fire_event" -WEBHOOK_TYPE_GET_CONFIG = "get_config" -WEBHOOK_TYPE_GET_ZONES = "get_zones" -WEBHOOK_TYPE_REGISTER_SENSOR = "register_sensor" -WEBHOOK_TYPE_RENDER_TEMPLATE = "render_template" -WEBHOOK_TYPE_UPDATE_LOCATION = "update_location" -WEBHOOK_TYPE_UPDATE_REGISTRATION = "update_registration" -WEBHOOK_TYPE_UPDATE_SENSOR_STATES = "update_sensor_states" - -WEBHOOK_TYPES = [ - WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_GET_CONFIG, - WEBHOOK_TYPE_GET_ZONES, - WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, - WEBHOOK_TYPE_UPDATE_REGISTRATION, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES, -] - - -REGISTRATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_APP_DATA, default={}): dict, - vol.Required(ATTR_APP_ID): cv.string, - vol.Required(ATTR_APP_NAME): cv.string, - vol.Required(ATTR_APP_VERSION): cv.string, - vol.Required(ATTR_DEVICE_NAME): cv.string, - vol.Required(ATTR_MANUFACTURER): cv.string, - vol.Required(ATTR_MODEL): cv.string, - vol.Required(ATTR_OS_NAME): cv.string, - vol.Optional(ATTR_OS_VERSION): cv.string, - vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, - } -) - -UPDATE_REGISTRATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_APP_DATA, default={}): dict, - vol.Required(ATTR_APP_VERSION): cv.string, - vol.Required(ATTR_DEVICE_NAME): cv.string, - vol.Required(ATTR_MANUFACTURER): cv.string, - vol.Required(ATTR_MODEL): cv.string, - vol.Optional(ATTR_OS_VERSION): cv.string, - } -) - -WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( - { - vol.Required(ATTR_WEBHOOK_TYPE): cv.string, # vol.In(WEBHOOK_TYPES) - vol.Required(ATTR_WEBHOOK_DATA, default={}): vol.Any(dict, list), - vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, - vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, - } -) - -CALL_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_DOMAIN): cv.string, - vol.Required(ATTR_SERVICE): cv.string, - vol.Optional(ATTR_SERVICE_DATA, default={}): dict, - } -) - -FIRE_EVENT_SCHEMA = vol.Schema( - { - vol.Required(ATTR_EVENT_TYPE): cv.string, - vol.Optional(ATTR_EVENT_DATA, default={}): dict, - } -) - -RENDER_TEMPLATE_SCHEMA = vol.Schema( - { - str: { - vol.Required(ATTR_TEMPLATE): cv.template, - vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, - } - } -) - -UPDATE_LOCATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_LOCATION_NAME): cv.string, - vol.Required(ATTR_GPS): cv.gps, - vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, - vol.Optional(ATTR_BATTERY): cv.positive_int, - vol.Optional(ATTR_SPEED): cv.positive_int, - vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), - vol.Optional(ATTR_COURSE): cv.positive_int, - vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, - } -) ATTR_SENSOR_ATTRIBUTES = "attributes" ATTR_SENSOR_DEVICE_CLASS = "device_class" @@ -177,49 +68,5 @@ ATTR_SENSOR_TYPE_SENSOR = "sensor" ATTR_SENSOR_UNIQUE_ID = "unique_id" ATTR_SENSOR_UOM = "unit_of_measurement" -SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] - -COMBINED_CLASSES = sorted(set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES)) - SIGNAL_SENSOR_UPDATE = DOMAIN + "_sensor_update" SIGNAL_LOCATION_UPDATE = DOMAIN + "_location_update_{}" - -REGISTER_SENSOR_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( - vol.Lower, vol.In(COMBINED_CLASSES) - ), - vol.Required(ATTR_SENSOR_NAME): cv.string, - vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - vol.Optional(ATTR_SENSOR_UOM): cv.string, - vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - } -) - -UPDATE_SENSOR_STATE_SCHEMA = vol.All( - cv.ensure_list, - [ - vol.Schema( - { - vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, - vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, - vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), - vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), - vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, - } - ) - ], -) - -WEBHOOK_SCHEMAS = { - WEBHOOK_TYPE_CALL_SERVICE: CALL_SERVICE_SCHEMA, - WEBHOOK_TYPE_FIRE_EVENT: FIRE_EVENT_SCHEMA, - WEBHOOK_TYPE_REGISTER_SENSOR: REGISTER_SENSOR_SCHEMA, - WEBHOOK_TYPE_RENDER_TEMPLATE: RENDER_TEMPLATE_SCHEMA, - WEBHOOK_TYPE_UPDATE_LOCATION: UPDATE_LOCATION_SCHEMA, - WEBHOOK_TYPE_UPDATE_REGISTRATION: UPDATE_REGISTRATION_SCHEMA, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES: UPDATE_SENSOR_STATE_SCHEMA, -} diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index f58f80aa5fc..480bfee512f 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,6 +1,12 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + ATTR_GPS, + ATTR_GPS_ACCURACY, + ATTR_LOCATION_NAME, +) from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_LATITUDE, ATTR_LONGITUDE @@ -9,13 +15,9 @@ from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ATTR_ALTITUDE, - ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_GPS, - ATTR_GPS_ACCURACY, - ATTR_LOCATION_NAME, ATTR_SPEED, ATTR_VERTICAL_ACCURACY, SIGNAL_LOCATION_UPDATE, diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index cad25f371dd..400ff31be89 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -111,7 +111,7 @@ def error_response( def supports_encryption() -> bool: """Test if we support encryption.""" try: - import nacl # noqa: F401 pylint: disable=unused-import + import nacl # noqa: F401 pylint: disable=unused-import, import-outside-toplevel return True except OSError: diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 5be2d19789e..7d8d6c28243 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,25 +1,33 @@ """Provides an HTTP API for mobile_app.""" +import secrets from typing import Dict -import uuid from aiohttp.web import Request, Response from nacl.secret import SecretBox - -from homeassistant.auth.util import generate_secret +import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED +from homeassistant.helpers import config_validation as cv from .const import ( + ATTR_APP_DATA, + ATTR_APP_ID, + ATTR_APP_NAME, + ATTR_APP_VERSION, ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_OS_NAME, + ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION, CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET, CONF_USER_ID, DOMAIN, - REGISTRATION_SCHEMA, ) from .helpers import supports_encryption @@ -30,30 +38,47 @@ class RegistrationsView(HomeAssistantView): url = "/api/mobile_app/registrations" name = "api:mobile_app:register" - @RequestDataValidator(REGISTRATION_SCHEMA) + @RequestDataValidator( + vol.Schema( + { + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_ID): cv.string, + vol.Required(ATTR_APP_NAME): cv.string, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_DEVICE_ID): cv.string, # Added in 0.104 + vol.Required(ATTR_OS_NAME): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + vol.Required(ATTR_SUPPORTS_ENCRYPTION, default=False): cv.boolean, + }, + # To allow future apps to send more data + extra=vol.REMOVE_EXTRA, + ) + ) async def post(self, request: Request, data: Dict) -> Response: """Handle the POST request for registration.""" hass = request.app["hass"] - webhook_id = generate_secret() + webhook_id = secrets.token_hex() if hass.components.cloud.async_active_subscription(): data[ CONF_CLOUDHOOK_URL ] = await hass.components.cloud.async_create_cloudhook(webhook_id) - data[ATTR_DEVICE_ID] = str(uuid.uuid4()).replace("-", "") - data[CONF_WEBHOOK_ID] = webhook_id if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): - data[CONF_SECRET] = generate_secret(SecretBox.KEY_SIZE) + data[CONF_SECRET] = secrets.token_hex(SecretBox.KEY_SIZE) data[CONF_USER_ID] = request["hass_user"].id - ctx = {"source": "registration"} await hass.async_create_task( - hass.config_entries.flow.async_init(DOMAIN, context=ctx, data=data) + hass.config_entries.flow.async_init( + DOMAIN, data=data, context={"source": "registration"} + ) ) remote_ui_url = None diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 230a60fdf25..d6e1156b233 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -4,7 +4,8 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mobile_app", "requirements": ["PyNaCl==1.3.0"], - "dependencies": ["http", "webhook"], + "dependencies": ["http", "webhook", "person"], "after_dependencies": ["cloud"], - "codeowners": ["@robbiet480"] + "codeowners": ["@robbiet480"], + "quality_scale": "internal" } diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 8ac34c9af1d..b16c47e29c0 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -134,14 +134,14 @@ class MobileAppNotificationService(BaseNotificationService): response = await self._session.post(push_url, json=data) result = await response.json() - if response.status == 201: + if response.status in [200, 201, 202]: log_rate_limits(self.hass, entry_data[ATTR_DEVICE_NAME], result) - return + continue fallback_error = result.get("errorMessage", "Unknown error") - fallback_message = ( - "Internal server error, " "please try again later: " "{}" - ).format(fallback_error) + fallback_message = "Internal server error, please try again later: {}".format( + fallback_error + ) message = result.get("message", fallback_message) if response.status == 429: _LOGGER.warning(message) diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index c2bc6c94112..3a477d89925 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,10 +1,21 @@ """Webhook handlers for mobile_app.""" +from functools import wraps import logging from aiohttp.web import HTTPBadRequest, Request, Response import voluptuous as vol +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES as BINARY_SENSOR_CLASSES, +) +from homeassistant.components.device_tracker import ( + ATTR_BATTERY, + ATTR_GPS, + ATTR_GPS_ACCURACY, + ATTR_LOCATION_NAME, +) from homeassistant.components.frontend import MANIFEST_JSON +from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN from homeassistant.const import ( ATTR_DOMAIN, @@ -16,12 +27,17 @@ from homeassistant.const import ( ) from homeassistant.core import EventOrigin from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError -from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.template import attach from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.decorator import Registry from .const import ( + ATTR_ALTITUDE, + ATTR_APP_DATA, + ATTR_APP_VERSION, + ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_EVENT_DATA, @@ -29,11 +45,21 @@ from .const import ( ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION, + ATTR_SENSOR_ATTRIBUTES, + ATTR_SENSOR_DEVICE_CLASS, + ATTR_SENSOR_ICON, + ATTR_SENSOR_NAME, + ATTR_SENSOR_STATE, ATTR_SENSOR_TYPE, + ATTR_SENSOR_TYPE_BINARY_SENSOR, + ATTR_SENSOR_TYPE_SENSOR, ATTR_SENSOR_UNIQUE_ID, + ATTR_SENSOR_UOM, + ATTR_SPEED, ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE, ATTR_TEMPLATE_VARIABLES, + ATTR_VERTICAL_ACCURACY, ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED, ATTR_WEBHOOK_ENCRYPTED_DATA, @@ -50,18 +76,6 @@ from .const import ( ERR_SENSOR_NOT_REGISTERED, SIGNAL_LOCATION_UPDATE, SIGNAL_SENSOR_UPDATE, - WEBHOOK_PAYLOAD_SCHEMA, - WEBHOOK_SCHEMAS, - WEBHOOK_TYPE_CALL_SERVICE, - WEBHOOK_TYPE_FIRE_EVENT, - WEBHOOK_TYPE_GET_CONFIG, - WEBHOOK_TYPE_GET_ZONES, - WEBHOOK_TYPE_REGISTER_SENSOR, - WEBHOOK_TYPE_RENDER_TEMPLATE, - WEBHOOK_TYPE_UPDATE_LOCATION, - WEBHOOK_TYPE_UPDATE_REGISTRATION, - WEBHOOK_TYPE_UPDATE_SENSOR_STATES, - WEBHOOK_TYPES, ) from .helpers import ( _decrypt_payload, @@ -76,6 +90,46 @@ from .helpers import ( _LOGGER = logging.getLogger(__name__) +WEBHOOK_COMMANDS = Registry() + +COMBINED_CLASSES = set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES) +SENSOR_TYPES = [ATTR_SENSOR_TYPE_BINARY_SENSOR, ATTR_SENSOR_TYPE_SENSOR] + +WEBHOOK_PAYLOAD_SCHEMA = vol.Schema( + { + vol.Required(ATTR_WEBHOOK_TYPE): cv.string, + vol.Required(ATTR_WEBHOOK_DATA, default={}): vol.Any(dict, list), + vol.Optional(ATTR_WEBHOOK_ENCRYPTED, default=False): cv.boolean, + vol.Optional(ATTR_WEBHOOK_ENCRYPTED_DATA): cv.string, + } +) + + +def validate_schema(schema): + """Decorate a webhook function with a schema.""" + if isinstance(schema, dict): + schema = vol.Schema(schema) + + def wrapper(func): + """Wrap function so we validate schema.""" + + @wraps(func) + async def validate_and_run(hass, config_entry, data): + """Validate input and call handler.""" + try: + data = schema(data) + except vol.Invalid as ex: + err = vol.humanize.humanize_error(data, ex) + _LOGGER.error("Received invalid webhook payload: %s", err) + return empty_okay_response() + + return await func(hass, config_entry, data) + + return validate_and_run + + return wrapper + + async def handle_webhook( hass: HomeAssistantType, webhook_id: str, request: Request ) -> Response: @@ -83,12 +137,8 @@ async def handle_webhook( if webhook_id in hass.data[DOMAIN][DATA_DELETED_IDS]: return Response(status=410) - headers = {} - config_entry = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][webhook_id] - registration = config_entry.data - try: req_data = await request.json() except ValueError: @@ -97,11 +147,11 @@ async def handle_webhook( if ( ATTR_WEBHOOK_ENCRYPTED not in req_data - and registration[ATTR_SUPPORTS_ENCRYPTION] + and config_entry.data[ATTR_SUPPORTS_ENCRYPTION] ): _LOGGER.warning( "Refusing to accept unencrypted webhook from %s", - registration[ATTR_DEVICE_NAME], + config_entry.data[ATTR_DEVICE_NAME], ) return error_response(ERR_ENCRYPTION_REQUIRED, "Encryption required") @@ -118,197 +168,286 @@ async def handle_webhook( if req_data[ATTR_WEBHOOK_ENCRYPTED]: enc_data = req_data[ATTR_WEBHOOK_ENCRYPTED_DATA] - webhook_payload = _decrypt_payload(registration[CONF_SECRET], enc_data) + webhook_payload = _decrypt_payload(config_entry.data[CONF_SECRET], enc_data) - if webhook_type not in WEBHOOK_TYPES: + if webhook_type not in WEBHOOK_COMMANDS: _LOGGER.error("Received invalid webhook type: %s", webhook_type) return empty_okay_response() - data = webhook_payload + _LOGGER.debug( + "Received webhook payload for type %s: %s", webhook_type, webhook_payload + ) - _LOGGER.debug("Received webhook payload for type %s: %s", webhook_type, data) + return await WEBHOOK_COMMANDS[webhook_type](hass, config_entry, webhook_payload) - if webhook_type in WEBHOOK_SCHEMAS: + +@WEBHOOK_COMMANDS.register("call_service") +@validate_schema( + { + vol.Required(ATTR_DOMAIN): cv.string, + vol.Required(ATTR_SERVICE): cv.string, + vol.Optional(ATTR_SERVICE_DATA, default={}): dict, + } +) +async def webhook_call_service(hass, config_entry, data): + """Handle a call service webhook.""" + try: + await hass.services.async_call( + data[ATTR_DOMAIN], + data[ATTR_SERVICE], + data[ATTR_SERVICE_DATA], + blocking=True, + context=registration_context(config_entry.data), + ) + except (vol.Invalid, ServiceNotFound, Exception) as ex: + _LOGGER.error( + "Error when calling service during mobile_app " + "webhook (device name: %s): %s", + config_entry.data[ATTR_DEVICE_NAME], + ex, + ) + raise HTTPBadRequest() + + return empty_okay_response() + + +@WEBHOOK_COMMANDS.register("fire_event") +@validate_schema( + { + vol.Required(ATTR_EVENT_TYPE): cv.string, + vol.Optional(ATTR_EVENT_DATA, default={}): dict, + } +) +async def webhook_fire_event(hass, config_entry, data): + """Handle a fire event webhook.""" + event_type = data[ATTR_EVENT_TYPE] + hass.bus.async_fire( + event_type, + data[ATTR_EVENT_DATA], + EventOrigin.remote, + context=registration_context(config_entry.data), + ) + return empty_okay_response() + + +@WEBHOOK_COMMANDS.register("render_template") +@validate_schema( + { + str: { + vol.Required(ATTR_TEMPLATE): cv.template, + vol.Optional(ATTR_TEMPLATE_VARIABLES, default={}): dict, + } + } +) +async def webhook_render_template(hass, config_entry, data): + """Handle a render template webhook.""" + resp = {} + for key, item in data.items(): try: - data = WEBHOOK_SCHEMAS[webhook_type](webhook_payload) - except vol.Invalid as ex: - err = vol.humanize.humanize_error(webhook_payload, ex) - _LOGGER.error("Received invalid webhook payload: %s", err) - return empty_okay_response(headers=headers) + tpl = item[ATTR_TEMPLATE] + attach(hass, tpl) + resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) + except TemplateError as ex: + resp[key] = {"error": str(ex)} - context = registration_context(registration) + return webhook_response(resp, registration=config_entry.data) - if webhook_type == WEBHOOK_TYPE_CALL_SERVICE: - try: - await hass.services.async_call( - data[ATTR_DOMAIN], - data[ATTR_SERVICE], - data[ATTR_SERVICE_DATA], - blocking=True, - context=context, + +@WEBHOOK_COMMANDS.register("update_location") +@validate_schema( + { + vol.Optional(ATTR_LOCATION_NAME): cv.string, + vol.Required(ATTR_GPS): cv.gps, + vol.Required(ATTR_GPS_ACCURACY): cv.positive_int, + vol.Optional(ATTR_BATTERY): cv.positive_int, + vol.Optional(ATTR_SPEED): cv.positive_int, + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_COURSE): cv.positive_int, + vol.Optional(ATTR_VERTICAL_ACCURACY): cv.positive_int, + } +) +async def webhook_update_location(hass, config_entry, data): + """Handle an update location webhook.""" + hass.helpers.dispatcher.async_dispatcher_send( + SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data + ) + return empty_okay_response() + + +@WEBHOOK_COMMANDS.register("update_registration") +@validate_schema( + { + vol.Optional(ATTR_APP_DATA, default={}): dict, + vol.Required(ATTR_APP_VERSION): cv.string, + vol.Required(ATTR_DEVICE_NAME): cv.string, + vol.Required(ATTR_MANUFACTURER): cv.string, + vol.Required(ATTR_MODEL): cv.string, + vol.Optional(ATTR_OS_VERSION): cv.string, + } +) +async def webhook_update_registration(hass, config_entry, data): + """Handle an update registration webhook.""" + new_registration = {**config_entry.data, **data} + + device_registry = await dr.async_get_registry(hass) + + device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, config_entry.data[ATTR_DEVICE_ID])}, + manufacturer=new_registration[ATTR_MANUFACTURER], + model=new_registration[ATTR_MODEL], + name=new_registration[ATTR_DEVICE_NAME], + sw_version=new_registration[ATTR_OS_VERSION], + ) + + hass.config_entries.async_update_entry(config_entry, data=new_registration) + + return webhook_response( + safe_registration(new_registration), registration=new_registration, + ) + + +@WEBHOOK_COMMANDS.register("register_sensor") +@validate_schema( + { + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_DEVICE_CLASS): vol.All( + vol.Lower, vol.In(COMBINED_CLASSES) + ), + vol.Required(ATTR_SENSOR_NAME): cv.string, + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + vol.Optional(ATTR_SENSOR_UOM): cv.string, + vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, + } +) +async def webhook_register_sensor(hass, config_entry, data): + """Handle a register sensor webhook.""" + entity_type = data[ATTR_SENSOR_TYPE] + + unique_id = data[ATTR_SENSOR_UNIQUE_ID] + + unique_store_key = f"{config_entry.data[CONF_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, + f"{entity_type} {unique_id} already exists!", + status=409, + ) + + data[CONF_WEBHOOK_ID] = config_entry.data[CONF_WEBHOOK_ID] + + hass.data[DOMAIN][entity_type][unique_store_key] = data + + try: + await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) + except HomeAssistantError as ex: + _LOGGER.error("Error registering sensor: %s", ex) + return empty_okay_response() + + register_signal = "{}_{}_register".format(DOMAIN, data[ATTR_SENSOR_TYPE]) + async_dispatcher_send(hass, register_signal, data) + + return webhook_response( + {"success": True}, registration=config_entry.data, status=HTTP_CREATED, + ) + + +@WEBHOOK_COMMANDS.register("update_sensor_states") +@validate_schema( + vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict, + vol.Optional(ATTR_SENSOR_ICON, default="mdi:cellphone"): cv.icon, + vol.Required(ATTR_SENSOR_STATE): vol.Any(bool, str, int, float), + vol.Required(ATTR_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Required(ATTR_SENSOR_UNIQUE_ID): cv.string, + } ) - except (vol.Invalid, ServiceNotFound, Exception) as ex: + ], + ) +) +async def webhook_update_sensor_states(hass, config_entry, data): + """Handle an update sensor states webhook.""" + resp = {} + for sensor in data: + entity_type = sensor[ATTR_SENSOR_TYPE] + + unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] + + unique_store_key = f"{config_entry.data[CONF_WEBHOOK_ID]}_{unique_id}" + + if unique_store_key not in hass.data[DOMAIN][entity_type]: _LOGGER.error( - "Error when calling service during mobile_app " - "webhook (device name: %s): %s", - registration[ATTR_DEVICE_NAME], - ex, + "Refusing to update non-registered sensor: %s", unique_store_key ) - raise HTTPBadRequest() + err_msg = f"{entity_type} {unique_id} is not registered" + resp[unique_id] = { + "success": False, + "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg}, + } + continue - return empty_okay_response(headers=headers) + entry = hass.data[DOMAIN][entity_type][unique_store_key] - if webhook_type == WEBHOOK_TYPE_FIRE_EVENT: - event_type = data[ATTR_EVENT_TYPE] - hass.bus.async_fire( - event_type, data[ATTR_EVENT_DATA], EventOrigin.remote, context=context - ) - return empty_okay_response(headers=headers) + new_state = {**entry, **sensor} - if webhook_type == WEBHOOK_TYPE_RENDER_TEMPLATE: - resp = {} - for key, item in data.items(): - try: - tpl = item[ATTR_TEMPLATE] - attach(hass, tpl) - resp[key] = tpl.async_render(item.get(ATTR_TEMPLATE_VARIABLES)) - except TemplateError as ex: - resp[key] = {"error": str(ex)} + hass.data[DOMAIN][entity_type][unique_store_key] = new_state - return webhook_response(resp, registration=registration, headers=headers) - - if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION: - hass.helpers.dispatcher.async_dispatcher_send( - SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data - ) - return empty_okay_response(headers=headers) - - if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION: - new_registration = {**registration, **data} - - device_registry = await dr.async_get_registry(hass) - - device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, - identifiers={(DOMAIN, registration[ATTR_DEVICE_ID])}, - manufacturer=new_registration[ATTR_MANUFACTURER], - model=new_registration[ATTR_MODEL], - name=new_registration[ATTR_DEVICE_NAME], - sw_version=new_registration[ATTR_OS_VERSION], - ) - - hass.config_entries.async_update_entry(config_entry, data=new_registration) - - return webhook_response( - safe_registration(new_registration), - registration=registration, - headers=headers, - ) - - if webhook_type == WEBHOOK_TYPE_REGISTER_SENSOR: - entity_type = data[ATTR_SENSOR_TYPE] - - unique_id = data[ATTR_SENSOR_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, - f"{entity_type} {unique_id} already exists!", - status=409, - ) - - data[CONF_WEBHOOK_ID] = webhook_id - - hass.data[DOMAIN][entity_type][unique_store_key] = data + safe = savable_state(hass) try: - await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) + await hass.data[DOMAIN][DATA_STORE].async_save(safe) except HomeAssistantError as ex: - _LOGGER.error("Error registering sensor: %s", ex) + _LOGGER.error("Error updating mobile_app registration: %s", ex) return empty_okay_response() - register_signal = "{}_{}_register".format(DOMAIN, data[ATTR_SENSOR_TYPE]) - async_dispatcher_send(hass, register_signal, data) + async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) - return webhook_response( - {"success": True}, - registration=registration, - status=HTTP_CREATED, - headers=headers, - ) + resp[unique_id] = {"success": True} - if webhook_type == WEBHOOK_TYPE_UPDATE_SENSOR_STATES: - resp = {} - for sensor in data: - entity_type = sensor[ATTR_SENSOR_TYPE] + return webhook_response(resp, registration=config_entry.data) - unique_id = sensor[ATTR_SENSOR_UNIQUE_ID] - unique_store_key = f"{webhook_id}_{unique_id}" +@WEBHOOK_COMMANDS.register("get_zones") +async def webhook_get_zones(hass, config_entry, data): + """Handle a get zones webhook.""" + zones = [ + hass.states.get(entity_id) + for entity_id in sorted(hass.states.async_entity_ids(ZONE_DOMAIN)) + ] + return webhook_response(zones, registration=config_entry.data) - 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 = f"{entity_type} {unique_id} is not registered" - resp[unique_id] = { - "success": False, - "error": {"code": ERR_SENSOR_NOT_REGISTERED, "message": err_msg}, - } - continue - entry = hass.data[DOMAIN][entity_type][unique_store_key] +@WEBHOOK_COMMANDS.register("get_config") +async def webhook_get_config(hass, config_entry, data): + """Handle a get config webhook.""" + hass_config = hass.config.as_dict() - new_state = {**entry, **sensor} + resp = { + "latitude": hass_config["latitude"], + "longitude": hass_config["longitude"], + "elevation": hass_config["elevation"], + "unit_system": hass_config["unit_system"], + "location_name": hass_config["location_name"], + "time_zone": hass_config["time_zone"], + "components": hass_config["components"], + "version": hass_config["version"], + "theme_color": MANIFEST_JSON["theme_color"], + } - hass.data[DOMAIN][entity_type][unique_store_key] = new_state + if CONF_CLOUDHOOK_URL in config_entry.data: + resp[CONF_CLOUDHOOK_URL] = config_entry.data[CONF_CLOUDHOOK_URL] - safe = savable_state(hass) + try: + resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() + except hass.components.cloud.CloudNotAvailable: + pass - try: - await hass.data[DOMAIN][DATA_STORE].async_save(safe) - except HomeAssistantError as ex: - _LOGGER.error("Error updating mobile_app registration: %s", ex) - return empty_okay_response() - - async_dispatcher_send(hass, SIGNAL_SENSOR_UPDATE, new_state) - - resp[unique_id] = {"success": True} - - return webhook_response(resp, registration=registration, headers=headers) - - if webhook_type == WEBHOOK_TYPE_GET_ZONES: - zones = ( - hass.states.get(entity_id) - for entity_id in sorted(hass.states.async_entity_ids(ZONE_DOMAIN)) - ) - return webhook_response(list(zones), registration=registration, headers=headers) - - if webhook_type == WEBHOOK_TYPE_GET_CONFIG: - - hass_config = hass.config.as_dict() - - resp = { - "latitude": hass_config["latitude"], - "longitude": hass_config["longitude"], - "elevation": hass_config["elevation"], - "unit_system": hass_config["unit_system"], - "location_name": hass_config["location_name"], - "time_zone": hass_config["time_zone"], - "components": hass_config["components"], - "version": hass_config["version"], - "theme_color": MANIFEST_JSON["theme_color"], - } - - if CONF_CLOUDHOOK_URL in registration: - resp[CONF_CLOUDHOOK_URL] = registration[CONF_CLOUDHOOK_URL] - - try: - resp[CONF_REMOTE_UI_URL] = hass.components.cloud.async_remote_ui_url() - except hass.components.cloud.CloudNotAvailable: - pass - - return webhook_response(resp, registration=registration, headers=headers) + return webhook_response(resp, registration=config_entry.data) diff --git a/homeassistant/components/mochad/__init__.py b/homeassistant/components/mochad/__init__.py index 77426e8ae2c..683681b50a0 100644 --- a/homeassistant/components/mochad/__init__.py +++ b/homeassistant/components/mochad/__init__.py @@ -2,11 +2,16 @@ import logging import threading +from pymochad import controller, exceptions import voluptuous as vol +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -from homeassistant.const import CONF_HOST, CONF_PORT _LOGGER = logging.getLogger(__name__) @@ -37,8 +42,6 @@ def setup(hass, config): host = conf.get(CONF_HOST) port = conf.get(CONF_PORT) - from pymochad import exceptions - global CONTROLLER try: CONTROLLER = MochadCtrl(host, port) @@ -68,8 +71,6 @@ class MochadCtrl: self._host = host self._port = port - from pymochad import controller - self.ctrl = controller.PyMochad(server=self._host, port=self._port) @property diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 899908c34bd..871caadd95c 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -1,30 +1,32 @@ """Support for X10 dimmer over Mochad.""" import logging +from pymochad import device import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) -from homeassistant.components import mochad -from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_DEVICES, CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_validation as cv +from . import CONF_COMM_TYPE, CONTROLLER, DOMAIN, REQ_LOCK + _LOGGER = logging.getLogger(__name__) CONF_BRIGHTNESS_LEVELS = "brightness_levels" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_PLATFORM): mochad.DOMAIN, + vol.Required(CONF_PLATFORM): DOMAIN, CONF_DEVICES: [ { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): cv.x10_address, - vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + vol.Optional(CONF_COMM_TYPE): cv.string, vol.Optional(CONF_BRIGHTNESS_LEVELS, default=32): vol.All( vol.Coerce(int), vol.In([32, 64, 256]) ), @@ -37,7 +39,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up X10 dimmers over a mochad controller.""" devs = config.get(CONF_DEVICES) - add_entities([MochadLight(hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + add_entities([MochadLight(hass, CONTROLLER.ctrl, dev) for dev in devs]) return True @@ -46,12 +48,11 @@ class MochadLight(Light): def __init__(self, hass, ctrl, dev): """Initialize a Mochad Light Device.""" - from pymochad import device self._controller = ctrl self._address = dev[CONF_ADDRESS] self._name = dev.get(CONF_NAME, f"x10_light_dev_{self._address}") - self._comm_type = dev.get(mochad.CONF_COMM_TYPE, "pl") + self._comm_type = dev.get(CONF_COMM_TYPE, "pl") self.light = device.Device(ctrl, self._address, comm_type=self._comm_type) self._brightness = 0 self._state = self._get_device_status() @@ -64,7 +65,7 @@ class MochadLight(Light): def _get_device_status(self): """Get the status of the light from mochad.""" - with mochad.REQ_LOCK: + with REQ_LOCK: status = self.light.get_status().rstrip() return status == "on" @@ -106,7 +107,7 @@ class MochadLight(Light): def turn_on(self, **kwargs): """Send the command to turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS, 255) - with mochad.REQ_LOCK: + with REQ_LOCK: if self._brightness_levels > 32: out_brightness = self._calculate_brightness_value(brightness) self.light.send_cmd(f"xdim {out_brightness}") @@ -124,7 +125,7 @@ class MochadLight(Light): def turn_off(self, **kwargs): """Send the command to turn the light on.""" - with mochad.REQ_LOCK: + with REQ_LOCK: self.light.send_cmd("off") self._controller.read_data() # There is no persistence for X10 modules so we need to prepare diff --git a/homeassistant/components/mochad/manifest.json b/homeassistant/components/mochad/manifest.json index 8994223fe31..c103b8d3922 100644 --- a/homeassistant/components/mochad/manifest.json +++ b/homeassistant/components/mochad/manifest.json @@ -2,9 +2,7 @@ "domain": "mochad", "name": "Mochad", "documentation": "https://www.home-assistant.io/integrations/mochad", - "requirements": [ - "pymochad==0.2.0" - ], + "requirements": ["pymochad==0.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mochad/switch.py b/homeassistant/components/mochad/switch.py index 0713d50eb4b..14fb601f919 100644 --- a/homeassistant/components/mochad/switch.py +++ b/homeassistant/components/mochad/switch.py @@ -1,24 +1,27 @@ """Support for X10 switch over Mochad.""" import logging +from pymochad import device +from pymochad.exceptions import MochadException import voluptuous as vol -from homeassistant.components import mochad from homeassistant.components.switch import SwitchDevice -from homeassistant.const import CONF_NAME, CONF_DEVICES, CONF_PLATFORM, CONF_ADDRESS +from homeassistant.const import CONF_ADDRESS, CONF_DEVICES, CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_validation as cv +from . import CONF_COMM_TYPE, CONTROLLER, DOMAIN, REQ_LOCK + _LOGGER = logging.getLogger(__name__) PLATFORM_SCHEMA = vol.Schema( { - vol.Required(CONF_PLATFORM): mochad.DOMAIN, + vol.Required(CONF_PLATFORM): DOMAIN, CONF_DEVICES: [ { vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_ADDRESS): cv.x10_address, - vol.Optional(mochad.CONF_COMM_TYPE): cv.string, + vol.Optional(CONF_COMM_TYPE): cv.string, } ], } @@ -28,7 +31,7 @@ PLATFORM_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up X10 switches over a mochad controller.""" devs = config.get(CONF_DEVICES) - add_entities([MochadSwitch(hass, mochad.CONTROLLER.ctrl, dev) for dev in devs]) + add_entities([MochadSwitch(hass, CONTROLLER.ctrl, dev) for dev in devs]) return True @@ -37,12 +40,11 @@ class MochadSwitch(SwitchDevice): def __init__(self, hass, ctrl, dev): """Initialize a Mochad Switch Device.""" - from pymochad import device self._controller = ctrl self._address = dev[CONF_ADDRESS] self._name = dev.get(CONF_NAME, "x10_switch_dev_%s" % self._address) - self._comm_type = dev.get(mochad.CONF_COMM_TYPE, "pl") + self._comm_type = dev.get(CONF_COMM_TYPE, "pl") self.switch = device.Device(ctrl, self._address, comm_type=self._comm_type) # Init with false to avoid locking HA for long on CM19A (goes from rf # to pl via TM751, but not other way around) @@ -58,10 +60,9 @@ class MochadSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - from pymochad.exceptions import MochadException _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) - with mochad.REQ_LOCK: + with REQ_LOCK: try: # Recycle socket on new command to recover mochad connection self._controller.reconnect() @@ -75,10 +76,9 @@ class MochadSwitch(SwitchDevice): def turn_off(self, **kwargs): """Turn the switch off.""" - from pymochad.exceptions import MochadException _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) - with mochad.REQ_LOCK: + with REQ_LOCK: try: # Recycle socket on new command to recover mochad connection self._controller.reconnect() @@ -92,7 +92,7 @@ class MochadSwitch(SwitchDevice): def _get_device_status(self): """Get the status of the switch from mochad.""" - with mochad.REQ_LOCK: + with REQ_LOCK: status = self.switch.get_status().rstrip() return status == "on" diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index a6e901af749..823703ac4c9 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -2,6 +2,8 @@ import logging import threading +from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.const import ( @@ -91,9 +93,7 @@ def setup_client(client_config): client_type = client_config[CONF_TYPE] if client_type == "serial": - from pymodbus.client.sync import ModbusSerialClient as ModbusClient - - return ModbusClient( + return ModbusSerialClient( method=client_config[CONF_METHOD], port=client_config[CONF_PORT], baudrate=client_config[CONF_BAUDRATE], @@ -103,27 +103,20 @@ def setup_client(client_config): timeout=client_config[CONF_TIMEOUT], ) if client_type == "rtuovertcp": - from pymodbus.client.sync import ModbusTcpClient as ModbusClient - from pymodbus.transaction import ModbusRtuFramer - - return ModbusClient( + return ModbusTcpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], framer=ModbusRtuFramer, timeout=client_config[CONF_TIMEOUT], ) if client_type == "tcp": - from pymodbus.client.sync import ModbusTcpClient as ModbusClient - - return ModbusClient( + return ModbusTcpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], timeout=client_config[CONF_TIMEOUT], ) if client_type == "udp": - from pymodbus.client.sync import ModbusUdpClient as ModbusClient - - return ModbusClient( + return ModbusUdpClient( host=client_config[CONF_HOST], port=client_config[CONF_PORT], timeout=client_config[CONF_TIMEOUT], diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index cbefd1271d0..9a431d24b0c 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -1,11 +1,15 @@ """Support for Modbus Coil sensors.""" import logging +from typing import Optional import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_SLAVE +from homeassistant.components.binary_sensor import ( + DEVICE_CLASSES_SCHEMA, + PLATFORM_SCHEMA, + BinarySensorDevice, +) +from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE from homeassistant.helpers import config_validation as cv from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN @@ -21,6 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_SLAVE): cv.positive_int, } @@ -36,7 +41,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub = hass.data[MODBUS_DOMAIN][coil.get(CONF_HUB)] sensors.append( ModbusCoilSensor( - hub, coil.get(CONF_NAME), coil.get(CONF_SLAVE), coil.get(CONF_COIL) + hub, + coil.get(CONF_NAME), + coil.get(CONF_SLAVE), + coil.get(CONF_COIL), + coil.get(CONF_DEVICE_CLASS), ) ) @@ -46,12 +55,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class ModbusCoilSensor(BinarySensorDevice): """Modbus coil sensor.""" - def __init__(self, hub, name, slave, coil): + def __init__(self, hub, name, slave, coil, device_class): """Initialize the Modbus coil sensor.""" self._hub = hub self._name = name self._slave = int(slave) if slave else None self._coil = int(coil) + self._device_class = device_class self._value = None @property @@ -64,6 +74,11 @@ class ModbusCoilSensor(BinarySensorDevice): """Return the state of the sensor.""" return self._value + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + def update(self): """Update the state of the sensor.""" result = self._hub.read_coils(self._slave, self._coil, 1) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index c6764482d96..99ea686543d 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -6,8 +6,8 @@ import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_AUTO, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 356a9f6a9c0..92ebd5b8686 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,11 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": [ - "pymodbus==1.5.2" - ], + "requirements": ["pymodbus==1.5.2"], "dependencies": [], - "codeowners": [ - "@adamchengtkc" - ] + "codeowners": ["@adamchengtkc"] } diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 1a5c71812d6..484382983ac 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -1,12 +1,13 @@ """Support for Modbus Register sensors.""" import logging import struct +from typing import Any, Optional, Union -from typing import Any, Union import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA from homeassistant.const import ( + CONF_DEVICE_CLASS, CONF_NAME, CONF_OFFSET, CONF_SLAVE, @@ -67,6 +68,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_INT): vol.In( [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] ), + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_OFFSET, default=0): number, vol.Optional(CONF_PRECISION, default=0): cv.positive_int, @@ -100,7 +102,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) except KeyError: _LOGGER.error( - "Unable to detect data type for %s sensor, " "try a custom type", + "Unable to detect data type for %s sensor, try a custom type", register.get(CONF_NAME), ) continue @@ -117,7 +119,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if register.get(CONF_COUNT) * 2 != size: _LOGGER.error( - "Structure size (%d bytes) mismatch registers count " "(%d words)", + "Structure size (%d bytes) mismatch registers count (%d words)", size, register.get(CONF_COUNT), ) @@ -139,6 +141,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): register.get(CONF_OFFSET), structure, register.get(CONF_PRECISION), + register.get(CONF_DEVICE_CLASS), ) ) @@ -164,6 +167,7 @@ class ModbusRegisterSensor(RestoreEntity): offset, structure, precision, + device_class, ): """Initialize the modbus register sensor.""" self._hub = hub @@ -178,6 +182,7 @@ class ModbusRegisterSensor(RestoreEntity): self._offset = offset self._precision = precision self._structure = structure + self._device_class = device_class self._value = None async def async_added_to_hass(self): @@ -202,6 +207,11 @@ class ModbusRegisterSensor(RestoreEntity): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + return self._device_class + def update(self): """Update the state of the sensor.""" if self._register_type == REGISTER_TYPE_INPUT: diff --git a/homeassistant/components/modbus/services.yaml b/homeassistant/components/modbus/services.yaml index 8713257b47c..2158528814f 100644 --- a/homeassistant/components/modbus/services.yaml +++ b/homeassistant/components/modbus/services.yaml @@ -1,7 +1,7 @@ write_coil: description: Write to a modbus coil. fields: - address: {description: Address of the register to read., example: 0} + address: {description: Address of the register to write to., example: 0} state: {description: State to write., example: false} unit: {description: Address of the modbus unit., example: 21} write_register: diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index 43ef649f788..0ed33dedb57 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, @@ -236,7 +236,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._is_on = False else: _LOGGER.error( - "Unexpected response from hub %s, slave %s " "register %s, got 0x%2x", + "Unexpected response from hub %s, slave %s register %s, got 0x%2x", self._hub.name, self._slave, self._verify_register, diff --git a/homeassistant/components/modem_callerid/manifest.json b/homeassistant/components/modem_callerid/manifest.json index 80174b1a83a..a5d516c15ab 100644 --- a/homeassistant/components/modem_callerid/manifest.json +++ b/homeassistant/components/modem_callerid/manifest.json @@ -1,10 +1,8 @@ { "domain": "modem_callerid", - "name": "Modem callerid", + "name": "Modem Caller ID", "documentation": "https://www.home-assistant.io/integrations/modem_callerid", - "requirements": [ - "basicmodem==0.7" - ], + "requirements": ["basicmodem==0.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mold_indicator/manifest.json b/homeassistant/components/mold_indicator/manifest.json index 1205b53ccaf..3e5518c1930 100644 --- a/homeassistant/components/mold_indicator/manifest.json +++ b/homeassistant/components/mold_indicator/manifest.json @@ -1,8 +1,9 @@ { "domain": "mold_indicator", - "name": "Mold indicator", + "name": "Mold Indicator", "documentation": "https://www.home-assistant.io/integrations/mold_indicator", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index d0e5c7d51dd..0d6c6f55284 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -6,20 +6,19 @@ import voluptuous as vol from homeassistant import util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.core import callback from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - CONF_NAME, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change -import homeassistant.helpers.config_validation as cv - _LOGGER = logging.getLogger(__name__) ATTR_CRITICAL_TEMP = "estimated_critical_temp" @@ -111,7 +110,7 @@ class MoldIndicator(Entity): def mold_indicator_sensors_state_listener(entity, old_state, new_state): """Handle for state changes for dependent sensors.""" _LOGGER.debug( - "Sensor state change for %s that had old state %s " "and new state %s", + "Sensor state change for %s that had old state %s and new state %s", entity, old_state, new_state, @@ -189,7 +188,7 @@ class MoldIndicator(Entity): # Return an error if the sensor change its state to Unknown. if state.state == STATE_UNKNOWN: _LOGGER.error( - "Unable to parse temperature sensor %s with state:" " %s", + "Unable to parse temperature sensor %s with state: %s", state.entity_id, state.state, ) @@ -200,7 +199,7 @@ class MoldIndicator(Entity): if temp is None: _LOGGER.error( - "Unable to parse temperature sensor %s with state:" " %s", + "Unable to parse temperature sensor %s with state: %s", state.entity_id, state.state, ) @@ -212,7 +211,7 @@ class MoldIndicator(Entity): if unit == TEMP_CELSIUS: return temp _LOGGER.error( - "Temp sensor %s has unsupported unit: %s (allowed: %s, " "%s)", + "Temp sensor %s has unsupported unit: %s (allowed: %s, %s)", state.entity_id, unit, TEMP_CELSIUS, @@ -307,7 +306,7 @@ class MoldIndicator(Entity): if None in (self._dewpoint, self._calib_factor) or self._calib_factor == 0: _LOGGER.debug( - "Invalid inputs - dewpoint: %s," " calibration-factor: %s", + "Invalid inputs - dewpoint: %s, calibration-factor: %s", self._dewpoint, self._calib_factor, ) diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index 47db73ce0de..d071276bcec 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -1,12 +1,8 @@ { "domain": "monoprice", - "name": "Monoprice", + "name": "Monoprice 6-Zone Amplifier", "documentation": "https://www.home-assistant.io/integrations/monoprice", - "requirements": [ - "pymonoprice==0.3" - ], + "requirements": ["pymonoprice==0.3"], "dependencies": [], - "codeowners": [ - "@etsinko" - ] + "codeowners": ["@etsinko"] } diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 1b1d9d2adf4..20b2ecebcf4 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,9 +1,11 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" import logging +from pymonoprice import get_monoprice +from serial import SerialException import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -20,6 +22,7 @@ from homeassistant.const import ( STATE_ON, ) import homeassistant.helpers.config_validation as cv + from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT _LOGGER = logging.getLogger(__name__) @@ -68,9 +71,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Monoprice 6-zone amplifier platform.""" port = config.get(CONF_PORT) - from serial import SerialException - from pymonoprice import get_monoprice - try: monoprice = get_monoprice(port) except SerialException: diff --git a/homeassistant/components/moon/manifest.json b/homeassistant/components/moon/manifest.json index 56b5a1b8181..508cd8f8867 100644 --- a/homeassistant/components/moon/manifest.json +++ b/homeassistant/components/moon/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/moon", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/mopar/manifest.json b/homeassistant/components/mopar/manifest.json index ddb51c9e682..e8fae4fb069 100644 --- a/homeassistant/components/mopar/manifest.json +++ b/homeassistant/components/mopar/manifest.json @@ -2,9 +2,7 @@ "domain": "mopar", "name": "Mopar", "documentation": "https://www.home-assistant.io/integrations/mopar", - "requirements": [ - "motorparts==1.1.0" - ], + "requirements": ["motorparts==1.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mpchc/manifest.json b/homeassistant/components/mpchc/manifest.json index 7d419243472..89e4b872871 100644 --- a/homeassistant/components/mpchc/manifest.json +++ b/homeassistant/components/mpchc/manifest.json @@ -1,6 +1,6 @@ { "domain": "mpchc", - "name": "Mpchc", + "name": "Media Player Classic Home Cinema (MPC-HC)", "documentation": "https://www.home-assistant.io/integrations/mpchc", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/mpchc/media_player.py b/homeassistant/components/mpchc/media_player.py index 580156a5653..a3f2c500030 100644 --- a/homeassistant/components/mpchc/media_player.py +++ b/homeassistant/components/mpchc/media_player.py @@ -5,7 +5,7 @@ import re import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, diff --git a/homeassistant/components/mpd/manifest.json b/homeassistant/components/mpd/manifest.json index b7440f655db..f6230e73a1e 100644 --- a/homeassistant/components/mpd/manifest.json +++ b/homeassistant/components/mpd/manifest.json @@ -1,12 +1,8 @@ { "domain": "mpd", - "name": "Mpd", + "name": "Music Player Daemon (MPD)", "documentation": "https://www.home-assistant.io/integrations/mpd", - "requirements": [ - "python-mpd2==1.0.0" - ], + "requirements": ["python-mpd2==1.0.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 2628815727c..6460becbb3e 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -37,6 +37,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -98,6 +99,8 @@ class MpdDevice(MediaPlayerDevice): self._is_connected = False self._muted = False self._muted_volume = 0 + self._media_position_updated_at = None + self._media_position = None # set up MPD client self._client = mpd.MPDClient() @@ -130,6 +133,11 @@ class MpdDevice(MediaPlayerDevice): self._status = self._client.status() self._currentsong = self._client.currentsong() + position = self._status["time"] + if self._media_position != position: + self._media_position_updated_at = dt_util.utcnow() + self._media_position = position + self._update_playlists() @property @@ -188,6 +196,20 @@ class MpdDevice(MediaPlayerDevice): # Time does not exist for streams return self._currentsong.get("time") + @property + def media_position(self): + """Position of current playing media in seconds. + + This is returned as part of the mpd status rather than in the details + of the current song. + """ + return self._media_position + + @property + def media_position_updated_at(self): + """Last valid time of media position.""" + return self._media_position_updated_at + @property def media_title(self): """Return the title of current playing media.""" diff --git a/homeassistant/components/mqtt/.translations/da.json b/homeassistant/components/mqtt/.translations/da.json index ebe5696f514..93ea57d49ea 100644 --- a/homeassistant/components/mqtt/.translations/da.json +++ b/homeassistant/components/mqtt/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af MQTT" + "single_instance_allowed": "Kun en enkelt konfiguration af MQTT er tilladt." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til broker" @@ -10,20 +10,20 @@ "broker": { "data": { "broker": "Broker", - "discovery": "Aktiv\u00e9r opdagelse", + "discovery": "Aktiv\u00e9r automatisk fund", "password": "Adgangskode", "port": "Port", "username": "Brugernavn" }, - "description": "Indtast venligst forbindelsesindstillinger for din MQTT broker.", + "description": "Indtast venligst forbindelsesindstillinger for din MQTT-broker.", "title": "MQTT" }, "hassio_confirm": { "data": { "discovery": "Aktiv\u00e9r opdagelse" }, - "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT brokeren, der leveres af hass.io add-on {addon}?", - "title": "MQTT Broker via Hass.io add-on" + "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til MQTT-brokeren, der leveres af hass.io-tilf\u00f8jelsen {addon}?", + "title": "MQTT-broker via Hass.io-tilf\u00f8jelse" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/en.json b/homeassistant/components/mqtt/.translations/en.json index b0de6dcd782..ad18951a9d7 100644 --- a/homeassistant/components/mqtt/.translations/en.json +++ b/homeassistant/components/mqtt/.translations/en.json @@ -22,7 +22,7 @@ "data": { "discovery": "Enable discovery" }, - "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the hass.io add-on {addon}?", + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the Hass.io add-on {addon}?", "title": "MQTT Broker via Hass.io add-on" } }, diff --git a/homeassistant/components/mqtt/.translations/it.json b/homeassistant/components/mqtt/.translations/it.json index ed33b182a96..cf2b3ddf7d5 100644 --- a/homeassistant/components/mqtt/.translations/it.json +++ b/homeassistant/components/mqtt/.translations/it.json @@ -22,8 +22,8 @@ "data": { "discovery": "Attiva l'individuazione" }, - "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dall'add-on di Hass.io {addon}?", - "title": "Broker MQTT tramite l'add-on di Hass.io" + "description": "Vuoi configurare Home Assistant per connettersi al broker MQTT fornito dal componente aggiuntivo di Hass.io: {addon}?", + "title": "Broker MQTT tramite il componente aggiuntivo di Hass.io" } }, "title": "MQTT" diff --git a/homeassistant/components/mqtt/.translations/ko.json b/homeassistant/components/mqtt/.translations/ko.json index 1d50d5bd3c3..307a6aaadeb 100644 --- a/homeassistant/components/mqtt/.translations/ko.json +++ b/homeassistant/components/mqtt/.translations/ko.json @@ -22,7 +22,7 @@ "data": { "discovery": "\uae30\uae30 \uac80\uc0c9 \ud65c\uc131\ud654" }, - "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc73c\ub85c MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Hass.io \uc560\ub4dc\uc628\uc758 MQTT \ube0c\ub85c\ucee4" } }, diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index 99a760dce4a..8dcc0bded9f 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -22,7 +22,7 @@ "data": { "discovery": "Aktiver oppdagelse" }, - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT megler gitt av hass.io tillegget {addon}?", + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT broker som er levert av Hass.io-tillegget (addon)?", "title": "MQTT megler via Hass.io tillegg" } }, diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json index 0050d1b040d..84553cc536a 100644 --- a/homeassistant/components/mqtt/.translations/sl.json +++ b/homeassistant/components/mqtt/.translations/sl.json @@ -22,7 +22,7 @@ "data": { "discovery": "Omogo\u010di odkrivanje" }, - "description": "\u017delite konfigurirati Home Assistant-a za povezavo s posrednikom MQTT, ki ga ponuja hass.io add-on {addon} ?", + "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s posrednikom MQTT, ki ga ponuja dodatek Hass.io {addon} ?", "title": "MQTT Broker prek dodatka Hass.io" } }, diff --git a/homeassistant/components/mqtt/.translations/zh-Hant.json b/homeassistant/components/mqtt/.translations/zh-Hant.json index 535ed848793..09f2f44a902 100644 --- a/homeassistant/components/mqtt/.translations/zh-Hant.json +++ b/homeassistant/components/mqtt/.translations/zh-Hant.json @@ -22,7 +22,7 @@ "data": { "discovery": "\u958b\u555f\u641c\u5c0b" }, - "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u7d44\u4ef6 {addon} \u4e4b MQTT broker\uff1f", + "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u6574\u5408 {addon} \u4e4b MQTT broker\uff1f", "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 MQTT Broker" } }, diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ad9166e2410..a9d5ac93ebc 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,6 +1,5 @@ """Support for MQTT message handling.""" import asyncio -import sys from functools import partial, wraps import inspect from itertools import groupby @@ -10,6 +9,7 @@ from operator import attrgetter import os import socket import ssl +import sys import time from typing import Any, Callable, List, Optional, Union @@ -32,9 +32,9 @@ from homeassistant.const import ( ) from homeassistant.core import Event, ServiceCall, callback from homeassistant.exceptions import ( + ConfigEntryNotReady, HomeAssistantError, Unauthorized, - ConfigEntryNotReady, ) from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -47,16 +47,16 @@ from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow from . import config_flow, discovery, server # noqa: F401 pylint: disable=unused-import from .const import ( + ATTR_DISCOVERY_HASH, CONF_BROKER, CONF_DISCOVERY, - DEFAULT_DISCOVERY, CONF_STATE_TOPIC, - ATTR_DISCOVERY_HASH, - PROTOCOL_311, + DEFAULT_DISCOVERY, DEFAULT_QOS, + PROTOCOL_311, ) from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash -from .models import PublishPayloadType, Message, MessageCallbackType +from .models import Message, MessageCallbackType, PublishPayloadType from .subscription import async_subscribe_topics, async_unsubscribe_topics _LOGGER = logging.getLogger(__name__) @@ -136,10 +136,10 @@ def valid_topic(value: Any) -> str: raise vol.Invalid("MQTT topic name/filter must not be empty.") if len(raw_value) > 65535: raise vol.Invalid( - "MQTT topic name/filter must not be longer than " "65535 encoded bytes." + "MQTT topic name/filter must not be longer than 65535 encoded bytes." ) if "\0" in value: - raise vol.Invalid("MQTT topic name/filter must not contain null " "character.") + raise vol.Invalid("MQTT topic name/filter must not contain null character.") return value @@ -151,7 +151,7 @@ def valid_subscribe_topic(value: Any) -> str: i < len(value) - 1 and value[i + 1] != "/" ): raise vol.Invalid( - "Single-level wildcard must occupy an entire " "level of the filter" + "Single-level wildcard must occupy an entire level of the filter" ) index = value.find("#") @@ -164,7 +164,7 @@ def valid_subscribe_topic(value: Any) -> str: ) if len(value) > 1 and value[index - 1] != "/": raise vol.Invalid( - "Multi-level wildcard must be after a topic " "level separator." + "Multi-level wildcard must be after a topic level separator." ) return value diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 5e995494a64..6f9b1720102 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -135,6 +135,8 @@ ABBREVIATIONS = { "stat_off": "state_off", "stat_on": "state_on", "stat_open": "state_open", + "stat_locked": "state_locked", + "stat_unlocked": "state_unlocked", "stat_t": "state_topic", "stat_tpl": "state_template", "stat_val_tpl": "state_value_template", diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index f3ae36c5746..831c47c3621 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components import camera, mqtt from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_NAME, CONF_DEVICE +from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 9b46057a414..91a36a310cb 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -14,22 +14,25 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_AWAY, SUPPORT_TARGET_TEMPERATURE_RANGE, - PRESET_NONE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( ATTR_TEMPERATURE, CONF_DEVICE, @@ -165,8 +168,7 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( - CONF_FAN_MODE_LIST, - default=[HVAC_MODE_AUTO, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH], + CONF_FAN_MODE_LIST, default=[FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH], ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -305,7 +307,7 @@ class MqttClimate( MqttEntityDeviceInfo.__init__(self, device_config, config_entry) async def async_added_to_hass(self): - """Handle being added to home assistant.""" + """Handle being added to Home Assistant.""" await super().async_added_to_hass() await self._subscribe_topics() @@ -339,7 +341,7 @@ class MqttClimate( self._target_temp_high = config[CONF_TEMP_INITIAL] if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: - self._current_fan_mode = SPEED_LOW + self._current_fan_mode = FAN_LOW if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._current_swing_mode = HVAC_MODE_OFF if self._topic[CONF_MODE_STATE_TOPIC] is None: diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index d25d7ce21d3..bcc969f0354 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -5,9 +5,9 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES +from homeassistant.const import CONF_DEVICES, STATE_HOME, STATE_NOT_HOME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_DEVICES, STATE_NOT_HOME, STATE_HOME from . import CONF_QOS diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 95a850fb9e8..a72008c059f 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -15,7 +15,8 @@ from homeassistant.components.mqtt.discovery import ( clear_discovery_hash, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 829809dd9c3..ff57db7c8c1 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -8,7 +8,6 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -16,29 +15,12 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_WHITE_VALUE, - Light, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, - SUPPORT_COLOR, SUPPORT_WHITE_VALUE, -) -from homeassistant.const import ( - CONF_BRIGHTNESS, - CONF_COLOR_TEMP, - CONF_DEVICE, - CONF_EFFECT, - CONF_HS, - CONF_NAME, - CONF_OPTIMISTIC, - CONF_PAYLOAD_OFF, - CONF_PAYLOAD_ON, - STATE_ON, - CONF_RGB, - CONF_STATE, - CONF_VALUE_TEMPLATE, - CONF_WHITE_VALUE, - CONF_XY, + Light, ) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, @@ -52,8 +34,26 @@ from homeassistant.components.mqtt import ( MqttEntityDeviceInfo, subscription, ) -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.const import ( + CONF_BRIGHTNESS, + CONF_COLOR_TEMP, + CONF_DEVICE, + CONF_EFFECT, + CONF_HS, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_PAYLOAD_OFF, + CONF_PAYLOAD_ON, + CONF_RGB, + CONF_STATE, + CONF_VALUE_TEMPLATE, + CONF_WHITE_VALUE, + CONF_XY, + STATE_ON, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index c80ab2f95a7..dd69a8e87d6 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -5,9 +5,9 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt_template/ """ import logging + import voluptuous as vol -from homeassistant.core import callback from homeassistant.components import mqtt from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,21 +17,14 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, - Light, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, -) -from homeassistant.const import ( - CONF_DEVICE, - CONF_NAME, - CONF_OPTIMISTIC, - STATE_ON, - STATE_OFF, + Light, ) from homeassistant.components.mqtt import ( CONF_COMMAND_TOPIC, @@ -45,9 +38,17 @@ from homeassistant.components.mqtt import ( MqttEntityDeviceInfo, subscription, ) +from homeassistant.const import ( + CONF_DEVICE, + CONF_NAME, + CONF_OPTIMISTIC, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.color as color_util from .schema import MQTT_LIGHT_SCHEMA_SCHEMA diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index ccf8f2569fa..6910e955288 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -36,10 +36,16 @@ _LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_UNLOCK = "payload_unlock" +CONF_STATE_LOCKED = "state_locked" +CONF_STATE_UNLOCKED = "state_unlocked" + DEFAULT_NAME = "MQTT Lock" DEFAULT_OPTIMISTIC = False DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" +DEFAULT_STATE_LOCKED = "LOCKED" +DEFAULT_STATE_UNLOCKED = "UNLOCKED" + PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { @@ -50,6 +56,10 @@ PLATFORM_SCHEMA = ( vol.Optional( CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK ): cv.string, + vol.Optional(CONF_STATE_LOCKED, default=DEFAULT_STATE_LOCKED): cv.string, + vol.Optional( + CONF_STATE_UNLOCKED, default=DEFAULT_STATE_UNLOCKED + ): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) @@ -152,9 +162,9 @@ class MqttLock( payload = msg.payload if value_template is not None: payload = value_template.async_render_with_possible_json_value(payload) - if payload == self._config[CONF_PAYLOAD_LOCK]: + if payload == self._config[CONF_STATE_LOCKED]: self._state = True - elif payload == self._config[CONF_PAYLOAD_UNLOCK]: + elif payload == self._config[CONF_STATE_UNLOCKED]: self._state = False self.async_write_ha_state() diff --git a/homeassistant/components/mqtt/manifest.json b/homeassistant/components/mqtt/manifest.json index b8ec38ec100..c5063dc14a1 100644 --- a/homeassistant/components/mqtt/manifest.json +++ b/homeassistant/components/mqtt/manifest.json @@ -3,14 +3,7 @@ "name": "MQTT", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/mqtt", - "requirements": [ - "hbmqtt==0.9.5", - "paho-mqtt==1.5.0" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "requirements": ["hbmqtt==0.9.5", "paho-mqtt==1.5.0"], + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"] } diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 46aaa23732f..cfdecd3383d 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,5 +1,5 @@ """Modesl used by multiple MQTT modules.""" -from typing import Union, Callable +from typing import Callable, Union import attr diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 40a68195f26..8bacfa530bd 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -15,7 +15,7 @@ }, "hassio_confirm": { "title": "MQTT Broker via Hass.io add-on", - "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the hass.io add-on {addon}?", + "description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the Hass.io add-on {addon}?", "data": { "discovery": "Enable discovery" } diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 12fd4c51693..84f564e5c7e 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -8,14 +8,15 @@ import logging import voluptuous as vol -from homeassistant.components.vacuum import DOMAIN from homeassistant.components.mqtt import ATTR_DISCOVERY_HASH from homeassistant.components.mqtt.discovery import ( MQTT_DISCOVERY_NEW, clear_discovery_hash, ) +from homeassistant.components.vacuum import DOMAIN from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .schema import CONF_SCHEMA, LEGACY, STATE, MQTT_VACUUM_SCHEMA + +from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index d770cfbb7f8..6c08b18bc9c 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -1,10 +1,18 @@ """Support for Legacy MQTT vacuum.""" -import logging import json +import logging import voluptuous as vol from homeassistant.components import mqtt +from homeassistant.components.mqtt import ( + CONF_UNIQUE_ID, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from homeassistant.components.vacuum import ( SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, @@ -24,15 +32,6 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from homeassistant.components.mqtt import ( - CONF_UNIQUE_ID, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) - from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 40b3eeb752c..9dd5053d019 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -1,27 +1,39 @@ """Support for a State MQTT vacuum.""" -import logging import json +import logging import voluptuous as vol from homeassistant.components import mqtt +from homeassistant.components.mqtt import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_RETAIN, + CONF_STATE_TOPIC, + CONF_UNIQUE_ID, + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, + subscription, +) from homeassistant.components.vacuum import ( + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, - SUPPORT_START, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, SUPPORT_SEND_COMMAND, + SUPPORT_START, SUPPORT_STATUS, SUPPORT_STOP, - STATE_CLEANING, - STATE_DOCKED, - STATE_PAUSED, - STATE_IDLE, - STATE_RETURNING, - STATE_ERROR, StateVacuumDevice, ) from homeassistant.const import ( @@ -33,19 +45,6 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.components.mqtt import ( - CONF_UNIQUE_ID, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, - CONF_COMMAND_TOPIC, - CONF_RETAIN, - CONF_STATE_TOPIC, - CONF_QOS, -) - from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt_eventstream/__init__.py b/homeassistant/components/mqtt_eventstream/__init__.py index ce11c7cb933..48448355df8 100644 --- a/homeassistant/components/mqtt_eventstream/__init__.py +++ b/homeassistant/components/mqtt_eventstream/__init__.py @@ -4,7 +4,6 @@ import json import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.mqtt import valid_publish_topic, valid_subscribe_topic from homeassistant.const import ( ATTR_SERVICE_DATA, @@ -13,7 +12,7 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.core import EventOrigin, State +from homeassistant.core import EventOrigin, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.json import JSONEncoder diff --git a/homeassistant/components/mqtt_eventstream/manifest.json b/homeassistant/components/mqtt_eventstream/manifest.json index 0d36cd6616d..228269babe9 100644 --- a/homeassistant/components/mqtt_eventstream/manifest.json +++ b/homeassistant/components/mqtt_eventstream/manifest.json @@ -1,10 +1,8 @@ { "domain": "mqtt_eventstream", - "name": "Mqtt eventstream", + "name": "MQTT Eventstream", "documentation": "https://www.home-assistant.io/integrations/mqtt_eventstream", "requirements": [], - "dependencies": [ - "mqtt" - ], + "dependencies": ["mqtt"], "codeowners": [] } diff --git a/homeassistant/components/mqtt_json/device_tracker.py b/homeassistant/components/mqtt_json/device_tracker.py index a7dda18e7e6..8f64636b817 100644 --- a/homeassistant/components/mqtt_json/device_tracker.py +++ b/homeassistant/components/mqtt_json/device_tracker.py @@ -5,17 +5,17 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.core import callback -from homeassistant.components.mqtt import CONF_QOS from homeassistant.components.device_tracker import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv +from homeassistant.components.mqtt import CONF_QOS from homeassistant.const import ( - CONF_DEVICES, + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, + CONF_DEVICES, ) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt_json/manifest.json b/homeassistant/components/mqtt_json/manifest.json index b5448839794..7f3e2165085 100644 --- a/homeassistant/components/mqtt_json/manifest.json +++ b/homeassistant/components/mqtt_json/manifest.json @@ -1,10 +1,8 @@ { "domain": "mqtt_json", - "name": "Mqtt json", + "name": "MQTT JSON", "documentation": "https://www.home-assistant.io/integrations/mqtt_json", "requirements": [], - "dependencies": [ - "mqtt" - ], + "dependencies": ["mqtt"], "codeowners": [] } diff --git a/homeassistant/components/mqtt_room/manifest.json b/homeassistant/components/mqtt_room/manifest.json index 93450b00163..c3cd7de3a06 100644 --- a/homeassistant/components/mqtt_room/manifest.json +++ b/homeassistant/components/mqtt_room/manifest.json @@ -1,10 +1,8 @@ { "domain": "mqtt_room", - "name": "Mqtt room", + "name": "MQTT Room Presence", "documentation": "https://www.home-assistant.io/integrations/mqtt_room", "requirements": [], - "dependencies": [ - "mqtt" - ], + "dependencies": ["mqtt"], "codeowners": [] } diff --git a/homeassistant/components/mqtt_room/sensor.py b/homeassistant/components/mqtt_room/sensor.py index 5323383e892..d8dfa65f799 100644 --- a/homeassistant/components/mqtt_room/sensor.py +++ b/homeassistant/components/mqtt_room/sensor.py @@ -1,16 +1,16 @@ """Support for MQTT room presence detection.""" -import logging -import json from datetime import timedelta +import json +import logging import voluptuous as vol from homeassistant.components import mqtt -import homeassistant.helpers.config_validation as cv from homeassistant.components.mqtt import CONF_STATE_TOPIC from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_TIMEOUT, STATE_NOT_HOME, ATTR_ID +from homeassistant.const import ATTR_ID, CONF_NAME, CONF_TIMEOUT, STATE_NOT_HOME from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import dt, slugify diff --git a/homeassistant/components/mqtt_statestream/__init__.py b/homeassistant/components/mqtt_statestream/__init__.py index f99b341df5b..e35f2653283 100644 --- a/homeassistant/components/mqtt_statestream/__init__.py +++ b/homeassistant/components/mqtt_statestream/__init__.py @@ -3,6 +3,7 @@ import json import voluptuous as vol +from homeassistant.components.mqtt import valid_publish_topic from homeassistant.const import ( CONF_DOMAINS, CONF_ENTITIES, @@ -11,11 +12,10 @@ from homeassistant.const import ( MATCH_ALL, ) from homeassistant.core import callback -from homeassistant.components.mqtt import valid_publish_topic +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.json import JSONEncoder -import homeassistant.helpers.config_validation as cv CONF_BASE_TOPIC = "base_topic" CONF_PUBLISH_ATTRIBUTES = "publish_attributes" diff --git a/homeassistant/components/mqtt_statestream/manifest.json b/homeassistant/components/mqtt_statestream/manifest.json index 840a53591a8..fdf85d21fa5 100644 --- a/homeassistant/components/mqtt_statestream/manifest.json +++ b/homeassistant/components/mqtt_statestream/manifest.json @@ -1,10 +1,8 @@ { "domain": "mqtt_statestream", - "name": "Mqtt statestream", + "name": "MQTT Statestream", "documentation": "https://www.home-assistant.io/integrations/mqtt_statestream", "requirements": [], - "dependencies": [ - "mqtt" - ], + "dependencies": ["mqtt"], "codeowners": [] } diff --git a/homeassistant/components/mvglive/manifest.json b/homeassistant/components/mvglive/manifest.json index e47b7c07d8b..3df5234f963 100644 --- a/homeassistant/components/mvglive/manifest.json +++ b/homeassistant/components/mvglive/manifest.json @@ -1,10 +1,8 @@ { "domain": "mvglive", - "name": "Mvglive", + "name": "MVG", "documentation": "https://www.home-assistant.io/integrations/mvglive", - "requirements": [ - "PyMVGLive==1.1.4" - ], + "requirements": ["PyMVGLive==1.1.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json index 20933c9b2fc..93382974005 100644 --- a/homeassistant/components/mychevy/manifest.json +++ b/homeassistant/components/mychevy/manifest.json @@ -1,10 +1,8 @@ { "domain": "mychevy", - "name": "Mychevy", + "name": "myChevrolet", "documentation": "https://www.home-assistant.io/integrations/mychevy", - "requirements": [ - "mychevy==1.2.0" - ], + "requirements": ["mychevy==1.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mycroft/manifest.json b/homeassistant/components/mycroft/manifest.json index 5d5ee195381..5b9dfad5d37 100644 --- a/homeassistant/components/mycroft/manifest.json +++ b/homeassistant/components/mycroft/manifest.json @@ -2,9 +2,7 @@ "domain": "mycroft", "name": "Mycroft", "documentation": "https://www.home-assistant.io/integrations/mycroft", - "requirements": [ - "mycroftapi==2.0" - ], + "requirements": ["mycroftapi==2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index b6da7174f05..3f0895d9931 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -6,10 +6,10 @@ from pymyq.errors import MyQError import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( CONF_PASSWORD, @@ -97,7 +97,7 @@ class MyQDevice(CoverDevice): @property def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return self._device.device_id async def async_close_cover(self, **kwargs): diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 115c9cf515b..7e00e025bd3 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -1,10 +1,8 @@ { "domain": "myq", - "name": "Myq", + "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": [ - "pymyq==2.0.1" - ], + "requirements": ["pymyq==2.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/mysensors/__init__.py b/homeassistant/components/mysensors/__init__.py index cbedd947843..a528be15e14 100644 --- a/homeassistant/components/mysensors/__init__.py +++ b/homeassistant/components/mysensors/__init__.py @@ -25,7 +25,7 @@ from .const import ( MYSENSORS_GATEWAYS, ) from .device import get_mysensors_devices -from .gateway import get_mysensors_gateway, setup_gateways, finish_setup +from .gateway import finish_setup, get_mysensors_gateway, setup_gateways _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mysensors/climate.py b/homeassistant/components/mysensors/climate.py index dc053e60de1..4939c0c83e5 100644 --- a/homeassistant/components/mysensors/climate.py +++ b/homeassistant/components/mysensors/climate.py @@ -8,10 +8,10 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - HVAC_MODE_OFF, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT diff --git a/homeassistant/components/mysensors/device.py b/homeassistant/components/mysensors/device.py index 6d766530b04..e5853fce5ca 100644 --- a/homeassistant/components/mysensors/device.py +++ b/homeassistant/components/mysensors/device.py @@ -1,6 +1,6 @@ """Handle MySensors devices.""" -import logging from functools import partial +import logging from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON from homeassistant.core import callback diff --git a/homeassistant/components/mysensors/gateway.py b/homeassistant/components/mysensors/gateway.py index 366692205a7..903ec069b51 100644 --- a/homeassistant/components/mysensors/gateway.py +++ b/homeassistant/components/mysensors/gateway.py @@ -6,6 +6,7 @@ import socket import sys import async_timeout +from mysensors import mysensors import voluptuous as vol from homeassistant.const import CONF_OPTIMISTIC, EVENT_HOMEASSISTANT_STOP @@ -84,7 +85,6 @@ async def setup_gateways(hass, config): async def _get_gateway(hass, config, gateway_conf, persistence_file): """Return gateway after setup of the gateway.""" - from mysensors import mysensors conf = config[DOMAIN] persistence = conf[CONF_PERSISTENCE] diff --git a/homeassistant/components/mysensors/handler.py b/homeassistant/components/mysensors/handler.py index 0923b6bc8de..31e836f4767 100644 --- a/homeassistant/components/mysensors/handler.py +++ b/homeassistant/components/mysensors/handler.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util import decorator -from .const import MYSENSORS_GATEWAY_READY, CHILD_CALLBACK, NODE_CALLBACK +from .const import CHILD_CALLBACK, MYSENSORS_GATEWAY_READY, NODE_CALLBACK from .device import get_mysensors_devices from .helpers import discover_mysensors_platform, validate_set_msg diff --git a/homeassistant/components/mysensors/light.py b/homeassistant/components/mysensors/light.py index 8f0d0906311..19eb8e9e92c 100644 --- a/homeassistant/components/mysensors/light.py +++ b/homeassistant/components/mysensors/light.py @@ -11,8 +11,8 @@ from homeassistant.components.light import ( Light, ) from homeassistant.const import STATE_OFF, STATE_ON -from homeassistant.util.color import rgb_hex_to_rgb_list import homeassistant.util.color as color_util +from homeassistant.util.color import rgb_hex_to_rgb_list SUPPORT_MYSENSORS_RGBW = SUPPORT_COLOR | SUPPORT_WHITE_VALUE diff --git a/homeassistant/components/mysensors/manifest.json b/homeassistant/components/mysensors/manifest.json index 8701424ea60..b691bbafee2 100644 --- a/homeassistant/components/mysensors/manifest.json +++ b/homeassistant/components/mysensors/manifest.json @@ -1,15 +1,9 @@ { "domain": "mysensors", - "name": "Mysensors", + "name": "MySensors", "documentation": "https://www.home-assistant.io/integrations/mysensors", - "requirements": [ - "pymysensors==0.18.0" - ], + "requirements": ["pymysensors==0.18.0"], "dependencies": [], - "after_dependencies": [ - "mqtt" - ], - "codeowners": [ - "@MartinHjelmare" - ] + "after_dependencies": ["mqtt"], + "codeowners": ["@MartinHjelmare"] } diff --git a/homeassistant/components/mysensors/sensor.py b/homeassistant/components/mysensors/sensor.py index a7d1cad98fa..ddad451d20f 100644 --- a/homeassistant/components/mysensors/sensor.py +++ b/homeassistant/components/mysensors/sensor.py @@ -2,10 +2,10 @@ from homeassistant.components import mysensors from homeassistant.components.sensor import DOMAIN from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, - POWER_WATT, - ENERGY_KILO_WATT_HOUR, ) SENSORS = { diff --git a/homeassistant/components/mysensors/switch.py b/homeassistant/components/mysensors/switch.py index fecec53370b..ec28649d70f 100644 --- a/homeassistant/components/mysensors/switch.py +++ b/homeassistant/components/mysensors/switch.py @@ -1,10 +1,10 @@ """Support for MySensors switches.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import mysensors from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv from .const import DOMAIN as MYSENSORS_DOMAIN, SERVICE_SEND_IR_CODE diff --git a/homeassistant/components/mystrom/manifest.json b/homeassistant/components/mystrom/manifest.json index fe09461bc82..7d74fca92a2 100644 --- a/homeassistant/components/mystrom/manifest.json +++ b/homeassistant/components/mystrom/manifest.json @@ -1,14 +1,8 @@ { "domain": "mystrom", - "name": "Mystrom", + "name": "myStrom", "documentation": "https://www.home-assistant.io/integrations/mystrom", - "requirements": [ - "python-mystrom==0.5.0" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@fabaff" - ] + "requirements": ["python-mystrom==0.5.0"], + "dependencies": ["http"], + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/mythicbeastsdns/manifest.json b/homeassistant/components/mythicbeastsdns/manifest.json index b912a80f75d..2df68f084f8 100644 --- a/homeassistant/components/mythicbeastsdns/manifest.json +++ b/homeassistant/components/mythicbeastsdns/manifest.json @@ -1,10 +1,8 @@ { "domain": "mythicbeastsdns", - "name": "Mythicbeastsdns", + "name": "Mythic Beasts DNS", "documentation": "https://www.home-assistant.io/integrations/mythicbeastsdns", - "requirements": [ - "mbddns==0.1.2" - ], + "requirements": ["mbddns==0.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/n26/manifest.json b/homeassistant/components/n26/manifest.json index 7f010ec6d39..ff763b951a4 100644 --- a/homeassistant/components/n26/manifest.json +++ b/homeassistant/components/n26/manifest.json @@ -2,9 +2,7 @@ "domain": "n26", "name": "N26", "documentation": "https://www.home-assistant.io/integrations/n26", - "requirements": [ - "n26==0.2.7" - ], + "requirements": ["n26==0.2.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nad/manifest.json b/homeassistant/components/nad/manifest.json index 7e01f818e35..beb4a166f12 100644 --- a/homeassistant/components/nad/manifest.json +++ b/homeassistant/components/nad/manifest.json @@ -1,10 +1,8 @@ { "domain": "nad", - "name": "Nad", + "name": "NAD", "documentation": "https://www.home-assistant.io/integrations/nad", - "requirements": [ - "nad_receiver==0.0.11" - ], + "requirements": ["nad_receiver==0.0.11"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/namecheapdns/manifest.json b/homeassistant/components/namecheapdns/manifest.json index aa1e2eb7422..f743bfa5f42 100644 --- a/homeassistant/components/namecheapdns/manifest.json +++ b/homeassistant/components/namecheapdns/manifest.json @@ -1,10 +1,8 @@ { "domain": "namecheapdns", - "name": "Namecheapdns", + "name": "Namecheap FreeDNS", "documentation": "https://www.home-assistant.io/integrations/namecheapdns", - "requirements": [ - "defusedxml==0.6.0" - ], + "requirements": ["defusedxml==0.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nanoleaf/manifest.json b/homeassistant/components/nanoleaf/manifest.json index 3318ad3438f..0da755e1663 100644 --- a/homeassistant/components/nanoleaf/manifest.json +++ b/homeassistant/components/nanoleaf/manifest.json @@ -2,9 +2,7 @@ "domain": "nanoleaf", "name": "Nanoleaf", "documentation": "https://www.home-assistant.io/integrations/nanoleaf", - "requirements": [ - "pynanoleaf==0.0.5" - ], + "requirements": ["pynanoleaf==0.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json index ca180efa005..736234e92da 100644 --- a/homeassistant/components/neato/.translations/da.json +++ b/homeassistant/components/neato/.translations/da.json @@ -5,7 +5,7 @@ "invalid_credentials": "Ugyldige legitimationsoplysninger" }, "create_entry": { - "default": "Se [Neato-dokumentation] ({docs_url})." + "default": "Se [Neato-dokumentation]({docs_url})." }, "error": { "invalid_credentials": "Ugyldige legitimationsoplysninger", @@ -15,10 +15,11 @@ "user": { "data": { "password": "Adgangskode", - "username": "Brugernavn" + "username": "Brugernavn", + "vendor": "Udbyder" }, - "description": "Se [Neato-dokumentation] ({docs_url}).", - "title": "Neato kontooplysninger" + "description": "Se [Neato-dokumentation]({docs_url}).", + "title": "Neato-kontooplysninger" } }, "title": "Neato" diff --git a/homeassistant/components/neato/.translations/ko.json b/homeassistant/components/neato/.translations/ko.json index aeb591f7b20..391d0aee191 100644 --- a/homeassistant/components/neato/.translations/ko.json +++ b/homeassistant/components/neato/.translations/ko.json @@ -5,7 +5,7 @@ "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "create_entry": { - "default": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "default": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "error": { "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -18,7 +18,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", "vendor": "\uacf5\uae09 \uc5c5\uccb4" }, - "description": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "description": "[Neato \uc124\uba85\uc11c]({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", "title": "Neato \uacc4\uc815 \uc815\ubcf4" } }, diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 5a697e7b9ad..ad4eb02eccc 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,11 +1,11 @@ """Support for Neato botvac connected vacuum cleaners.""" import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from pybotvac import Account, Neato, Vorwerk from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException +import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 03f8089159e..af44874799b 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -1,14 +1,9 @@ { "domain": "neato", - "name": "Neato", + "name": "Neato Botvac", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", - "requirements": [ - "pybotvac==0.0.17" - ], + "requirements": ["pybotvac==0.0.17"], "dependencies": [], - "codeowners": [ - "@dshokouhi", - "@Santobert" - ] -} \ No newline at end of file + "codeowners": ["@dshokouhi", "@Santobert"] +} diff --git a/homeassistant/components/nederlandse_spoorwegen/manifest.json b/homeassistant/components/nederlandse_spoorwegen/manifest.json index c1360ecbe30..322452f5f59 100644 --- a/homeassistant/components/nederlandse_spoorwegen/manifest.json +++ b/homeassistant/components/nederlandse_spoorwegen/manifest.json @@ -1,10 +1,8 @@ { "domain": "nederlandse_spoorwegen", - "name": "Nederlandse spoorwegen", + "name": "Nederlandse Spoorwegen (NS)", "documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen", - "requirements": [ - "nsapi==2.7.4" - ], + "requirements": ["nsapi==2.7.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nello/manifest.json b/homeassistant/components/nello/manifest.json index e747bf7739f..06ca6931bc7 100644 --- a/homeassistant/components/nello/manifest.json +++ b/homeassistant/components/nello/manifest.json @@ -2,11 +2,7 @@ "domain": "nello", "name": "Nello", "documentation": "https://www.home-assistant.io/integrations/nello", - "requirements": [ - "pynello==2.0.2" - ], + "requirements": ["pynello==2.0.2"], "dependencies": [], - "codeowners": [ - "@pschmitt" - ] + "codeowners": ["@pschmitt"] } diff --git a/homeassistant/components/ness_alarm/__init__.py b/homeassistant/components/ness_alarm/__init__.py index 328d3506a97..7131ac505b5 100644 --- a/homeassistant/components/ness_alarm/__init__.py +++ b/homeassistant/components/ness_alarm/__init__.py @@ -1,17 +1,18 @@ """Support for Ness D8X/D16X devices.""" +from collections import namedtuple import datetime import logging -from collections import namedtuple +from nessclient import ArmingState, Client import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES from homeassistant.const import ( ATTR_CODE, ATTR_STATE, - EVENT_HOMEASSISTANT_STOP, - CONF_SCAN_INTERVAL, CONF_HOST, + CONF_SCAN_INTERVAL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -82,7 +83,6 @@ SERVICE_SCHEMA_AUX = vol.Schema( async def async_setup(hass, config): """Set up the Ness Alarm platform.""" - from nessclient import Client, ArmingState conf = config[DOMAIN] diff --git a/homeassistant/components/ness_alarm/alarm_control_panel.py b/homeassistant/components/ness_alarm/alarm_control_panel.py index d2feebfb64f..f77244a584e 100644 --- a/homeassistant/components/ness_alarm/alarm_control_panel.py +++ b/homeassistant/components/ness_alarm/alarm_control_panel.py @@ -2,6 +2,8 @@ import logging +from nessclient import ArmingState + import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -91,7 +93,6 @@ class NessAlarmPanel(alarm.AlarmControlPanel): @callback def _handle_arming_state_change(self, arming_state): """Handle arming state update.""" - from nessclient import ArmingState if arming_state == ArmingState.UNKNOWN: self._state = None diff --git a/homeassistant/components/ness_alarm/manifest.json b/homeassistant/components/ness_alarm/manifest.json index a1cae2df1d7..3cbed89b18c 100644 --- a/homeassistant/components/ness_alarm/manifest.json +++ b/homeassistant/components/ness_alarm/manifest.json @@ -1,12 +1,8 @@ { "domain": "ness_alarm", - "name": "Ness alarm", + "name": "Ness Alarm", "documentation": "https://www.home-assistant.io/integrations/ness_alarm", - "requirements": [ - "nessclient==0.9.15" - ], + "requirements": ["nessclient==0.9.15"], "dependencies": [], - "codeowners": [ - "@nickw444" - ] + "codeowners": ["@nickw444"] } diff --git a/homeassistant/components/nest/.translations/da.json b/homeassistant/components/nest/.translations/da.json index 7dfd1c8b250..39b85754c18 100644 --- a/homeassistant/components/nest/.translations/da.json +++ b/homeassistant/components/nest/.translations/da.json @@ -18,14 +18,14 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Nest.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" }, "link": { "data": { "code": "PIN-kode" }, "description": "For at forbinde din Nest-konto, [godkend din konto]({url}). \n\nEfter godkendelse skal du kopiere pin koden nedenfor.", - "title": "Link Nest-konto" + "title": "Forbind Nest-konto" } }, "title": "Nest" diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 32bbd009417..73a28aa121f 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -1,11 +1,11 @@ """Support for Nest devices.""" +from datetime import datetime, timedelta import logging import socket -from datetime import datetime, timedelta import threading from nest import Nest -from nest.nest import AuthorizationError, APIError +from nest.nest import APIError, AuthorizationError import voluptuous as vol from homeassistant import config_entries @@ -20,11 +20,11 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity -from .const import DOMAIN from . import local_auth +from .const import DOMAIN _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) @@ -216,7 +216,7 @@ async def async_setup_entry(hass, entry): structure.set_eta(trip_id, eta_begin, eta_end) else: _LOGGER.info( - "No thermostats found in structure: %s, " "unable to set ETA", + "No thermostats found in structure: %s, unable to set ETA", structure.name, ) diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index efc0bfbc822..34b4f6c5693 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -1,11 +1,11 @@ """Support for Nest Cameras.""" -import logging from datetime import timedelta +import logging import requests from homeassistant.components import nest -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_ON_OFF +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_ON_OFF, Camera from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 795ce5c80e9..f75e3a692f3 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -8,22 +8,22 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_AUTO, FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - SUPPORT_PRESET_MODE, - SUPPORT_FAN_MODE, - SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_AWAY, PRESET_ECO, PRESET_NONE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_COOL, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( ATTR_TEMPERATURE, diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index b78896b0499..b8fa2ad93f5 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -14,7 +14,6 @@ from homeassistant.util.json import load_json from .const import DOMAIN - DATA_FLOW_IMPL = "nest_flow_implementation" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 38d1827326d..8b0af5011ec 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -2,9 +2,10 @@ import asyncio from functools import partial -from nest.nest import NestAuth, AUTHORIZE_URL, AuthorizationError +from nest.nest import AUTHORIZE_URL, AuthorizationError, NestAuth from homeassistant.core import callback + from . import config_flow from .const import DOMAIN diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index b7d5f132045..c14f5982da0 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -3,11 +3,7 @@ "name": "Nest", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": [ - "python-nest==4.1.0" - ], + "requirements": ["python-nest==4.1.0"], "dependencies": [], - "codeowners": [ - "@awarecan" - ] + "codeowners": ["@awarecan"] } diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index de371f97e28..6becedde611 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -1,6 +1,6 @@ """Support for the Netatmo devices.""" -import logging from datetime import timedelta +import logging from urllib.error import HTTPError import pyatmo @@ -8,17 +8,17 @@ import voluptuous as vol from homeassistant.const import ( CONF_API_KEY, - CONF_PASSWORD, - CONF_USERNAME, CONF_DISCOVERY, + CONF_PASSWORD, CONF_URL, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle -from .const import DOMAIN, DATA_NETATMO_AUTH +from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,6 +30,7 @@ CONF_WEBHOOKS = "webhooks" SERVICE_ADDWEBHOOK = "addwebhook" SERVICE_DROPWEBHOOK = "dropwebhook" +SERVICE_SETSCHEDULE = "set_schedule" NETATMO_AUTH = None NETATMO_WEBHOOK_URL = None @@ -63,6 +64,7 @@ ATTR_IS_KNOWN = "is_known" ATTR_FACE_URL = "face_url" ATTR_SNAPSHOT_URL = "snapshot_url" ATTR_VIGNETTE_URL = "vignette_url" +ATTR_SCHEDULE = "schedule" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) MIN_TIME_BETWEEN_EVENT_UPDATES = timedelta(seconds=5) @@ -87,6 +89,8 @@ SCHEMA_SERVICE_ADDWEBHOOK = vol.Schema({vol.Optional(CONF_URL): cv.string}) SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) +SCHEMA_SERVICE_SETSCHEDULE = vol.Schema({vol.Required(ATTR_SCHEDULE): cv.string}) + def setup(hass, config): """Set up the Netatmo devices.""" @@ -106,6 +110,12 @@ def setup(hass, config): _LOGGER.error("Unable to connect to Netatmo API") return False + try: + home_data = pyatmo.HomeData(auth) + except pyatmo.NoDevice: + home_data = None + _LOGGER.debug("No climate device. Disable %s service", SERVICE_SETSCHEDULE) + # Store config to be used during entry setup hass.data[DATA_NETATMO_AUTH] = auth @@ -151,6 +161,20 @@ def setup(hass, config): schema=SCHEMA_SERVICE_DROPWEBHOOK, ) + def _service_setschedule(service): + """Service to change current home schedule.""" + schedule_name = service.data.get(ATTR_SCHEDULE) + home_data.switchHomeSchedule(schedule=schedule_name) + _LOGGER.info("Set home schedule to %s", schedule_name) + + if home_data is not None: + hass.services.register( + DOMAIN, + SERVICE_SETSCHEDULE, + _service_setschedule, + schema=SCHEMA_SERVICE_SETSCHEDULE, + ) + return True diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 1a40d3952e9..a449b7bb43d 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -8,8 +8,8 @@ from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensor from homeassistant.const import CONF_TIMEOUT from homeassistant.helpers import config_validation as cv -from .const import DATA_NETATMO_AUTH from . import CameraData +from .const import DATA_NETATMO_AUTH _LOGGER = logging.getLogger(__name__) @@ -155,9 +155,9 @@ class NetatmoBinarySensor(BinarySensorDevice): else: self._name = camera_name if module_name: - self._name += " / " + module_name + self._name += f" / {module_name}" self._sensor_name = sensor - self._name += " " + sensor + self._name += f" {sensor}" self._cameratype = camera_type self._state = None diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index f3bf6a6784c..546a5da3c15 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -6,20 +6,20 @@ import requests import voluptuous as vol from homeassistant.components.camera import ( - PLATFORM_SCHEMA, - Camera, - SUPPORT_STREAM, CAMERA_SERVICE_SCHEMA, + PLATFORM_SCHEMA, + SUPPORT_STREAM, + Camera, ) -from homeassistant.const import CONF_VERIFY_SSL, STATE_ON, STATE_OFF +from homeassistant.const import CONF_VERIFY_SSL, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) -from .const import DATA_NETATMO_AUTH, DOMAIN from . import CameraData +from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -106,7 +106,7 @@ class NetatmoCamera(Camera): self._camera_name = camera_name self._home = home if home: - self._name = home + " / " + camera_name + self._name = f"{home} / {camera_name}" else: self._name = camera_name self._cameratype = camera_type @@ -383,7 +383,7 @@ class NetatmoCamera(Camera): """Set light mode ('auto', 'on', 'off').""" if self.model == "Presence": try: - config = '{"mode":"' + mode + '"}' + config = f'{{"mode":"{mode}"}}' if self._localurl: requests.get( f"{self._localurl}/command/floodlight_set_config?config={config}", diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 8ba13a03889..9e320c303c8 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -1,34 +1,34 @@ """Support for Netatmo Smart thermostats.""" from datetime import timedelta import logging -from typing import Optional, List +from typing import List, Optional import pyatmo import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + DEFAULT_MIN_TEMP, HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_BOOST, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, - DEFAULT_MIN_TEMP, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( - TEMP_CELSIUS, + ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, CONF_NAME, PRECISION_HALVES, STATE_OFF, - ATTR_BATTERY_LEVEL, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle from .const import DATA_NETATMO_AUTH diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 232e99eeae8..ff421363506 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,11 +2,7 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": [ - "pyatmo==3.1.0" - ], - "dependencies": [ - "webhook" - ], + "requirements": ["pyatmo==3.1.0"], + "dependencies": ["webhook"], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 1ae076c6560..d4d624061f5 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -1,7 +1,7 @@ """Support for the Netatmo Weather Service.""" +from datetime import timedelta import logging import threading -from datetime import timedelta from time import time import pyatmo @@ -9,19 +9,20 @@ import requests import urllib3 import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_MODE, - TEMP_CELSIUS, + CONF_NAME, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import call_later from homeassistant.util import Throttle + from .const import DATA_NETATMO_AUTH, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netatmo/services.yaml b/homeassistant/components/netatmo/services.yaml index a928f4765e0..d8fa223780a 100644 --- a/homeassistant/components/netatmo/services.yaml +++ b/homeassistant/components/netatmo/services.yaml @@ -28,3 +28,10 @@ set_light_off: entity_id: description: Entity id. example: 'camera.living_room' + +set_schedule: + description: Set the home heating schedule + fields: + schedule: + description: Schedule name + example: Standard \ No newline at end of file diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index da4d15c28aa..cfddfe9d208 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -2,11 +2,7 @@ "domain": "netdata", "name": "Netdata", "documentation": "https://www.home-assistant.io/integrations/netdata", - "requirements": [ - "netdata==0.1.2" - ], + "requirements": ["netdata==0.1.2"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index 2e20f6423a5..3e87bcac53c 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -4,21 +4,21 @@ import logging from pynetgear import Netgear import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - CONF_PORT, - CONF_SSL, CONF_DEVICES, CONF_EXCLUDE, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_SSL, + CONF_USERNAME, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -116,7 +116,7 @@ class NetgearDeviceScanner(DeviceScanner): self.tracked_accesspoints and dev.conn_ap_mac in self.tracked_accesspoints ): - devices.append(dev.mac + "_" + dev.conn_ap_mac) + devices.append(f"{dev.mac}_{dev.conn_ap_mac}") return devices @@ -144,7 +144,7 @@ class NetgearDeviceScanner(DeviceScanner): ap_name = dev.name break - return name + " on " + ap_name + return f"{name} on {ap_name}" return name diff --git a/homeassistant/components/netgear/manifest.json b/homeassistant/components/netgear/manifest.json index af709c755c8..4cfffab7d73 100644 --- a/homeassistant/components/netgear/manifest.json +++ b/homeassistant/components/netgear/manifest.json @@ -2,9 +2,7 @@ "domain": "netgear", "name": "Netgear", "documentation": "https://www.home-assistant.io/integrations/netgear", - "requirements": [ - "pynetgear==0.6.1" - ], + "requirements": ["pynetgear==0.6.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 4758a13c391..ac36cc1eb44 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -8,6 +8,9 @@ import attr import eternalegypt import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_MONITORED_CONDITIONS, @@ -17,14 +20,11 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN -from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index f8c4c39cb83..0be2ca146b1 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -1,10 +1,8 @@ { "domain": "netgear_lte", - "name": "Netgear lte", + "name": "Netgear LTE", "documentation": "https://www.home-assistant.io/integrations/netgear_lte", - "requirements": [ - "eternalegypt==0.0.11" - ], + "requirements": ["eternalegypt==0.0.11"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 9700ee3c715..3e91394aa4f 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -4,7 +4,7 @@ import logging import attr import eternalegypt -from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService, DOMAIN +from homeassistant.components.notify import ATTR_TARGET, DOMAIN, BaseNotificationService from . import CONF_RECIPIENT, DATA_KEY diff --git a/homeassistant/components/netgear_lte/sensor.py b/homeassistant/components/netgear_lte/sensor.py index 5435df88727..49301a61e4f 100644 --- a/homeassistant/components/netgear_lte/sensor.py +++ b/homeassistant/components/netgear_lte/sensor.py @@ -5,7 +5,7 @@ from homeassistant.components.sensor import DOMAIN from homeassistant.exceptions import PlatformNotReady from . import CONF_MONITORED_CONDITIONS, DATA_KEY, LTEEntity -from .sensor_types import SENSOR_SMS, SENSOR_SMS_TOTAL, SENSOR_USAGE, SENSOR_UNITS +from .sensor_types import SENSOR_SMS, SENSOR_SMS_TOTAL, SENSOR_UNITS, SENSOR_USAGE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/netio/manifest.json b/homeassistant/components/netio/manifest.json index 3755746aee1..ef3d4a9519f 100644 --- a/homeassistant/components/netio/manifest.json +++ b/homeassistant/components/netio/manifest.json @@ -2,11 +2,7 @@ "domain": "netio", "name": "Netio", "documentation": "https://www.home-assistant.io/integrations/netio", - "requirements": [ - "pynetio==0.1.9.1" - ], - "dependencies": [ - "http" - ], + "requirements": ["pynetio==0.1.9.1"], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/neurio_energy/manifest.json b/homeassistant/components/neurio_energy/manifest.json index 735baf58e5a..06b8b6bce83 100644 --- a/homeassistant/components/neurio_energy/manifest.json +++ b/homeassistant/components/neurio_energy/manifest.json @@ -2,9 +2,7 @@ "domain": "neurio_energy", "name": "Neurio energy", "documentation": "https://www.home-assistant.io/integrations/neurio_energy", - "requirements": [ - "neurio==0.3.1" - ], + "requirements": ["neurio==0.3.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nextbus/manifest.json b/homeassistant/components/nextbus/manifest.json index 525947a8c74..581d5d62693 100644 --- a/homeassistant/components/nextbus/manifest.json +++ b/homeassistant/components/nextbus/manifest.json @@ -1,8 +1,8 @@ { - "domain": "nextbus", - "name": "NextBus", - "documentation": "https://www.home-assistant.io/integrations/nextbus", - "dependencies": [], - "codeowners": ["@vividboarder"], - "requirements": ["py_nextbusnext==0.1.4"] + "domain": "nextbus", + "name": "NextBus", + "documentation": "https://www.home-assistant.io/integrations/nextbus", + "dependencies": [], + "codeowners": ["@vividboarder"], + "requirements": ["py_nextbusnext==0.1.4"] } diff --git a/homeassistant/components/nextbus/sensor.py b/homeassistant/components/nextbus/sensor.py index 7622bd133f0..5909804ebd1 100644 --- a/homeassistant/components/nextbus/sensor.py +++ b/homeassistant/components/nextbus/sensor.py @@ -1,13 +1,13 @@ """NextBus sensor.""" -import logging from itertools import chain +import logging +from py_nextbus import NextBusClient import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME -from homeassistant.const import DEVICE_CLASS_TIMESTAMP +from homeassistant.const import CONF_NAME, DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utc_from_timestamp @@ -94,8 +94,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): stop = config[CONF_STOP] name = config.get(CONF_NAME) - from py_nextbus import NextBusClient - client = NextBusClient(output_format="json") # Ensures that the tags provided are valid, also logs out valid values @@ -227,7 +225,9 @@ class NextBusDepartureSensor(Entity): return # Generate list of upcoming times - self._attributes["upcoming"] = ", ".join(p["minutes"] for p in predictions) + self._attributes["upcoming"] = ", ".join( + sorted(p["minutes"] for p in predictions) + ) latest_prediction = maybe_first(predictions) self._state = utc_from_timestamp( diff --git a/homeassistant/components/nfandroidtv/manifest.json b/homeassistant/components/nfandroidtv/manifest.json index 45eedf8a154..859a704cc63 100644 --- a/homeassistant/components/nfandroidtv/manifest.json +++ b/homeassistant/components/nfandroidtv/manifest.json @@ -1,6 +1,6 @@ { "domain": "nfandroidtv", - "name": "Nfandroidtv", + "name": "Notifications for Android TV / FireTV", "documentation": "https://www.home-assistant.io/integrations/nfandroidtv", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/nfandroidtv/notify.py b/homeassistant/components/nfandroidtv/notify.py index 36eed0a11db..52f0af607bc 100644 --- a/homeassistant/components/nfandroidtv/notify.py +++ b/homeassistant/components/nfandroidtv/notify.py @@ -7,9 +7,6 @@ import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth import voluptuous as vol -from homeassistant.const import CONF_TIMEOUT, CONF_HOST -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -17,6 +14,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_HOST, CONF_TIMEOUT +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/niko_home_control/manifest.json b/homeassistant/components/niko_home_control/manifest.json index 0e168601c4c..2a8ec9ab270 100644 --- a/homeassistant/components/niko_home_control/manifest.json +++ b/homeassistant/components/niko_home_control/manifest.json @@ -1,10 +1,8 @@ { "domain": "niko_home_control", - "name": "Niko home control", + "name": "Niko Home Control", "documentation": "https://www.home-assistant.io/integrations/niko_home_control", - "requirements": [ - "niko-home-control==0.2.1" - ], + "requirements": ["niko-home-control==0.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nilu/manifest.json b/homeassistant/components/nilu/manifest.json index 77df26312e9..df4e704200f 100644 --- a/homeassistant/components/nilu/manifest.json +++ b/homeassistant/components/nilu/manifest.json @@ -1,12 +1,8 @@ { "domain": "nilu", - "name": "Nilu", + "name": "Norwegian Institute for Air Research (NILU)", "documentation": "https://www.home-assistant.io/integrations/nilu", - "requirements": [ - "niluclient==0.1.2" - ], + "requirements": ["niluclient==0.1.2"], "dependencies": [], - "codeowners": [ - "@hfurubotten" - ] -} \ No newline at end of file + "codeowners": ["@hfurubotten"] +} diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 0c72f4f43ea..fba84c936f5 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -1,8 +1,9 @@ """Support for the Nissan Leaf Carwings/Nissan Connect API.""" -from datetime import datetime, timedelta import asyncio +from datetime import datetime, timedelta import logging import sys + from pycarwings2 import CarwingsError, Session import voluptuous as vol @@ -123,9 +124,7 @@ def setup(hass, config): # for the charging request to reach the car. result = await hass.async_add_executor_job(data_store.leaf.start_charging) if result: - _LOGGER.debug( - "Start charging sent, " "request updated data in 1 minute" - ) + _LOGGER.debug("Start charging sent, request updated data in 1 minute") check_charge_at = utcnow() + timedelta(minutes=1) data_store.next_update = check_charge_at async_track_point_in_utc_time( @@ -413,7 +412,7 @@ class LeafDataStore: for attempt in range(MAX_RESPONSE_ATTEMPTS): if attempt > 0: _LOGGER.debug( - "Climate data not in yet (%s) (%s). " "Waiting (%s) seconds", + "Climate data not in yet (%s) (%s). Waiting (%s) seconds", self.leaf.vin, attempt, PYCARWINGS2_SLEEP, diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index a3181292147..717eb0d1d6a 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -1,12 +1,8 @@ { "domain": "nissan_leaf", - "name": "Nissan leaf", + "name": "Nissan Leaf", "documentation": "https://www.home-assistant.io/integrations/nissan_leaf", - "requirements": [ - "pycarwings2==2.9" - ], + "requirements": ["pycarwings2==2.9"], "dependencies": [], - "codeowners": [ - "@filcole" - ] + "codeowners": ["@filcole"] } diff --git a/homeassistant/components/nissan_leaf/sensor.py b/homeassistant/components/nissan_leaf/sensor.py index cd82735d207..bbdb8ad7527 100644 --- a/homeassistant/components/nissan_leaf/sensor.py +++ b/homeassistant/components/nissan_leaf/sensor.py @@ -69,7 +69,7 @@ class LeafRangeSensor(LeafEntity): """Nissan Leaf Range Sensor.""" def __init__(self, car, ac_on): - """Set-up range sensor. Store if AC on.""" + """Set up range sensor. Store if AC on.""" self._ac_on = ac_on super().__init__(car) @@ -83,7 +83,7 @@ class LeafRangeSensor(LeafEntity): def log_registration(self): """Log registration.""" _LOGGER.debug( - "Registered LeafRangeSensor integration with HASS for VIN %s", + "Registered LeafRangeSensor integration with Home Assistant for VIN %s", self.car.leaf.vin, ) diff --git a/homeassistant/components/nissan_leaf/switch.py b/homeassistant/components/nissan_leaf/switch.py index 77ba5ad125b..e15d595d65b 100644 --- a/homeassistant/components/nissan_leaf/switch.py +++ b/homeassistant/components/nissan_leaf/switch.py @@ -32,7 +32,7 @@ class LeafClimateSwitch(LeafEntity, ToggleEntity): def log_registration(self): """Log registration.""" _LOGGER.debug( - "Registered LeafClimateSwitch integration with HASS for VIN %s", + "Registered LeafClimateSwitch integration with Home Assistant for VIN %s", self.car.leaf.vin, ) diff --git a/homeassistant/components/nmap_tracker/manifest.json b/homeassistant/components/nmap_tracker/manifest.json index b207bd07a42..2d3aa0437fe 100644 --- a/homeassistant/components/nmap_tracker/manifest.json +++ b/homeassistant/components/nmap_tracker/manifest.json @@ -1,11 +1,8 @@ { "domain": "nmap_tracker", - "name": "Nmap tracker", + "name": "Nmap Tracker", "documentation": "https://www.home-assistant.io/integrations/nmap_tracker", - "requirements": [ - "python-nmap==0.6.1", - "getmac==0.8.1" - ], + "requirements": ["python-nmap==0.6.1", "getmac==0.8.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nmbs/manifest.json b/homeassistant/components/nmbs/manifest.json index 5fe5f743fd3..89de2288c54 100644 --- a/homeassistant/components/nmbs/manifest.json +++ b/homeassistant/components/nmbs/manifest.json @@ -1,12 +1,8 @@ { "domain": "nmbs", - "name": "Nmbs", + "name": "NMBS", "documentation": "https://www.home-assistant.io/integrations/nmbs", - "requirements": [ - "pyrail==0.0.3" - ], + "requirements": ["pyrail==0.0.3"], "dependencies": [], - "codeowners": [ - "@thibmaek" - ] + "codeowners": ["@thibmaek"] } diff --git a/homeassistant/components/nmbs/sensor.py b/homeassistant/components/nmbs/sensor.py index 26802808c0a..35c928deb37 100644 --- a/homeassistant/components/nmbs/sensor.py +++ b/homeassistant/components/nmbs/sensor.py @@ -92,7 +92,7 @@ class NMBSLiveBoard(Entity): """Initialize the sensor for getting liveboard data.""" self._station = live_station self._api_client = api_client - + self._unique_id = f"nmbs_live_{self._station}" self._attrs = {} self._state = None @@ -101,6 +101,11 @@ class NMBSLiveBoard(Entity): """Return the sensor default name.""" return "NMBS Live" + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id + @property def icon(self): """Return the default icon or an alert icon if delays.""" diff --git a/homeassistant/components/no_ip/__init__.py b/homeassistant/components/no_ip/__init__.py index 70ac7099d30..12d0fb08ad3 100644 --- a/homeassistant/components/no_ip/__init__.py +++ b/homeassistant/components/no_ip/__init__.py @@ -5,11 +5,11 @@ from datetime import timedelta import logging import aiohttp -from aiohttp.hdrs import USER_AGENT, AUTHORIZATION +from aiohttp.hdrs import AUTHORIZATION, USER_AGENT import async_timeout import voluptuous as vol -from homeassistant.const import CONF_DOMAIN, CONF_TIMEOUT, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_DOMAIN, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/no_ip/manifest.json b/homeassistant/components/no_ip/manifest.json index 438b61136eb..d192b716008 100644 --- a/homeassistant/components/no_ip/manifest.json +++ b/homeassistant/components/no_ip/manifest.json @@ -1,10 +1,8 @@ { "domain": "no_ip", - "name": "No ip", + "name": "No-IP.com", "documentation": "https://www.home-assistant.io/integrations/no_ip", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/noaa_tides/manifest.json b/homeassistant/components/noaa_tides/manifest.json index 069fc238f72..9bb9d7b2f1f 100644 --- a/homeassistant/components/noaa_tides/manifest.json +++ b/homeassistant/components/noaa_tides/manifest.json @@ -1,10 +1,8 @@ { "domain": "noaa_tides", - "name": "Noaa tides", + "name": "NOAA Tides", "documentation": "https://www.home-assistant.io/integrations/noaa_tides", - "requirements": [ - "py_noaa==0.3.0" - ], + "requirements": ["py_noaa==0.3.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/norway_air/manifest.json b/homeassistant/components/norway_air/manifest.json index e7ee2d2dcd5..62218c52174 100644 --- a/homeassistant/components/norway_air/manifest.json +++ b/homeassistant/components/norway_air/manifest.json @@ -1,10 +1,8 @@ { "domain": "norway_air", - "name": "Norway air", + "name": "Om Luftkvalitet i Norge (Norway Air)", "documentation": "https://www.home-assistant.io/integrations/norway_air", - "requirements": [ - "pyMetno==0.4.6" - ], + "requirements": ["pyMetno==0.4.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 6ede7f18da7..8211fdc0828 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -1,20 +1,19 @@ """Provides functionality to notify people.""" import asyncio -import logging from functools import partial +import logging from typing import Optional import voluptuous as vol -from homeassistant.setup import async_prepare_setup_platform -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_prepare_setup_platform from homeassistant.util import slugify - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notify/manifest.json b/homeassistant/components/notify/manifest.json index 6586496abbe..5361947bfd9 100644 --- a/homeassistant/components/notify/manifest.json +++ b/homeassistant/components/notify/manifest.json @@ -1,10 +1,9 @@ { "domain": "notify", - "name": "Notify", + "name": "Notifications", "documentation": "https://www.home-assistant.io/integrations/notify", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/notion/.translations/da.json b/homeassistant/components/notion/.translations/da.json index 2373920effe..bf17b41d777 100644 --- a/homeassistant/components/notion/.translations/da.json +++ b/homeassistant/components/notion/.translations/da.json @@ -9,7 +9,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Brugernavn/e-mail adresse" + "username": "Brugernavn/e-mailadresse" }, "title": "Udfyld dine oplysninger" } diff --git a/homeassistant/components/notion/.translations/hu.json b/homeassistant/components/notion/.translations/hu.json index 2f7664cf74e..79878858ddc 100644 --- a/homeassistant/components/notion/.translations/hu.json +++ b/homeassistant/components/notion/.translations/hu.json @@ -11,7 +11,7 @@ "password": "Jelsz\u00f3", "username": "Felhaszn\u00e1l\u00f3n\u00e9v/Email C\u00edm" }, - "title": "T\u00f6ltse ki adatait" + "title": "T\u00f6ltsd ki az adataid" } } } diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 85495c040fa..5079348e821 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -17,7 +17,6 @@ from . import ( SENSOR_WINDOW_HINGED_VERTICAL, NotionEntity, ) - from .const import DATA_CLIENT, DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/notion/config_flow.py b/homeassistant/components/notion/config_flow.py index affda29e4d6..2af231d582e 100644 --- a/homeassistant/components/notion/config_flow.py +++ b/homeassistant/components/notion/config_flow.py @@ -1,4 +1,6 @@ """Config flow to configure the Notion integration.""" +from aionotion import async_get_client +from aionotion.errors import NotionError import voluptuous as vol from homeassistant import config_entries @@ -40,8 +42,6 @@ class NotionFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from aionotion import async_get_client - from aionotion.errors import NotionError if not user_input: return await self._show_form() diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 2a400754b46..6afcc74a713 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -3,11 +3,7 @@ "name": "Notion", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/notion", - "requirements": [ - "aionotion==1.1.0" - ], + "requirements": ["aionotion==1.1.0"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/nsw_fuel_station/manifest.json b/homeassistant/components/nsw_fuel_station/manifest.json index 744497c22c4..0a76c46ba2c 100644 --- a/homeassistant/components/nsw_fuel_station/manifest.json +++ b/homeassistant/components/nsw_fuel_station/manifest.json @@ -1,12 +1,8 @@ { "domain": "nsw_fuel_station", - "name": "Nsw fuel station", + "name": "NSW Fuel Station Price", "documentation": "https://www.home-assistant.io/integrations/nsw_fuel_station", - "requirements": [ - "nsw-fuel-api-client==1.0.10" - ], + "requirements": ["nsw-fuel-api-client==1.0.10"], "dependencies": [], - "codeowners": [ - "@nickw444" - ] + "codeowners": ["@nickw444"] } diff --git a/homeassistant/components/nsw_fuel_station/sensor.py b/homeassistant/components/nsw_fuel_station/sensor.py index a84aa554be9..b4cd7bd161e 100644 --- a/homeassistant/components/nsw_fuel_station/sensor.py +++ b/homeassistant/components/nsw_fuel_station/sensor.py @@ -3,11 +3,12 @@ import datetime import logging from typing import Optional +from nsw_fuel import FuelCheckClient, FuelCheckError import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.light import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -52,7 +53,6 @@ NOTIFICATION_TITLE = "NSW Fuel Station Sensor Setup" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NSW Fuel Station sensor.""" - from nsw_fuel import FuelCheckClient station_id = config[CONF_STATION_ID] fuel_types = config[CONF_FUEL_TYPES] @@ -97,7 +97,6 @@ class StationPriceData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the internal data using the API client.""" - from nsw_fuel import FuelCheckError if self._reference_data is None: try: diff --git a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json index 7dd7d10d6be..b2dcfe10cf6 100644 --- a/homeassistant/components/nsw_rural_fire_service_feed/manifest.json +++ b/homeassistant/components/nsw_rural_fire_service_feed/manifest.json @@ -1,12 +1,8 @@ { "domain": "nsw_rural_fire_service_feed", - "name": "Nsw rural fire service feed", + "name": "NSW Rural Fire Service Incidents", "documentation": "https://www.home-assistant.io/integrations/nsw_rural_fire_service_feed", - "requirements": [ - "aio_geojson_nsw_rfs_incidents==0.1" - ], + "requirements": ["aio_geojson_nsw_rfs_incidents==0.1"], "dependencies": [], - "codeowners": [ - "@exxamalte" - ] + "codeowners": ["@exxamalte"] } diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index 3d055f8f975..fa011443245 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -1,10 +1,8 @@ { "domain": "nuheat", - "name": "Nuheat", + "name": "NuHeat", "documentation": "https://www.home-assistant.io/integrations/nuheat", - "requirements": [ - "nuheat==0.3.0" - ], + "requirements": ["nuheat==0.3.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nuimo_controller/manifest.json b/homeassistant/components/nuimo_controller/manifest.json index a88faaa6f30..58969bcafe2 100644 --- a/homeassistant/components/nuimo_controller/manifest.json +++ b/homeassistant/components/nuimo_controller/manifest.json @@ -2,9 +2,7 @@ "domain": "nuimo_controller", "name": "Nuimo controller", "documentation": "https://www.home-assistant.io/integrations/nuimo_controller", - "requirements": [ - "--only-binary=all nuimo==0.1.0" - ], + "requirements": ["--only-binary=all nuimo==0.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index 21231c27873..a44e70f9aa9 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -1,10 +1,8 @@ { "domain": "nut", - "name": "Nut", + "name": "Network UPS Tools (NUT)", "documentation": "https://www.home-assistant.io/integrations/nut", - "requirements": [ - "pynut2==2.1.2" - ], + "requirements": ["pynut2==2.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 34e3bfaf086..bdf0eaafc99 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -189,7 +189,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data.update(no_throttle=True) except data.pynuterror as err: _LOGGER.error( - "Failure while testing NUT status retrieval. " "Cannot continue setup: %s", + "Failure while testing NUT status retrieval. Cannot continue setup: %s", err, ) raise PlatformNotReady diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index b112a9ea4ea..85867b32bde 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -1,8 +1,8 @@ { - "domain": "nws", - "name": "National Weather Service", - "documentation": "https://www.home-assistant.io/integrations/nws", - "dependencies": [], - "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==0.8.1"] + "domain": "nws", + "name": "National Weather Service (NWS)", + "documentation": "https://www.home-assistant.io/integrations/nws", + "dependencies": [], + "codeowners": ["@MatthewFlamm"], + "requirements": ["pynws==0.8.1"] } diff --git a/homeassistant/components/nws/weather.py b/homeassistant/components/nws/weather.py index 23cf84411a3..c22700f1cf8 100644 --- a/homeassistant/components/nws/weather.py +++ b/homeassistant/components/nws/weather.py @@ -9,32 +9,32 @@ 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, + ATTR_FORECAST_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, ) from homeassistant.const import ( CONF_API_KEY, - CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, + CONF_NAME, LENGTH_KILOMETERS, LENGTH_METERS, LENGTH_MILES, PRESSURE_HPA, - PRESSURE_PA, PRESSURE_INHG, + PRESSURE_PA, 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.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import Throttle from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index 62bc7ae32bb..7a064ef0d00 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -1,6 +1,7 @@ """Support for NX584 alarm control panels.""" import logging +from nx584 import client import requests import voluptuous as vol @@ -56,7 +57,6 @@ class NX584Alarm(alarm.AlarmControlPanel): def __init__(self, hass, url, name): """Init the nx584 alarm panel.""" - from nx584 import client self._hass = hass self._name = name diff --git a/homeassistant/components/nx584/binary_sensor.py b/homeassistant/components/nx584/binary_sensor.py index 6f1c66d8d87..f6006ff2de4 100644 --- a/homeassistant/components/nx584/binary_sensor.py +++ b/homeassistant/components/nx584/binary_sensor.py @@ -3,13 +3,14 @@ import logging import threading import time +from nx584 import client as nx584_client import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES, - BinarySensorDevice, PLATFORM_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -39,7 +40,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NX584 binary sensor platform.""" - from nx584 import client as nx584_client host = config.get(CONF_HOST) port = config.get(CONF_PORT) diff --git a/homeassistant/components/nx584/manifest.json b/homeassistant/components/nx584/manifest.json index 64f72986d07..72d9a270775 100644 --- a/homeassistant/components/nx584/manifest.json +++ b/homeassistant/components/nx584/manifest.json @@ -1,10 +1,8 @@ { "domain": "nx584", - "name": "Nx584", + "name": "NX584", "documentation": "https://www.home-assistant.io/integrations/nx584", - "requirements": [ - "pynx584==0.4" - ], + "requirements": ["pynx584==0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/nzbget/manifest.json b/homeassistant/components/nzbget/manifest.json index 1dc16fdba40..a72ede807e2 100644 --- a/homeassistant/components/nzbget/manifest.json +++ b/homeassistant/components/nzbget/manifest.json @@ -1,6 +1,6 @@ { "domain": "nzbget", - "name": "Nzbget", + "name": "NZBGet", "documentation": "https://www.home-assistant.io/integrations/nzbget", "requirements": ["pynzbgetapi==0.2.0"], "dependencies": [], diff --git a/homeassistant/components/oasa_telematics/manifest.json b/homeassistant/components/oasa_telematics/manifest.json index e6cc998352d..0d524094b10 100644 --- a/homeassistant/components/oasa_telematics/manifest.json +++ b/homeassistant/components/oasa_telematics/manifest.json @@ -1,10 +1,8 @@ { - "domain": "oasa_telematics", - "name": "OASA Telematics", - "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", - "requirements": [ - "oasatelematics==0.3" - ], - "dependencies": [], - "codeowners": [] -} \ No newline at end of file + "domain": "oasa_telematics", + "name": "OASA Telematics", + "documentation": "https://www.home-assistant.io/integrations/oasa_telematics/", + "requirements": ["oasatelematics==0.3"], + "dependencies": [], + "codeowners": [] +} diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index 09087663b47..3a979922eba 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -1,11 +1,8 @@ { - "domain": "obihai", - "name": "Obihai", - "documentation": "https://www.home-assistant.io/integrations/obihai", - "requirements": [ - "pyobihai==1.2.0" - ], - "dependencies": [], - "codeowners": ["@dshokouhi"] - } - \ No newline at end of file + "domain": "obihai", + "name": "Obihai", + "documentation": "https://www.home-assistant.io/integrations/obihai", + "requirements": ["pyobihai==1.2.0"], + "dependencies": [], + "codeowners": ["@dshokouhi"] +} diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 89bfee7d4ee..13d09de0542 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -1,10 +1,9 @@ """Support for Obihai Sensors.""" +from datetime import timedelta import logging -from datetime import timedelta -import voluptuous as vol - from pyobihai import PyObihai +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( @@ -13,10 +12,8 @@ from homeassistant.const import ( CONF_USERNAME, DEVICE_CLASS_TIMESTAMP, ) - -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv - +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/octoprint/__init__.py b/homeassistant/components/octoprint/__init__.py index bc71b1a5911..f73e525efe3 100644 --- a/homeassistant/components/octoprint/__init__.py +++ b/homeassistant/components/octoprint/__init__.py @@ -2,23 +2,23 @@ import logging import time +from aiohttp.hdrs import CONTENT_TYPE import requests import voluptuous as vol -from aiohttp.hdrs import CONTENT_TYPE from homeassistant.components.discovery import SERVICE_OCTOPRINT from homeassistant.const import ( CONF_API_KEY, + CONF_BINARY_SENSORS, CONF_HOST, - CONTENT_TYPE_JSON, + CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PATH, CONF_PORT, - CONF_SSL, - TEMP_CELSIUS, - CONF_MONITORED_CONDITIONS, CONF_SENSORS, - CONF_BINARY_SENSORS, + CONF_SSL, + CONTENT_TYPE_JSON, + TEMP_CELSIUS, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -45,7 +45,7 @@ def ensure_valid_path(value): """Validate the path, ensuring it starts and ends with a /.""" vol.Schema(cv.string)(value) if value[0] != "/": - value = "/" + value + value = f"/{value}" if value[-1] != "/": value += "/" return value @@ -189,7 +189,7 @@ class OctoPrintAPI: tools = [] if self.number_of_tools > 0: for tool_number in range(0, self.number_of_tools): - tools.append("tool" + str(tool_number)) + tools.append(f"tool{tool_number!s}") if self.bed: tools.append("bed") if not self.bed and self.number_of_tools == 0: @@ -231,18 +231,16 @@ class OctoPrintAPI: self.printer_error_logged = False return response.json() except Exception as conn_exc: # pylint: disable=broad-except - log_string = "Failed to update OctoPrint status. " + " Error: %s" % ( - conn_exc - ) + log_string = "Failed to update OctoPrint status. Error: %s" % conn_exc # Only log the first failure if endpoint == "job": - log_string = "Endpoint: job " + log_string + log_string = f"Endpoint: job {log_string}" if not self.job_error_logged: _LOGGER.error(log_string) self.job_error_logged = True self.job_available = False elif endpoint == "printer": - log_string = "Endpoint: printer " + log_string + log_string = f"Endpoint: printer {log_string}" if not self.printer_error_logged: _LOGGER.error(log_string) self.printer_error_logged = True diff --git a/homeassistant/components/octoprint/manifest.json b/homeassistant/components/octoprint/manifest.json index 77f6a22e69f..d63a543f227 100644 --- a/homeassistant/components/octoprint/manifest.json +++ b/homeassistant/components/octoprint/manifest.json @@ -1,6 +1,6 @@ { "domain": "octoprint", - "name": "Octoprint", + "name": "OctoPrint", "documentation": "https://www.home-assistant.io/integrations/octoprint", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/oem/manifest.json b/homeassistant/components/oem/manifest.json index 1634676a60b..8be08a6e0dd 100644 --- a/homeassistant/components/oem/manifest.json +++ b/homeassistant/components/oem/manifest.json @@ -1,10 +1,8 @@ { "domain": "oem", - "name": "Oem", + "name": "OpenEnergyMonitor WiFi Thermostat", "documentation": "https://www.home-assistant.io/integrations/oem", - "requirements": [ - "oemthermostat==1.1" - ], + "requirements": ["oemthermostat==1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ohmconnect/manifest.json b/homeassistant/components/ohmconnect/manifest.json index c958d23e725..0a3fbe678ac 100644 --- a/homeassistant/components/ohmconnect/manifest.json +++ b/homeassistant/components/ohmconnect/manifest.json @@ -1,12 +1,8 @@ { "domain": "ohmconnect", - "name": "Ohmconnect", + "name": "OhmConnect", "documentation": "https://www.home-assistant.io/integrations/ohmconnect", - "requirements": [ - "defusedxml==0.6.0" - ], + "requirements": ["defusedxml==0.6.0"], "dependencies": [], - "codeowners": [ - "@robbiet480" - ] + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index a9606e25bad..490ebbe75b3 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -66,9 +66,7 @@ class OhmconnectSensor(Entity): def update(self): """Get the latest data from OhmConnect.""" try: - url = ("https://login.ohmconnect.com" "/verify-ohm-hour/{}").format( - self._ohmid - ) + url = "https://login.ohmconnect.com/verify-ohm-hour/{}".format(self._ohmid) response = requests.get(url, timeout=10) root = ET.fromstring(response.text) diff --git a/homeassistant/components/ombi/manifest.json b/homeassistant/components/ombi/manifest.json index 0407aa5a106..a2629a8fdca 100644 --- a/homeassistant/components/ombi/manifest.json +++ b/homeassistant/components/ombi/manifest.json @@ -1,8 +1,8 @@ { - "domain": "ombi", - "name": "Ombi", - "documentation": "https://www.home-assistant.io/integrations/ombi/", - "dependencies": [], - "codeowners": ["@larssont"], - "requirements": ["pyombi==0.1.10"] + "domain": "ombi", + "name": "Ombi", + "documentation": "https://www.home-assistant.io/integrations/ombi/", + "dependencies": [], + "codeowners": ["@larssont"], + "requirements": ["pyombi==0.1.10"] } diff --git a/homeassistant/components/onboarding/__init__.py b/homeassistant/components/onboarding/__init__.py index 5527f5f2abe..bedfa703a9b 100644 --- a/homeassistant/components/onboarding/__init__.py +++ b/homeassistant/components/onboarding/__init__.py @@ -1,9 +1,10 @@ """Support to help onboard new users.""" from homeassistant.core import callback -from homeassistant.loader import bind_hass from homeassistant.helpers.storage import Store +from homeassistant.loader import bind_hass -from .const import DOMAIN, STEP_USER, STEPS, STEP_INTEGRATION, STEP_CORE_CONFIG +from . import views +from .const import DOMAIN, STEP_CORE_CONFIG, STEP_INTEGRATION, STEP_USER, STEPS STORAGE_KEY = DOMAIN STORAGE_VERSION = 3 @@ -64,8 +65,6 @@ async def async_setup(hass, config): hass.data[DOMAIN] = data - from . import views - await views.async_setup(hass, data, store) return True diff --git a/homeassistant/components/onboarding/manifest.json b/homeassistant/components/onboarding/manifest.json index 8e525ff0baa..203918b9816 100644 --- a/homeassistant/components/onboarding/manifest.json +++ b/homeassistant/components/onboarding/manifest.json @@ -1,8 +1,9 @@ { "domain": "onboarding", - "name": "Onboarding", + "name": "Home Assistant Onboarding", "documentation": "https://www.home-assistant.io/integrations/onboarding", "requirements": [], "dependencies": ["auth", "http", "person"], - "codeowners": ["@home-assistant/core"] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 2e79393fe42..8eac430ac49 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -8,12 +8,12 @@ from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback from .const import ( + DEFAULT_AREAS, DOMAIN, + STEP_CORE_CONFIG, + STEP_INTEGRATION, STEP_USER, STEPS, - DEFAULT_AREAS, - STEP_INTEGRATION, - STEP_CORE_CONFIG, ) diff --git a/homeassistant/components/onewire/manifest.json b/homeassistant/components/onewire/manifest.json index 2d8c6c71071..13000caa8b1 100644 --- a/homeassistant/components/onewire/manifest.json +++ b/homeassistant/components/onewire/manifest.json @@ -1,8 +1,8 @@ { "domain": "onewire", - "name": "Onewire", + "name": "1-Wire", "documentation": "https://www.home-assistant.io/integrations/onewire", - "requirements": [], + "requirements": ["pyownet==0.10.0.post1"], "dependencies": [], - "codeowners": [] + "codeowners": ["@garbled1"] } diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index e0a47a45b25..936bf9f751b 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -1,15 +1,16 @@ """Support for 1-Wire environment sensors.""" +from glob import glob +import logging import os import time -import logging -from glob import glob +from pyownet import protocol import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_PORT, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.const import TEMP_CELSIUS -from homeassistant.components.sensor import PLATFORM_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -30,35 +31,124 @@ DEVICE_SENSORS = { "28": {"temperature": "temperature"}, "3B": {"temperature": "temperature"}, "42": {"temperature": "temperature"}, + "1D": {"counter_a": "counter.A", "counter_b": "counter.B"}, + "EF": {"HobbyBoard": "special"}, +} + +# EF sensors are usually hobbyboards specialized sensors. +# These can only be read by OWFS. Currently this driver only supports them +# via owserver (network protocol) + +HOBBYBOARD_EF = { + "HobbyBoards_EF": { + "humidity": "humidity/humidity_corrected", + "humidity_raw": "humidity/humidity_raw", + "temperature": "humidity/temperature", + }, + "HB_MOISTURE_METER": { + "moisture_0": "moisture/sensor.0", + "moisture_1": "moisture/sensor.1", + "moisture_2": "moisture/sensor.2", + "moisture_3": "moisture/sensor.3", + }, } SENSOR_TYPES = { "temperature": ["temperature", TEMP_CELSIUS], "humidity": ["humidity", "%"], + "humidity_raw": ["humidity", "%"], "pressure": ["pressure", "mb"], "illuminance": ["illuminance", "lux"], + "wetness_0": ["wetness", "%"], + "wetness_1": ["wetness", "%"], + "wetness_2": ["wetness", "%"], + "wetness_3": ["wetness", "%"], + "moisture_0": ["moisture", "cb"], + "moisture_1": ["moisture", "cb"], + "moisture_2": ["moisture", "cb"], + "moisture_3": ["moisture", "cb"], + "counter_a": ["counter", "count"], + "counter_b": ["counter", "count"], + "HobbyBoard": ["none", "none"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAMES): {cv.string: cv.string}, vol.Optional(CONF_MOUNT_DIR, default=DEFAULT_MOUNT_DIR): cv.string, + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=4304): cv.port, } ) +def hb_info_from_type(dev_type="std"): + """Return the proper info array for the device type.""" + if "std" in dev_type: + return DEVICE_SENSORS + if "HobbyBoard" in dev_type: + return HOBBYBOARD_EF + + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the one wire Sensors.""" - base_dir = config.get(CONF_MOUNT_DIR) + base_dir = config[CONF_MOUNT_DIR] + owport = config[CONF_PORT] + owhost = config.get(CONF_HOST) devs = [] device_names = {} if "names" in config: if isinstance(config["names"], dict): device_names = config["names"] - if base_dir == DEFAULT_MOUNT_DIR: + # We have an owserver on a remote(or local) host/port + if owhost: + try: + owproxy = protocol.proxy(host=owhost, port=owport) + devices = owproxy.dir() + except protocol.Error as exc: + _LOGGER.error( + "Cannot connect to owserver on %s:%d, got: %s", owhost, owport, exc + ) + devices = [] + for device in devices: + _LOGGER.debug("found device: %s", device) + family = owproxy.read(f"{device}family").decode() + dev_type = "std" + if "EF" in family: + dev_type = "HobbyBoard" + family = owproxy.read(f"{device}type").decode() + + if family not in hb_info_from_type(dev_type): + _LOGGER.warning( + "Ignoring unknown family (%s) of sensor found for device: %s", + family, + device, + ) + continue + for sensor_key, sensor_value in hb_info_from_type(dev_type)[family].items(): + if "moisture" in sensor_key: + s_id = sensor_key.split("_")[1] + is_leaf = int( + owproxy.read(f"{device}moisture/is_leaf.{s_id}").decode() + ) + if is_leaf: + sensor_key = f"wetness_{id}" + sensor_id = os.path.split(os.path.split(device)[0])[1] + device_file = os.path.join(os.path.split(device)[0], sensor_value) + devs.append( + OneWireProxy( + device_names.get(sensor_id, sensor_id), + device_file, + sensor_key, + owproxy, + ) + ) + + # We have a raw GPIO ow sensor on a Pi + elif base_dir == DEFAULT_MOUNT_DIR: for device_family in DEVICE_SENSORS: - for device_folder in glob(os.path.join(base_dir, device_family + "[.-]*")): + for device_folder in glob(os.path.join(base_dir, f"{device_family}[.-]*")): sensor_id = os.path.split(device_folder)[1] device_file = os.path.join(device_folder, "w1_slave") devs.append( @@ -68,10 +158,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "temperature", ) ) + + # We have an owfs mounted else: for family_file_path in glob(os.path.join(base_dir, "*", "family")): with open(family_file_path, "r") as family_file: family = family_file.read() + if "EF" in family: + continue if family in DEVICE_SENSORS: for sensor_key, sensor_value in DEVICE_SENSORS[family].items(): sensor_id = os.path.split(os.path.split(family_file_path)[0])[1] @@ -121,6 +215,8 @@ class OneWire(Entity): @property def state(self): """Return the state of the sensor.""" + if "count" in self._unit_of_measurement: + return int(self._state) return self._state @property @@ -129,6 +225,34 @@ class OneWire(Entity): return self._unit_of_measurement +class OneWireProxy(OneWire): + """Implementation of a One wire Sensor through owserver.""" + + def __init__(self, name, device_file, sensor_type, owproxy): + """Initialize the onewire sensor via owserver.""" + super().__init__(name, device_file, sensor_type) + self._owproxy = owproxy + + def _read_value_ownet(self): + """Read a value from the owserver.""" + if self._owproxy: + return self._owproxy.read(self._device_file).decode().lstrip() + return None + + def update(self): + """Get the latest data from the device.""" + value = None + value_read = False + try: + value_read = self._read_value_ownet() + except protocol.Error as exc: + _LOGGER.error("Owserver failure in read(), got: %s", exc) + if value_read: + value = round(float(value_read), 1) + + self._state = value + + class OneWireDirect(OneWire): """Implementation of an One wire Sensor directly connected to RPI GPIO.""" diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index ef28c14a8b0..857fc11f15f 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -2,9 +2,7 @@ "domain": "onkyo", "name": "Onkyo", "documentation": "https://www.home-assistant.io/integrations/onkyo", - "requirements": [ - "onkyo-eiscp==1.2.7" - ], + "requirements": ["onkyo-eiscp==1.2.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 86f0f418c3f..93107b2eb48 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -2,12 +2,13 @@ import logging from typing import List -import voluptuous as vol import eiscp from eiscp import eISCP +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + DOMAIN, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_SELECT_SOURCE, @@ -16,14 +17,13 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - DOMAIN, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON, - ATTR_ENTITY_ID, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/onvif/manifest.json b/homeassistant/components/onvif/manifest.json index f6c23712188..a927fd9072b 100644 --- a/homeassistant/components/onvif/manifest.json +++ b/homeassistant/components/onvif/manifest.json @@ -1,12 +1,8 @@ { "domain": "onvif", - "name": "Onvif", + "name": "ONVIF", "documentation": "https://www.home-assistant.io/integrations/onvif", - "requirements": [ - "onvif-zeep-async==0.2.0" - ], - "dependencies": [ - "ffmpeg" - ], + "requirements": ["onvif-zeep-async==0.2.0"], + "dependencies": ["ffmpeg"], "codeowners": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 66081d9b271..64ba0d83844 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -1,26 +1,26 @@ """Component that will help set the OpenALPR cloud for ALPR processing.""" import asyncio -import logging from base64 import b64encode +import logging import aiohttp import async_timeout import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id -from homeassistant.const import CONF_API_KEY from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, CONF_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, ) from homeassistant.components.openalpr_local.image_processing import ( ImageProcessingAlprEntity, ) +from homeassistant.const import CONF_API_KEY +from homeassistant.core import split_entity_id from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/openalpr_cloud/manifest.json b/homeassistant/components/openalpr_cloud/manifest.json index 56128f0e886..00662f31ed8 100644 --- a/homeassistant/components/openalpr_cloud/manifest.json +++ b/homeassistant/components/openalpr_cloud/manifest.json @@ -1,6 +1,6 @@ { "domain": "openalpr_cloud", - "name": "Openalpr cloud", + "name": "OpenALPR Cloud", "documentation": "https://www.home-assistant.io/integrations/openalpr_cloud", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/openalpr_local/image_processing.py b/homeassistant/components/openalpr_local/image_processing.py index 32a08b53165..df7b235a224 100644 --- a/homeassistant/components/openalpr_local/image_processing.py +++ b/homeassistant/components/openalpr_local/image_processing.py @@ -6,19 +6,19 @@ import re import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id, callback -from homeassistant.const import CONF_REGION from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingEntity, + ATTR_CONFIDENCE, + ATTR_ENTITY_ID, CONF_CONFIDENCE, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, - ATTR_ENTITY_ID, - ATTR_CONFIDENCE, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, ) +from homeassistant.const import CONF_REGION +from homeassistant.core import callback, split_entity_id +import homeassistant.helpers.config_validation as cv from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/openalpr_local/manifest.json b/homeassistant/components/openalpr_local/manifest.json index 65fa6b1bec6..c5521b056ce 100644 --- a/homeassistant/components/openalpr_local/manifest.json +++ b/homeassistant/components/openalpr_local/manifest.json @@ -1,6 +1,6 @@ { "domain": "openalpr_local", - "name": "Openalpr local", + "name": "OpenALPR Local", "documentation": "https://www.home-assistant.io/integrations/openalpr_local", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 4fe6026dfef..3d13db3ead3 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -1,6 +1,6 @@ { "domain": "opencv", - "name": "Opencv", + "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", "requirements": ["numpy==1.17.4", "opencv-python-headless==4.1.2.30"], "dependencies": [], diff --git a/homeassistant/components/openevse/manifest.json b/homeassistant/components/openevse/manifest.json index a56f0caddab..daa5d283b2b 100644 --- a/homeassistant/components/openevse/manifest.json +++ b/homeassistant/components/openevse/manifest.json @@ -1,10 +1,8 @@ { "domain": "openevse", - "name": "Openevse", + "name": "OpenEVSE", "documentation": "https://www.home-assistant.io/integrations/openevse", - "requirements": [ - "openevsewifi==0.4" - ], + "requirements": ["openevsewifi==0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/openexchangerates/manifest.json b/homeassistant/components/openexchangerates/manifest.json index fae9e616e5e..d707ded5188 100644 --- a/homeassistant/components/openexchangerates/manifest.json +++ b/homeassistant/components/openexchangerates/manifest.json @@ -1,6 +1,6 @@ { "domain": "openexchangerates", - "name": "Openexchangerates", + "name": "Open Exchange Rates", "documentation": "https://www.home-assistant.io/integrations/openexchangerates", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/openexchangerates/sensor.py b/homeassistant/components/openexchangerates/sensor.py index 9b79eb564e0..cc6da709dff 100644 --- a/homeassistant/components/openexchangerates/sensor.py +++ b/homeassistant/components/openexchangerates/sensor.py @@ -7,11 +7,11 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_BASE, - CONF_QUOTE, ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_BASE, + CONF_NAME, + CONF_QUOTE, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/opengarage/cover.py b/homeassistant/components/opengarage/cover.py index 6b5cbc912e6..26a69fa11af 100644 --- a/homeassistant/components/opengarage/cover.py +++ b/homeassistant/components/opengarage/cover.py @@ -5,22 +5,22 @@ import requests import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, DEVICE_CLASS_GARAGE, PLATFORM_SCHEMA, - SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( - CONF_NAME, - STATE_CLOSED, - STATE_OPEN, CONF_COVERS, CONF_HOST, + CONF_NAME, CONF_PORT, CONF_SSL, CONF_VERIFY_SSL, + STATE_CLOSED, STATE_CLOSING, + STATE_OPEN, STATE_OPENING, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/opengarage/manifest.json b/homeassistant/components/opengarage/manifest.json index 4dcb53e98ce..87b0e65dcd5 100644 --- a/homeassistant/components/opengarage/manifest.json +++ b/homeassistant/components/opengarage/manifest.json @@ -1,6 +1,6 @@ { "domain": "opengarage", - "name": "Opengarage", + "name": "OpenGarage", "documentation": "https://www.home-assistant.io/integrations/opengarage", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/openhardwaremonitor/manifest.json b/homeassistant/components/openhardwaremonitor/manifest.json index 4a17f933515..069cac06f3c 100644 --- a/homeassistant/components/openhardwaremonitor/manifest.json +++ b/homeassistant/components/openhardwaremonitor/manifest.json @@ -1,6 +1,6 @@ { "domain": "openhardwaremonitor", - "name": "Openhardwaremonitor", + "name": "Open Hardware Monitor", "documentation": "https://www.home-assistant.io/integrations/openhardwaremonitor", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/openhome/manifest.json b/homeassistant/components/openhome/manifest.json index 7640c388747..ed10387bda1 100644 --- a/homeassistant/components/openhome/manifest.json +++ b/homeassistant/components/openhome/manifest.json @@ -1,6 +1,6 @@ { "domain": "openhome", - "name": "Openhome", + "name": "Linn / OpenHome", "documentation": "https://www.home-assistant.io/integrations/openhome", "requirements": ["openhomedevice==0.6.3"], "dependencies": [], diff --git a/homeassistant/components/opensensemap/manifest.json b/homeassistant/components/opensensemap/manifest.json index 632ab82918e..ca783bbc465 100644 --- a/homeassistant/components/opensensemap/manifest.json +++ b/homeassistant/components/opensensemap/manifest.json @@ -1,10 +1,8 @@ { "domain": "opensensemap", - "name": "Opensensemap", + "name": "openSenseMap", "documentation": "https://www.home-assistant.io/integrations/opensensemap", - "requirements": [ - "opensensemap-api==0.1.5" - ], + "requirements": ["opensensemap-api==0.1.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/opensky/manifest.json b/homeassistant/components/opensky/manifest.json index a98e828a52e..99ffd81a04e 100644 --- a/homeassistant/components/opensky/manifest.json +++ b/homeassistant/components/opensky/manifest.json @@ -1,6 +1,6 @@ { "domain": "opensky", - "name": "Opensky", + "name": "OpenSky Network", "documentation": "https://www.home-assistant.io/integrations/opensky", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/opensky/sensor.py b/homeassistant/components/opensky/sensor.py index 0c17daa0ab4..d916d9f7f29 100644 --- a/homeassistant/components/opensky/sensor.py +++ b/homeassistant/components/opensky/sensor.py @@ -1,26 +1,25 @@ """Sensor for the Open Sky Network.""" -import logging from datetime import timedelta +import logging import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, - CONF_RADIUS, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_RADIUS, LENGTH_KILOMETERS, LENGTH_METERS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import distance as util_distance -from homeassistant.util import location as util_location +from homeassistant.util import distance as util_distance, location as util_location _LOGGER = logging.getLogger(__name__) @@ -41,7 +40,7 @@ EVENT_OPENSKY_EXIT = f"{DOMAIN}_exit" SCAN_INTERVAL = timedelta(seconds=12) # opensky public limit is 10 seconds OPENSKY_ATTRIBUTION = ( - "Information provided by the OpenSky Network " "(https://opensky-network.org)" + "Information provided by the OpenSky Network (https://opensky-network.org)" ) OPENSKY_API_URL = "https://opensky-network.org/api/states/all" OPENSKY_API_FIELDS = [ diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json index 152e38a5bba..743adb715f6 100644 --- a/homeassistant/components/opentherm_gw/.translations/da.json +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -3,14 +3,17 @@ "error": { "already_configured": "Gateway allerede konfigureret", "id_exists": "Gateway-id findes allerede", - "serial_error": "Fejl ved tilslutning til enheden" + "serial_error": "Fejl ved tilslutning til enheden", + "timeout": "Forbindelsesfors\u00f8g fik timeout" }, "step": { "init": { "data": { - "device": "Sti eller URL", - "id": "ID", - "name": "Navn" + "device": "Sti eller webadresse", + "floor_temperature": "Gulvklima-temperatur", + "id": "Id", + "name": "Navn", + "precision": "Klimatemperatur-pr\u00e6cision" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json index 3b18aa71b6c..c29be320d20 100644 --- a/homeassistant/components/opentherm_gw/.translations/de.json +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -26,7 +26,8 @@ "data": { "floor_temperature": "Boden-Temperatur", "precision": "Genauigkeit" - } + }, + "description": "Optionen f\u00fcr das OpenTherm Gateway" } } } diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json index 426459237aa..bba6421ed3d 100644 --- a/homeassistant/components/opentherm_gw/.translations/sl.json +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -13,7 +13,7 @@ "floor_temperature": "Temperatura nadstropja", "id": "ID", "name": "Ime", - "precision": "Natan\u010dnost temperature " + "precision": "Natan\u010dnost temperature" }, "title": "OpenTherm Prehod" } diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 3a1255e3697..c6cf14bfdce 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -1,16 +1,16 @@ """Support for OpenTherm Gateway devices.""" import asyncio +from datetime import date, datetime import logging -from datetime import datetime, date import pyotgw import pyotgw.vars as gw_vars import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.climate import DOMAIN as COMP_CLIMATE from homeassistant.components.sensor import DOMAIN as COMP_SENSOR +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_DATE, ATTR_ID, @@ -25,14 +25,13 @@ from homeassistant.const import ( PRECISION_TENTHS, PRECISION_WHOLE, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -import homeassistant.helpers.config_validation as cv - from .const import ( + ATTR_DHW_OVRD, ATTR_GW_ID, ATTR_LEVEL, - ATTR_DHW_OVRD, CONF_CLIMATE, CONF_FLOOR_TEMP, CONF_PRECISION, @@ -42,15 +41,14 @@ from .const import ( SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, - SERVICE_SET_HOT_WATER_OVRD, SERVICE_SET_GPIO_MODE, + SERVICE_SET_HOT_WATER_OVRD, SERVICE_SET_LED_MODE, SERVICE_SET_MAX_MOD, SERVICE_SET_OAT, SERVICE_SET_SB_TEMP, ) - _LOGGER = logging.getLogger(__name__) CLIMATE_SCHEMA = vol.Schema( diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 39fd78f5fe8..eff11554a39 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -10,7 +10,6 @@ from homeassistant.helpers.entity import async_generate_entity_id from . import DOMAIN from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 8c21c6560c1..2db20662a77 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -3,17 +3,17 @@ import logging from pyotgw import vars as gw_vars -from homeassistant.components.climate import ClimateDevice, ENTITY_ID_FORMAT +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_COOL, HVAC_MODE_HEAT, - SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, PRESET_NONE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -30,7 +30,6 @@ from homeassistant.helpers.entity import async_generate_entity_id from . import DOMAIN from .const import CONF_FLOOR_TEMP, CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW - _LOGGER = logging.getLogger(__name__) DEFAULT_FLOOR_TEMP = False diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index 2d7a65bbd84..b52641105e4 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -1,8 +1,8 @@ """OpenTherm Gateway config flow.""" import asyncio -from serial import SerialException import pyotgw +from serial import SerialException import voluptuous as vol from homeassistant import config_entries @@ -15,7 +15,6 @@ from homeassistant.const import ( PRECISION_WHOLE, ) from homeassistant.core import callback - import homeassistant.helpers.config_validation as cv from . import DOMAIN diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 990df0ba4e3..81a41b8fb47 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -1,13 +1,9 @@ { "domain": "opentherm_gw", - "name": "Opentherm Gateway", + "name": "OpenTherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", - "requirements": [ - "pyotgw==0.5b1" - ], + "requirements": ["pyotgw==0.5b1"], "dependencies": [], - "codeowners": [ - "@mvn23" - ], + "codeowners": ["@mvn23"], "config_flow": true -} \ No newline at end of file +} diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index cd9ce9fb095..3739f77e69d 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -10,7 +10,6 @@ from homeassistant.helpers.entity import Entity, async_generate_entity_id from . import DOMAIN from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/openuv/.translations/da.json b/homeassistant/components/openuv/.translations/da.json index a783c8646e0..eaf2e127026 100644 --- a/homeassistant/components/openuv/.translations/da.json +++ b/homeassistant/components/openuv/.translations/da.json @@ -2,12 +2,12 @@ "config": { "error": { "identifier_exists": "Koordinater er allerede registreret", - "invalid_api_key": "Ugyldig API n\u00f8gle" + "invalid_api_key": "Ugyldig API-n\u00f8gle" }, "step": { "user": { "data": { - "api_key": "OpenUV API N\u00f8gle", + "api_key": "OpenUV API-n\u00f8gle", "elevation": "Elevation", "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad" diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index 16b7a50a4ae..167fcdcd0e6 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -1,7 +1,9 @@ """Support for UV data from openuv.io.""" -import logging import asyncio +import logging +from pyopenuv import Client +from pyopenuv.errors import OpenUvError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -164,8 +166,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up OpenUV as config entry.""" - from pyopenuv import Client - from pyopenuv.errors import OpenUvError _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -255,7 +255,6 @@ class OpenUV: async def async_update_protection_data(self): """Update binary sensor (protection window) data.""" - from pyopenuv.errors import OpenUvError if TYPE_PROTECTION_WINDOW in self.binary_sensor_conditions: try: @@ -268,7 +267,6 @@ class OpenUV: 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): try: diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 621950965f6..1e765abbbce 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -75,7 +75,7 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): @property def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return f"{self._latitude}_{self._longitude}_{self._sensor_type}" async def async_added_to_hass(self): diff --git a/homeassistant/components/openuv/config_flow.py b/homeassistant/components/openuv/config_flow.py index 40ec2abf2fe..7dd8ed45a79 100644 --- a/homeassistant/components/openuv/config_flow.py +++ b/homeassistant/components/openuv/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure the OpenUV component.""" - +from pyopenuv import Client +from pyopenuv.errors import OpenUvError import voluptuous as vol from homeassistant import config_entries @@ -59,8 +60,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from pyopenuv import Client - from pyopenuv.errors import OpenUvError if not user_input: return await self._show_form() diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 69342df235f..2366542167d 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -3,11 +3,7 @@ "name": "Openuv", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", - "requirements": [ - "pyopenuv==1.0.9" - ], + "requirements": ["pyopenuv==1.0.9"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index de2688ab121..a482464e4d0 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -97,7 +97,7 @@ class OpenUvSensor(OpenUvEntity): @property def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return f"{self._latitude}_{self._longitude}_{self._sensor_type}" @property diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index c28d27c1b8b..4424b53cf0d 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -2,11 +2,7 @@ "domain": "openweathermap", "name": "Openweathermap", "documentation": "https://www.home-assistant.io/integrations/openweathermap", - "requirements": [ - "pyowm==2.10.0" - ], + "requirements": ["pyowm==2.10.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 69ca965d660..ce8676ad440 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -272,7 +272,7 @@ class WeatherData: self.latitude, self.longitude ) except APICallError: - _LOGGER.error("Exception when calling OWM web API " "to update forecast") + _LOGGER.error("Exception when calling OWM web API to update forecast") return if fcd is None: diff --git a/homeassistant/components/opple/manifest.json b/homeassistant/components/opple/manifest.json index cee6447abc1..331db0eeaef 100644 --- a/homeassistant/components/opple/manifest.json +++ b/homeassistant/components/opple/manifest.json @@ -2,9 +2,7 @@ "domain": "opple", "name": "Opple", "documentation": "https://www.home-assistant.io/integrations/opple", - "requirements": [ - "pyoppleio==1.0.5" - ], + "requirements": ["pyoppleio==1.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py index 71d8d65d8b8..f8d5ff1be0b 100644 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -21,7 +21,7 @@ async def async_setup(hass, config): GPIO.cleanup() def prepare_gpio(event): - """Stuff to do when home assistant starts.""" + """Stuff to do when Home Assistant starts.""" hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 52c8f8f509f..19f805f1132 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -2,11 +2,7 @@ "domain": "orangepi_gpio", "name": "Orangepi GPIO", "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", - "requirements": [ - "OPi.GPIO==0.4.0" - ], + "requirements": ["OPi.GPIO==0.4.0"], "dependencies": [], - "codeowners": [ - "@pascallj" - ] + "codeowners": ["@pascallj"] } diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json index ff5e74fd260..5ed1d12e730 100644 --- a/homeassistant/components/oru/manifest.json +++ b/homeassistant/components/oru/manifest.json @@ -1,8 +1,8 @@ { - "domain": "oru", - "name": "Orange and Rockland Utility Smart Energy Meter Sensor", - "documentation": "https://www.home-assistant.io/integrations/oru", - "dependencies": [], - "codeowners": ["@bvlaicu"], - "requirements": ["oru==0.1.9"] -} \ No newline at end of file + "domain": "oru", + "name": "Orange and Rockland Utility (ORU)", + "documentation": "https://www.home-assistant.io/integrations/oru", + "dependencies": [], + "codeowners": ["@bvlaicu"], + "requirements": ["oru==0.1.9"] +} diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py index e68d8e1c45a..d6620ed39e5 100644 --- a/homeassistant/components/oru/sensor.py +++ b/homeassistant/components/oru/sensor.py @@ -2,14 +2,12 @@ from datetime import timedelta import logging +from oru import Meter, MeterError import voluptuous as vol -from oru import Meter -from oru import MeterError - from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import ENERGY_KILO_WATT_HOUR +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -52,7 +50,7 @@ class CurrentEnergyUsageSensor(Entity): @property def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return self.meter.meter_id @property diff --git a/homeassistant/components/orvibo/manifest.json b/homeassistant/components/orvibo/manifest.json index 9dee62697c3..0c4a9f2d820 100644 --- a/homeassistant/components/orvibo/manifest.json +++ b/homeassistant/components/orvibo/manifest.json @@ -2,9 +2,7 @@ "domain": "orvibo", "name": "Orvibo", "documentation": "https://www.home-assistant.io/integrations/orvibo", - "requirements": [ - "orvibo==1.1.1" - ], + "requirements": ["orvibo==1.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index 8c6c9f30b10..87a81e74bbb 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -2,9 +2,7 @@ "domain": "osramlightify", "name": "Osramlightify", "documentation": "https://www.home-assistant.io/integrations/osramlightify", - "requirements": [ - "lightify==1.0.7.2" - ], + "requirements": ["lightify==1.0.7.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/otp/manifest.json b/homeassistant/components/otp/manifest.json index 25ece71c11b..2230ea8a478 100644 --- a/homeassistant/components/otp/manifest.json +++ b/homeassistant/components/otp/manifest.json @@ -1,10 +1,9 @@ { "domain": "otp", - "name": "Otp", + "name": "One-Time Password (OTP)", "documentation": "https://www.home-assistant.io/integrations/otp", - "requirements": [ - "pyotp==2.3.0" - ], + "requirements": ["pyotp==2.3.0"], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/owlet/__init__.py b/homeassistant/components/owlet/__init__.py index afde50cae49..3882ba4bf7d 100644 --- a/homeassistant/components/owlet/__init__.py +++ b/homeassistant/components/owlet/__init__.py @@ -51,7 +51,7 @@ def setup(hass, config): device = PyOwlet(username, password) except KeyError: _LOGGER.error( - "Owlet authentication failed. Please verify your " "credentials are correct" + "Owlet authentication failed. Please verify your credentials are correct" ) return False diff --git a/homeassistant/components/owlet/manifest.json b/homeassistant/components/owlet/manifest.json index 2ba00671b48..632115a93cb 100644 --- a/homeassistant/components/owlet/manifest.json +++ b/homeassistant/components/owlet/manifest.json @@ -2,11 +2,7 @@ "domain": "owlet", "name": "Owlet", "documentation": "https://www.home-assistant.io/integrations/owlet", - "requirements": [ - "pyowlet==1.0.3" - ], + "requirements": ["pyowlet==1.0.3"], "dependencies": [], - "codeowners": [ - "@oblogic7" - ] + "codeowners": ["@oblogic7"] } diff --git a/homeassistant/components/owntracks/.translations/da.json b/homeassistant/components/owntracks/.translations/da.json index bc1328d57e4..110f60193e6 100644 --- a/homeassistant/components/owntracks/.translations/da.json +++ b/homeassistant/components/owntracks/.translations/da.json @@ -4,7 +4,7 @@ "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning" }, "create_entry": { - "default": "\n\nP\u00e5 Android skal du \u00e5bne [OwnTracks applikationen]({android_url}), g\u00e5 til indstillinger -> forbindelse. Skift f\u00f8lgende indstillinger: \n - Tilstand: Privat HTTP\n - V\u00e6rt: {webhook_url}\n - Identifikation:\n - Brugernavn: ` ` \n - Enheds-id: ` ` \n\nP\u00e5 iOS skal du \u00e5bne [OwnTracks applikationen]({ios_url}), tryk p\u00e5 (i) ikonet \u00f8verst til venstre -> indstillinger. Skift f\u00f8lgende indstillinger: \n - Tilstand: HTTP\n - URL: {webhook_url}\n - Aktiver godkendelse \n - Bruger ID: ` ` \n\n {secret}\n \n Se [dokumentationen]({docs_url}) for at f\u00e5 flere oplysninger." + "default": "\n\nP\u00e5 Android skal du \u00e5bne [OwnTracks-appen]({android_url}), g\u00e5 til indstillinger -> forbindelse. Skift f\u00f8lgende indstillinger: \n - Tilstand: Privat HTTP\n - V\u00e6rt: {webhook_url}\n - Identifikation:\n - Brugernavn: ` ` \n - Enheds-id: ` ` \n\nP\u00e5 iOS skal du \u00e5bne [OwnTracks-appen]({ios_url}), tryk p\u00e5 (i) ikonet \u00f8verst til venstre -> indstillinger. Skift f\u00f8lgende indstillinger: \n - Tilstand: HTTP\n - URL: {webhook_url}\n - Aktiver godkendelse \n - Bruger ID: ` ` \n\n {secret}\n \n Se [dokumentationen]({docs_url}) for at f\u00e5 flere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/.translations/ko.json b/homeassistant/components/owntracks/.translations/ko.json index d70ca8b114e..ee1507d9e0a 100644 --- a/homeassistant/components/owntracks/.translations/ko.json +++ b/homeassistant/components/owntracks/.translations/ko.json @@ -8,7 +8,7 @@ }, "step": { "user": { - "description": "OwnTracks \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "OwnTracks \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "OwnTracks \uc124\uc815" } }, diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 8556e8a7556..71494e9e805 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -14,8 +14,8 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup -from .const import DOMAIN from .config_flow import CONF_SECRET +from .const import DOMAIN from .messages import async_handle_message _LOGGER = logging.getLogger(__name__) @@ -233,7 +233,7 @@ class OwnTracksContext: if self.max_gps_accuracy is not None and acc > self.max_gps_accuracy: _LOGGER.info( - "Ignoring %s update because expected GPS " "accuracy %s is not met: %s", + "Ignoring %s update because expected GPS accuracy %s is not met: %s", message["_type"], self.max_gps_accuracy, message, diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 1a8bb838e18..0aba24217cc 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -1,7 +1,8 @@ """Config flow for OwnTracks.""" +import secrets + from homeassistant import config_entries from homeassistant.const import CONF_WEBHOOK_ID -from homeassistant.auth.util import generate_secret from .const import DOMAIN # noqa pylint: disable=unused-import from .helper import supports_encryption @@ -25,7 +26,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): webhook_id, webhook_url, cloudhook = await self._get_webhook_id() - secret = generate_secret(16) + secret = secrets.token_hex(16) if supports_encryption(): secret_desc = f"The encryption key is {secret} (on Android under preferences -> advanced)" @@ -53,7 +54,7 @@ class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="one_instance_allowed") webhook_id, _webhook_url, cloudhook = await self._get_webhook_id() - secret = generate_secret(16) + secret = secrets.token_hex(16) return self.async_create_entry( title="OwnTracks", data={ diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py index 6d3254eda99..00fa023d6c1 100644 --- a/homeassistant/components/owntracks/device_tracker.py +++ b/homeassistant/components/owntracks/device_tracker.py @@ -1,21 +1,22 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging -from homeassistant.core import callback +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import ( + ATTR_SOURCE_TYPE, + ENTITY_ID_FORMAT, + SOURCE_TYPE_GPS, +) from homeassistant.const import ( + ATTR_BATTERY_LEVEL, ATTR_GPS_ACCURACY, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_BATTERY_LEVEL, ) -from homeassistant.components.device_tracker.const import ( - ENTITY_ID_FORMAT, - ATTR_SOURCE_TYPE, - SOURCE_TYPE_GPS, -) -from homeassistant.components.device_tracker.config_entry import TrackerEntity -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.core import callback from homeassistant.helpers import device_registry +from homeassistant.helpers.restore_state import RestoreEntity + from . import DOMAIN as OT_DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/owntracks/manifest.json b/homeassistant/components/owntracks/manifest.json index 63fdfb94cf7..0fcca8953c7 100644 --- a/homeassistant/components/owntracks/manifest.json +++ b/homeassistant/components/owntracks/manifest.json @@ -1,6 +1,6 @@ { "domain": "owntracks", - "name": "Owntracks", + "name": "OwnTracks", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/owntracks", "requirements": ["PyNaCl==1.3.0"], diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 7c388f9eb17..d357843c42e 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -2,15 +2,14 @@ import json import logging -from nacl.secret import SecretBox from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox from homeassistant.components import zone as zone_comp from homeassistant.components.device_tracker import ( - SOURCE_TYPE_GPS, SOURCE_TYPE_BLUETOOTH_LE, + SOURCE_TYPE_GPS, ) - from homeassistant.const import STATE_HOME from homeassistant.util import decorator, slugify diff --git a/homeassistant/components/panasonic_bluray/manifest.json b/homeassistant/components/panasonic_bluray/manifest.json index 7f386464dc9..03fb171fc6f 100644 --- a/homeassistant/components/panasonic_bluray/manifest.json +++ b/homeassistant/components/panasonic_bluray/manifest.json @@ -1,10 +1,8 @@ { "domain": "panasonic_bluray", - "name": "Panasonic bluray", + "name": "Panasonic Blu-Ray Player", "documentation": "https://www.home-assistant.io/integrations/panasonic_bluray", - "requirements": [ - "panacotta==0.1" - ], + "requirements": ["panacotta==0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/panasonic_viera/manifest.json b/homeassistant/components/panasonic_viera/manifest.json index e7e06479eec..5438ae1a2c5 100644 --- a/homeassistant/components/panasonic_viera/manifest.json +++ b/homeassistant/components/panasonic_viera/manifest.json @@ -1,11 +1,8 @@ { "domain": "panasonic_viera", - "name": "Panasonic viera", + "name": "Panasonic Viera TV", "documentation": "https://www.home-assistant.io/integrations/panasonic_viera", - "requirements": [ - "panasonic_viera==0.3.2", - "wakeonlan==1.1.6" - ], + "requirements": ["panasonic_viera==0.3.2", "wakeonlan==1.1.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pandora/manifest.json b/homeassistant/components/pandora/manifest.json index a15267b7d85..f2d1cfbc23b 100644 --- a/homeassistant/components/pandora/manifest.json +++ b/homeassistant/components/pandora/manifest.json @@ -2,9 +2,7 @@ "domain": "pandora", "name": "Pandora", "documentation": "https://www.home-assistant.io/integrations/pandora", - "requirements": [ - "pexpect==4.6.0" - ], + "requirements": ["pexpect==4.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/panel_custom/manifest.json b/homeassistant/components/panel_custom/manifest.json index 5d0cc555705..3aa0f1e3b29 100644 --- a/homeassistant/components/panel_custom/manifest.json +++ b/homeassistant/components/panel_custom/manifest.json @@ -1,12 +1,9 @@ { "domain": "panel_custom", - "name": "Panel custom", + "name": "Custom Panel", "documentation": "https://www.home-assistant.io/integrations/panel_custom", "requirements": [], - "dependencies": [ - "frontend" - ], - "codeowners": [ - "@home-assistant/frontend" - ] + "dependencies": ["frontend"], + "codeowners": ["@home-assistant/frontend"], + "quality_scale": "internal" } diff --git a/homeassistant/components/panel_iframe/manifest.json b/homeassistant/components/panel_iframe/manifest.json index 6d7c66f6097..6ccd444db26 100644 --- a/homeassistant/components/panel_iframe/manifest.json +++ b/homeassistant/components/panel_iframe/manifest.json @@ -1,12 +1,9 @@ { "domain": "panel_iframe", - "name": "Panel iframe", + "name": "iframe Panel", "documentation": "https://www.home-assistant.io/integrations/panel_iframe", "requirements": [], - "dependencies": [ - "frontend" - ], - "codeowners": [ - "@home-assistant/frontend" - ] + "dependencies": ["frontend"], + "codeowners": ["@home-assistant/frontend"], + "quality_scale": "internal" } diff --git a/homeassistant/components/pcal9535a/binary_sensor.py b/homeassistant/components/pcal9535a/binary_sensor.py index fd4e92ccf03..236fd47af73 100644 --- a/homeassistant/components/pcal9535a/binary_sensor.py +++ b/homeassistant/components/pcal9535a/binary_sensor.py @@ -1,10 +1,10 @@ """Support for binary sensor using I2C PCAL9535A chip.""" import logging -import voluptuous as vol from pcal9535a import PCAL9535A +import voluptuous as vol -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/pcal9535a/manifest.json b/homeassistant/components/pcal9535a/manifest.json index e2fb140c7a9..548acbc3c1e 100644 --- a/homeassistant/components/pcal9535a/manifest.json +++ b/homeassistant/components/pcal9535a/manifest.json @@ -2,9 +2,7 @@ "domain": "pcal9535a", "name": "PCAL9535A I/O Expander", "documentation": "https://www.home-assistant.io/components/pcal9535a", - "requirements": [ - "pcal9535a==0.7" - ], + "requirements": ["pcal9535a==0.7"], "dependencies": [], "codeowners": ["@Shulyaka"] } diff --git a/homeassistant/components/pcal9535a/switch.py b/homeassistant/components/pcal9535a/switch.py index faebce5d67e..87c8ced1b0d 100644 --- a/homeassistant/components/pcal9535a/switch.py +++ b/homeassistant/components/pcal9535a/switch.py @@ -1,8 +1,8 @@ """Support for switch sensor using I2C PCAL9535A chip.""" import logging -import voluptuous as vol from pcal9535a import PCAL9535A +import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import DEVICE_DEFAULT_NAME diff --git a/homeassistant/components/pencom/manifest.json b/homeassistant/components/pencom/manifest.json index d31b9a811cf..33c91b811c7 100644 --- a/homeassistant/components/pencom/manifest.json +++ b/homeassistant/components/pencom/manifest.json @@ -2,9 +2,7 @@ "domain": "pencom", "name": "Pencom", "documentation": "https://www.home-assistant.io/integrations/pencom", - "requirements": [ - "pencompy==0.0.3" - ], + "requirements": ["pencompy==0.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 33f17b18a80..0311bd4d30d 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -14,7 +14,6 @@ from homeassistant.loader import bind_hass from homeassistant.util import slugify import homeassistant.util.dt as dt_util - # mypy: allow-untyped-calls, allow-untyped-defs ATTR_CREATED_AT = "created_at" diff --git a/homeassistant/components/persistent_notification/manifest.json b/homeassistant/components/persistent_notification/manifest.json index 9ee9692c655..81fa8a9497b 100644 --- a/homeassistant/components/persistent_notification/manifest.json +++ b/homeassistant/components/persistent_notification/manifest.json @@ -1,10 +1,9 @@ { "domain": "persistent_notification", - "name": "Persistent notification", + "name": "Persistent Notification", "documentation": "https://www.home-assistant.io/integrations/persistent_notification", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 832853c670d..c34fb89a718 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -1,34 +1,48 @@ """Support for tracking people.""" -from collections import OrderedDict -from itertools import chain import logging -from typing import Optional -import uuid +from typing import List, Optional, cast import voluptuous as vol +from homeassistant.auth import EVENT_USER_REMOVED from homeassistant.components import websocket_api from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN, ATTR_SOURCE_TYPE, + DOMAIN as DEVICE_TRACKER_DOMAIN, SOURCE_TYPE_GPS, ) from homeassistant.const import ( + ATTR_EDITABLE, + ATTR_ENTITY_ID, + ATTR_GPS_ACCURACY, ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_GPS_ACCURACY, + ATTR_NAME, CONF_ID, CONF_NAME, + CONF_TYPE, EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, - STATE_UNAVAILABLE, + SERVICE_RELOAD, STATE_HOME, STATE_NOT_HOME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import ( + Event, + HomeAssistant, + ServiceCall, + State, + callback, + split_entity_id, +) +from homeassistant.helpers import ( + collection, + config_validation as cv, + entity_registry, + service, ) -from homeassistant.core import callback, Event, State -from homeassistant.auth import EVENT_USER_REMOVED -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity @@ -38,7 +52,6 @@ from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) -ATTR_EDITABLE = "editable" ATTR_SOURCE = "source" ATTR_USER_ID = "user_id" @@ -48,8 +61,7 @@ CONF_USER_ID = "user_id" DOMAIN = "person" STORAGE_KEY = DOMAIN -STORAGE_VERSION = 1 -SAVE_DELAY = 10 +STORAGE_VERSION = 2 # Device tracker states to ignore IGNORE_STATES = (STATE_UNKNOWN, STATE_UNAVAILABLE) @@ -75,217 +87,248 @@ _UNDEF = object() @bind_hass async def async_create_person(hass, name, *, user_id=None, device_trackers=None): """Create a new person.""" - await hass.data[DOMAIN].async_create_person( - name=name, user_id=user_id, device_trackers=device_trackers + await hass.data[DOMAIN][1].async_create_item( + {ATTR_NAME: name, ATTR_USER_ID: user_id, "device_trackers": device_trackers} ) -class PersonManager: - """Manage person data.""" +@bind_hass +async def async_add_user_device_tracker( + hass: HomeAssistant, user_id: str, device_tracker_entity_id: str +): + """Add a device tracker to a person linked to a user.""" + coll = cast(PersonStorageCollection, hass.data[DOMAIN][1]) + + for person in coll.async_items(): + if person.get(ATTR_USER_ID) != user_id: + continue + + device_trackers = person["device_trackers"] + + if device_tracker_entity_id in device_trackers: + return + + await coll.async_update_item( + person[collection.CONF_ID], + {"device_trackers": device_trackers + [device_tracker_entity_id]}, + ) + break + + +CREATE_FIELDS = { + vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_USER_ID): vol.Any(str, None), + vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All( + cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN) + ), +} + + +UPDATE_FIELDS = { + vol.Optional(CONF_NAME): vol.All(str, vol.Length(min=1)), + vol.Optional(CONF_USER_ID): vol.Any(str, None), + vol.Optional(CONF_DEVICE_TRACKERS, default=list): vol.All( + cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN) + ), +} + + +class PersonStore(Store): + """Person storage.""" + + async def _async_migrate_func(self, old_version, old_data): + """Migrate to the new version. + + Migrate storage to use format of collection helper. + """ + return {"items": old_data["persons"]} + + +class PersonStorageCollection(collection.StorageCollection): + """Person collection stored in storage.""" + + CREATE_SCHEMA = vol.Schema(CREATE_FIELDS) + UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS) def __init__( - self, hass: HomeAssistantType, component: EntityComponent, config_persons + self, + store: Store, + logger: logging.Logger, + id_manager: collection.IDManager, + yaml_collection: collection.YamlCollection, ): - """Initialize person storage.""" - self.hass = hass - self.component = component - self.store = Store(hass, STORAGE_VERSION, STORAGE_KEY) - self.storage_data = None + """Initialize a person storage collection.""" + super().__init__(store, logger, id_manager) + self.async_add_listener(self._collection_changed) + self.yaml_collection = yaml_collection - config_data = self.config_data = OrderedDict() - for conf in config_persons: - person_id = conf[CONF_ID] + async def async_load(self) -> None: + """Load the Storage collection.""" + await super().async_load() + self.hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._entity_registry_updated + ) - if person_id in config_data: - _LOGGER.error("Found config user with duplicate ID: %s", person_id) + async def _entity_registry_updated(self, event) -> None: + """Handle entity registry updated.""" + if event.data["action"] != "remove": + return + + entity_id = event.data[ATTR_ENTITY_ID] + + if split_entity_id(entity_id)[0] != "device_tracker": + return + + for person in list(self.data.values()): + if entity_id not in person["device_trackers"]: continue - config_data[person_id] = conf + await self.async_update_item( + person[collection.CONF_ID], + { + "device_trackers": [ + devt for devt in person["device_trackers"] if devt != entity_id + ] + }, + ) - @property - def storage_persons(self): - """Iterate over persons stored in storage.""" - return list(self.storage_data.values()) + async def _process_create_data(self, data: dict) -> dict: + """Validate the config is valid.""" + data = self.CREATE_SCHEMA(data) - @property - def config_persons(self): - """Iterate over persons stored in config.""" - return list(self.config_data.values()) - - async def async_initialize(self): - """Get the person data.""" - raw_storage = await self.store.async_load() - - if raw_storage is None: - raw_storage = {"persons": []} - - storage_data = self.storage_data = OrderedDict() - - for person in raw_storage["persons"]: - storage_data[person[CONF_ID]] = person - - entities = [] - seen_users = set() - - for person_conf in self.config_data.values(): - person_id = person_conf[CONF_ID] - user_id = person_conf.get(CONF_USER_ID) - - if user_id is not None: - if await self.hass.auth.async_get_user(user_id) is None: - _LOGGER.error("Invalid user_id detected for person %s", person_id) - continue - - if user_id in seen_users: - _LOGGER.error( - "Duplicate user_id %s detected for person %s", - user_id, - person_id, - ) - continue - - seen_users.add(user_id) - - entities.append(Person(person_conf, False)) - - # To make sure IDs don't overlap between config/storage - seen_persons = set(self.config_data) - - for person_conf in storage_data.values(): - person_id = person_conf[CONF_ID] - user_id = person_conf[CONF_USER_ID] - - if person_id in seen_persons: - _LOGGER.error( - "Skipping adding person from storage with same ID as" - " configuration.yaml entry: %s", - person_id, - ) - continue - - if user_id is not None and user_id in seen_users: - _LOGGER.error( - "Duplicate user_id %s detected for person %s", user_id, person_id - ) - continue - - # To make sure all users have just 1 person linked. - seen_users.add(user_id) - - entities.append(Person(person_conf, True)) - - if entities: - await self.component.async_add_entities(entities) - - self.hass.bus.async_listen(EVENT_USER_REMOVED, self._user_removed) - - async def async_create_person(self, *, name, device_trackers=None, user_id=None): - """Create a new person.""" - if not name: - raise ValueError("Name is required") + user_id = data.get(CONF_USER_ID) if user_id is not None: await self._validate_user_id(user_id) - person = { - CONF_ID: uuid.uuid4().hex, - CONF_NAME: name, - CONF_USER_ID: user_id, - CONF_DEVICE_TRACKERS: device_trackers or [], - } - self.storage_data[person[CONF_ID]] = person - self._async_schedule_save() - await self.component.async_add_entities([Person(person, True)]) - return person + return data - async def async_update_person( - self, person_id, *, name=_UNDEF, device_trackers=_UNDEF, user_id=_UNDEF - ): - """Update person.""" - current = self.storage_data.get(person_id) + @callback + def _get_suggested_id(self, info: dict) -> str: + """Suggest an ID based on the config.""" + return info[CONF_NAME] - if current is None: - raise ValueError("Invalid person specified.") + async def _update_data(self, data: dict, update_data: dict) -> dict: + """Return a new updated data object.""" + update_data = self.UPDATE_SCHEMA(update_data) - changes = { - key: value - for key, value in ( - (CONF_NAME, name), - (CONF_DEVICE_TRACKERS, device_trackers), - (CONF_USER_ID, user_id), - ) - if value is not _UNDEF and current[key] != value - } + user_id = update_data.get(CONF_USER_ID) - if CONF_USER_ID in changes and user_id is not None: + if user_id is not None and user_id != data.get(CONF_USER_ID): await self._validate_user_id(user_id) - self.storage_data[person_id].update(changes) - self._async_schedule_save() - - for entity in self.component.entities: - if entity.unique_id == person_id: - entity.person_updated() - break - - return self.storage_data[person_id] - - async def async_delete_person(self, person_id): - """Delete person.""" - if person_id not in self.storage_data: - raise ValueError("Invalid person specified.") - - self.storage_data.pop(person_id) - self._async_schedule_save() - ent_reg = await self.hass.helpers.entity_registry.async_get_registry() - - for entity in self.component.entities: - if entity.unique_id == person_id: - await entity.async_remove() - ent_reg.async_remove(entity.entity_id) - break - - @callback - def _async_schedule_save(self) -> None: - """Schedule saving the area registry.""" - self.store.async_delay_save(self._data_to_save, SAVE_DELAY) - - @callback - def _data_to_save(self) -> dict: - """Return data of area registry to store in a file.""" - return {"persons": list(self.storage_data.values())} + return {**data, **update_data} async def _validate_user_id(self, user_id): """Validate the used user_id.""" if await self.hass.auth.async_get_user(user_id) is None: raise ValueError("User does not exist") - if any( - person - for person in chain(self.storage_data.values(), self.config_data.values()) - if person.get(CONF_USER_ID) == user_id - ): - raise ValueError("User already taken") + for persons in (self.data.values(), self.yaml_collection.async_items()): + if any(person for person in persons if person.get(CONF_USER_ID) == user_id): + raise ValueError("User already taken") - async def _user_removed(self, event: Event): - """Handle event that a person is removed.""" - user_id = event.data["user_id"] - for person in self.storage_data.values(): - if person[CONF_USER_ID] == user_id: - await self.async_update_person(person_id=person[CONF_ID], user_id=None) + async def _collection_changed( + self, change_type: str, item_id: str, config: Optional[dict] + ) -> None: + """Handle a collection change.""" + if change_type != collection.CHANGE_REMOVED: + return + + ent_reg = await entity_registry.async_get_registry(self.hass) + ent_reg.async_remove(ent_reg.async_get_entity_id(DOMAIN, DOMAIN, item_id)) + + +async def filter_yaml_data(hass: HomeAssistantType, persons: List[dict]) -> List[dict]: + """Validate YAML data that we can't validate via schema.""" + filtered = [] + person_invalid_user = [] + + for person_conf in persons: + user_id = person_conf.get(CONF_USER_ID) + + if user_id is not None: + if await hass.auth.async_get_user(user_id) is None: + _LOGGER.error( + "Invalid user_id detected for person %s", + person_conf[collection.CONF_ID], + ) + person_invalid_user.append( + f"- Person {person_conf[CONF_NAME]} (id: {person_conf[collection.CONF_ID]}) points at invalid user {user_id}" + ) + continue + + filtered.append(person_conf) + + if person_invalid_user: + hass.components.persistent_notification.async_create( + f""" +The following persons point at invalid users: + +{"- ".join(person_invalid_user)} + """, + "Invalid Person Configuration", + DOMAIN, + ) + + return filtered async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the person component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass) - conf_persons = config.get(DOMAIN, []) - manager = hass.data[DOMAIN] = PersonManager(hass, component, conf_persons) - await manager.async_initialize() + entity_component = EntityComponent(_LOGGER, DOMAIN, hass) + id_manager = collection.IDManager() + yaml_collection = collection.YamlCollection( + logging.getLogger(f"{__name__}.yaml_collection"), id_manager + ) + storage_collection = PersonStorageCollection( + PersonStore(hass, STORAGE_VERSION, STORAGE_KEY), + logging.getLogger(f"{__name__}.storage_collection"), + id_manager, + yaml_collection, + ) + + collection.attach_entity_component_collection( + entity_component, yaml_collection, lambda conf: Person(conf, False) + ) + collection.attach_entity_component_collection( + entity_component, storage_collection, lambda conf: Person(conf, True) + ) + + await yaml_collection.async_load( + await filter_yaml_data(hass, config.get(DOMAIN, [])) + ) + await storage_collection.async_load() + + hass.data[DOMAIN] = (yaml_collection, storage_collection) + + collection.StorageCollectionWebsocket( + storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS + ).async_setup(hass, create_list=False) websocket_api.async_register_command(hass, ws_list_person) - websocket_api.async_register_command(hass, ws_create_person) - websocket_api.async_register_command(hass, ws_update_person) - websocket_api.async_register_command(hass, ws_delete_person) + + async def _handle_user_removed(event: Event) -> None: + """Handle a user being removed.""" + user_id = event.data[ATTR_USER_ID] + for person in storage_collection.async_items(): + if person[CONF_USER_ID] == user_id: + await storage_collection.async_update_item( + person[CONF_ID], {CONF_USER_ID: None} + ) + + hass.bus.async_listen(EVENT_USER_REMOVED, _handle_user_removed) + + async def async_reload_yaml(call: ServiceCall): + """Reload YAML.""" + conf = await entity_component.async_prepare_reload(skip_reset=True) + if conf is None: + return + await yaml_collection.async_load(await filter_yaml_data(hass, conf[DOMAIN])) + + service.async_register_admin_service( + hass, DOMAIN, SERVICE_RELOAD, async_reload_yaml + ) return True @@ -353,21 +396,21 @@ class Person(RestoreEntity): if self.hass.is_running: # Update person now if hass is already running. - self.person_updated() + await self.async_update_config(self._config) else: # Wait for hass start to not have race between person # and device trackers finishing setup. - @callback - def person_start_hass(now): - self.person_updated() + async def person_start_hass(now): + await self.async_update_config(self._config) self.hass.bus.async_listen_once( EVENT_HOMEASSISTANT_START, person_start_hass ) - @callback - def person_updated(self): + async def async_update_config(self, config): """Handle when the config is updated.""" + self._config = config + if self._unsub_track_device is not None: self._unsub_track_device() self._unsub_track_device = None @@ -436,94 +479,17 @@ class Person(RestoreEntity): self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY) -@websocket_api.websocket_command({vol.Required("type"): "person/list"}) +@websocket_api.websocket_command({vol.Required(CONF_TYPE): "person/list"}) def ws_list_person( hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg ): """List persons.""" - manager: PersonManager = hass.data[DOMAIN] + yaml, storage = hass.data[DOMAIN] connection.send_result( - msg["id"], - {"storage": manager.storage_persons, "config": manager.config_persons}, + msg[ATTR_ID], {"storage": storage.async_items(), "config": yaml.async_items()} ) -@websocket_api.websocket_command( - { - vol.Required("type"): "person/create", - vol.Required("name"): vol.All(str, vol.Length(min=1)), - vol.Optional("user_id"): vol.Any(str, None), - vol.Optional("device_trackers", default=[]): vol.All( - cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN) - ), - } -) -@websocket_api.require_admin -@websocket_api.async_response -async def ws_create_person( - hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg -): - """Create a person.""" - manager: PersonManager = hass.data[DOMAIN] - try: - person = await manager.async_create_person( - name=msg["name"], - user_id=msg.get("user_id"), - device_trackers=msg["device_trackers"], - ) - connection.send_result(msg["id"], person) - except ValueError as err: - connection.send_error( - msg["id"], websocket_api.const.ERR_INVALID_FORMAT, str(err) - ) - - -@websocket_api.websocket_command( - { - vol.Required("type"): "person/update", - vol.Required("person_id"): str, - vol.Required("name"): vol.All(str, vol.Length(min=1)), - vol.Optional("user_id"): vol.Any(str, None), - vol.Optional(CONF_DEVICE_TRACKERS, default=[]): vol.All( - cv.ensure_list, cv.entities_domain(DEVICE_TRACKER_DOMAIN) - ), - } -) -@websocket_api.require_admin -@websocket_api.async_response -async def ws_update_person( - hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg -): - """Update a person.""" - manager: PersonManager = hass.data[DOMAIN] - changes = {} - for key in ("name", "user_id", "device_trackers"): - if key in msg: - changes[key] = msg[key] - - try: - person = await manager.async_update_person(msg["person_id"], **changes) - connection.send_result(msg["id"], person) - except ValueError as err: - connection.send_error( - msg["id"], websocket_api.const.ERR_INVALID_FORMAT, str(err) - ) - - -@websocket_api.websocket_command( - {vol.Required("type"): "person/delete", vol.Required("person_id"): str} -) -@websocket_api.require_admin -@websocket_api.async_response -async def ws_delete_person( - hass: HomeAssistantType, connection: websocket_api.ActiveConnection, msg -): - """Delete a person.""" - manager: PersonManager = hass.data[DOMAIN] - await manager.async_delete_person(msg["person_id"]) - connection.send_result(msg["id"]) - - def _get_latest(prev: Optional[State], curr: State): """Get latest state.""" if prev is None or curr.last_updated > prev.last_updated: diff --git a/homeassistant/components/person/manifest.json b/homeassistant/components/person/manifest.json index cf50b8029c2..df54743ce75 100644 --- a/homeassistant/components/person/manifest.json +++ b/homeassistant/components/person/manifest.json @@ -4,5 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/person", "requirements": [], "dependencies": [], - "codeowners": [] + "after_dependencies": ["device_tracker"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/person/services.yaml b/homeassistant/components/person/services.yaml new file mode 100644 index 00000000000..0af934f56b8 --- /dev/null +++ b/homeassistant/components/person/services.yaml @@ -0,0 +1,2 @@ +reload: + description: Reload the person configuration. diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 4845aa16a37..e8e347722a6 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -1,10 +1,8 @@ { "domain": "philips_js", - "name": "Philips js", + "name": "Philips TV", "documentation": "https://www.home-assistant.io/integrations/philips_js", - "requirements": [ - "ha-philipsjs==0.0.8" - ], + "requirements": ["ha-philipsjs==0.0.8"], "dependencies": [], "codeowners": ["@elupus"] } diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index 95351083b5a..ed6144af47e 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -1,110 +1,222 @@ """The pi_hole component.""" import logging -import voluptuous as vol from hole import Hole from hole.exceptions import HoleError +import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + CONF_API_KEY, CONF_HOST, CONF_NAME, - CONF_API_KEY, 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, + CONF_SLUG, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, DEFAULT_VERIFY_SSL, + DOMAIN, MIN_TIME_BETWEEN_UPDATES, SERVICE_DISABLE, SERVICE_DISABLE_ATTR_DURATION, + SERVICE_DISABLE_ATTR_NAME, SERVICE_ENABLE, + SERVICE_ENABLE_ATTR_NAME, ) + +def ensure_unique_names_and_slugs(config): + """Ensure that each configuration dict contains a unique `name` value.""" + names = {} + slugs = {} + for conf in config: + if conf[CONF_NAME] not in names and conf[CONF_SLUG] not in slugs: + names[conf[CONF_NAME]] = conf[CONF_HOST] + slugs[conf[CONF_SLUG]] = conf[CONF_HOST] + else: + raise vol.Invalid( + "Duplicate name '{}' (or slug '{}') for '{}' (already in use by '{}'). Each configured Pi-hole must have a unique name.".format( + conf[CONF_NAME], + conf[CONF_SLUG], + conf[CONF_HOST], + names.get(conf[CONF_NAME], slugs[conf[CONF_SLUG]]), + ) + ) + return config + + +def coerce_slug(config): + """Coerce the name of the Pi-Hole into a slug.""" + config[CONF_SLUG] = cv.slugify(config[CONF_NAME]) + return config + + LOGGER = logging.getLogger(__name__) +PI_HOLE_SCHEMA = vol.Schema( + vol.All( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_API_KEY): 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, + }, + coerce_slug, + ) +) + 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_API_KEY): 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.All(cv.ensure_list, [PI_HOLE_SCHEMA], ensure_unique_names_and_slugs) ) }, extra=vol.ALLOW_EXTRA, ) -SERVICE_DISABLE_SCHEMA = vol.Schema( - { - vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( - cv.time_period_str, cv.positive_timedelta - ) - } -) - 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] - api_key = conf.get(CONF_API_KEY) + def get_data(): + """Retrive component data.""" + return hass.data[DOMAIN] - LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + def ensure_api_token(call_data): + """Ensure the Pi-Hole to be enabled/disabled has a api_token configured.""" + data = get_data() + if SERVICE_DISABLE_ATTR_NAME not in call_data: + for slug in data: + call_data[SERVICE_DISABLE_ATTR_NAME] = data[slug].name + ensure_api_token(call_data) - session = async_get_clientsession(hass, verify_tls) - pi_hole = PiHoleData( - Hole( - host, hass.loop, session, location=location, tls=use_tls, api_token=api_key - ), - name, + call_data[SERVICE_DISABLE_ATTR_NAME] = None + else: + slug = cv.slugify(call_data[SERVICE_DISABLE_ATTR_NAME]) + + if (data[slug]).api.api_token is None: + raise vol.Invalid( + "Pi-hole '{}' must have an api_key provided in configuration to be enabled.".format( + pi_hole.name + ) + ) + + return call_data + + service_disable_schema = vol.Schema( # pylint: disable=invalid-name + vol.All( + { + vol.Required(SERVICE_DISABLE_ATTR_DURATION): vol.All( + cv.time_period_str, cv.positive_timedelta + ), + vol.Optional(SERVICE_DISABLE_ATTR_NAME): vol.In( + [conf[CONF_NAME] for conf in config[DOMAIN]], msg="Unknown Pi-Hole", + ), + }, + ensure_api_token, + ) ) - await pi_hole.async_update() + service_enable_schema = vol.Schema( + { + vol.Optional(SERVICE_ENABLE_ATTR_NAME): vol.In( + [conf[CONF_NAME] for conf in config[DOMAIN]], msg="Unknown Pi-Hole" + ) + } + ) - hass.data[DOMAIN] = pi_hole + hass.data[DOMAIN] = {} - async def handle_disable(call): - if api_key is None: - raise vol.Invalid("Pi-hole api_key must be provided in configuration") + for conf in config[DOMAIN]: + name = conf[CONF_NAME] + slug = conf[CONF_SLUG] + host = conf[CONF_HOST] + use_tls = conf[CONF_SSL] + verify_tls = conf[CONF_VERIFY_SSL] + location = conf[CONF_LOCATION] + api_key = conf.get(CONF_API_KEY) + LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) + + session = async_get_clientsession(hass, verify_tls) + pi_hole = PiHoleData( + Hole( + host, + hass.loop, + session, + location=location, + tls=use_tls, + api_token=api_key, + ), + name, + ) + + await pi_hole.async_update() + + hass.data[DOMAIN][slug] = pi_hole + + async def disable_service_handler(call): + """Handle the service call to disable a single Pi-Hole or all configured Pi-Holes.""" duration = call.data[SERVICE_DISABLE_ATTR_DURATION].total_seconds() + name = call.data.get(SERVICE_DISABLE_ATTR_NAME) - LOGGER.debug("Disabling %s %s for %d seconds", DOMAIN, host, duration) - await pi_hole.api.disable(duration) + async def do_disable(name): + """Disable the named Pi-Hole.""" + slug = cv.slugify(name) + pi_hole = hass.data[DOMAIN][slug] - async def handle_enable(call): - if api_key is None: - raise vol.Invalid("Pi-hole api_key must be provided in configuration") + LOGGER.debug( + "Disabling Pi-hole '%s' (%s) for %d seconds", + name, + pi_hole.api.host, + duration, + ) + await pi_hole.api.disable(duration) - LOGGER.debug("Enabling %s %s", DOMAIN, host) - await pi_hole.api.enable() + if name is not None: + await do_disable(name) + else: + for pi_hole in hass.data[DOMAIN].values(): + await do_disable(pi_hole.name) + + async def enable_service_handler(call): + """Handle the service call to enable a single Pi-Hole or all configured Pi-Holes.""" + + name = call.data.get(SERVICE_ENABLE_ATTR_NAME) + + async def do_enable(name): + """Enable the named Pi-Hole.""" + slug = cv.slugify(name) + pi_hole = hass.data[DOMAIN][slug] + + LOGGER.debug("Enabling Pi-hole '%s' (%s)", name, pi_hole.api.host) + await pi_hole.api.enable() + + if name is not None: + await do_enable(name) + else: + for pi_hole in hass.data[DOMAIN].values(): + await do_enable(pi_hole.name) hass.services.async_register( - DOMAIN, SERVICE_DISABLE, handle_disable, schema=SERVICE_DISABLE_SCHEMA + DOMAIN, SERVICE_DISABLE, disable_service_handler, schema=service_disable_schema ) - hass.services.async_register(DOMAIN, SERVICE_ENABLE, handle_enable) + hass.services.async_register( + DOMAIN, SERVICE_ENABLE, enable_service_handler, schema=service_enable_schema + ) hass.async_create_task(async_load_platform(hass, SENSOR_DOMAIN, DOMAIN, {}, config)) diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index 54220547950..0ae62b31865 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -4,8 +4,8 @@ from datetime import timedelta DOMAIN = "pi_hole" CONF_LOCATION = "location" +CONF_SLUG = "slug" -DEFAULT_HOST = "pi.hole" DEFAULT_LOCATION = "admin" DEFAULT_METHOD = "GET" DEFAULT_NAME = "Pi-Hole" @@ -13,8 +13,10 @@ DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True SERVICE_DISABLE = "disable" -SERVICE_ENABLE = "enable" SERVICE_DISABLE_ATTR_DURATION = "duration" +SERVICE_DISABLE_ATTR_NAME = "name" +SERVICE_ENABLE = "enable" +SERVICE_ENABLE_ATTR_NAME = SERVICE_DISABLE_ATTR_NAME ATTR_BLOCKED_DOMAINS = "domains_blocked" diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 089e1e60a11..2f93929d8aa 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -1,13 +1,8 @@ { "domain": "pi_hole", - "name": "Pi hole", + "name": "Pi-hole", "documentation": "https://www.home-assistant.io/integrations/pi_hole", - "requirements": [ - "hole==0.5.0" - ], + "requirements": ["hole==0.5.0"], "dependencies": [], - "codeowners": [ - "@fabaff", - "@johnluetke" - ] + "codeowners": ["@fabaff", "@johnluetke"] } diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 4e80e9767a6..c01a0167e53 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -4,10 +4,10 @@ import logging from homeassistant.helpers.entity import Entity from .const import ( - DOMAIN as PIHOLE_DOMAIN, ATTR_BLOCKED_DOMAINS, - SENSOR_LIST, + DOMAIN as PIHOLE_DOMAIN, SENSOR_DICT, + SENSOR_LIST, ) LOGGER = logging.getLogger(__name__) @@ -18,10 +18,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if discovery_info is None: return - pi_hole = hass.data[PIHOLE_DOMAIN] - sensors = [] - sensors = [PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST] + for pi_hole in hass.data[PIHOLE_DOMAIN].values(): + for sensor in [ + PiHoleSensor(pi_hole, sensor_name) for sensor_name in SENSOR_LIST + ]: + sensors.append(sensor) async_add_entities(sensors, True) diff --git a/homeassistant/components/pi_hole/services.yaml b/homeassistant/components/pi_hole/services.yaml index b16ed21a5d3..e3cc8624e36 100644 --- a/homeassistant/components/pi_hole/services.yaml +++ b/homeassistant/components/pi_hole/services.yaml @@ -1,8 +1,15 @@ disable: - description: Disable Pi-hole for an amount of time + description: Disable configured Pi-hole(s) for an amount of time fields: duration: description: Time that the Pi-hole should be disabled for example: "00:00:15" + name: + description: "[Optional] When multiple Pi-holes are configured, the name of the one to disable. If omitted, all configured Pi-holes will be disabled." + example: "Pi-Hole" enable: - description: Enable Pi-hole \ No newline at end of file + description: Enable configured Pi-hole(s) + fields: + name: + description: "[Optional] When multiple Pi-holes are configured, the name of the one to enable. If omitted, all configured Pi-holes will be enabled." + example: "Pi-Hole" \ No newline at end of file diff --git a/homeassistant/components/picotts/manifest.json b/homeassistant/components/picotts/manifest.json index 5150ddd0404..43963bb2cd8 100644 --- a/homeassistant/components/picotts/manifest.json +++ b/homeassistant/components/picotts/manifest.json @@ -1,6 +1,6 @@ { "domain": "picotts", - "name": "Picotts", + "name": "Pico TTS", "documentation": "https://www.home-assistant.io/integrations/picotts", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/piglow/manifest.json b/homeassistant/components/piglow/manifest.json index 012ce4f014e..1aa0ead9fd8 100644 --- a/homeassistant/components/piglow/manifest.json +++ b/homeassistant/components/piglow/manifest.json @@ -2,9 +2,7 @@ "domain": "piglow", "name": "Piglow", "documentation": "https://www.home-assistant.io/integrations/piglow", - "requirements": [ - "piglow==1.2.4" - ], + "requirements": ["piglow==1.2.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pilight/__init__.py b/homeassistant/components/pilight/__init__.py index e8cc0862a53..50ee1b248b0 100644 --- a/homeassistant/components/pilight/__init__.py +++ b/homeassistant/components/pilight/__init__.py @@ -1,25 +1,24 @@ """Component to create an interface to a Pilight daemon.""" -import logging +from datetime import timedelta import functools +import logging import socket import threading -from datetime import timedelta - -import voluptuous as vol from pilight import pilight +import voluptuous as vol -from homeassistant.helpers.event import track_point_in_utc_time -from homeassistant.util import dt as dt_util -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT, - CONF_WHITELIST, CONF_PROTOCOL, + CONF_WHITELIST, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_point_in_utc_time +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pilight/base_class.py b/homeassistant/components/pilight/base_class.py new file mode 100644 index 00000000000..382d47f41cd --- /dev/null +++ b/homeassistant/components/pilight/base_class.py @@ -0,0 +1,185 @@ +"""Base class for pilight.""" +import logging + +import voluptuous as vol + +from homeassistant.components.pilight import DOMAIN, EVENT, SERVICE_NAME +from homeassistant.const import ( + CONF_ID, + CONF_NAME, + CONF_PROTOCOL, + CONF_STATE, + STATE_OFF, + STATE_ON, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import ( + CONF_ECHO, + CONF_OFF, + CONF_OFF_CODE, + CONF_OFF_CODE_RECEIVE, + CONF_ON, + CONF_ON_CODE, + CONF_ON_CODE_RECEIVE, + CONF_SYSTEMCODE, + CONF_UNIT, + CONF_UNITCODE, +) + +_LOGGER = logging.getLogger(__name__) + +COMMAND_SCHEMA = vol.Schema( + { + vol.Optional(CONF_PROTOCOL): cv.string, + vol.Optional(CONF_ON): cv.positive_int, + vol.Optional(CONF_OFF): cv.positive_int, + vol.Optional(CONF_UNIT): cv.positive_int, + vol.Optional(CONF_UNITCODE): cv.positive_int, + vol.Optional(CONF_ID): vol.Any(cv.positive_int, cv.string), + vol.Optional(CONF_STATE): vol.Any(STATE_ON, STATE_OFF), + vol.Optional(CONF_SYSTEMCODE): cv.positive_int, + }, + extra=vol.ALLOW_EXTRA, +) + +RECEIVE_SCHEMA = COMMAND_SCHEMA.extend({vol.Optional(CONF_ECHO): cv.boolean}) + +SWITCHES_SCHEMA = vol.Schema( + { + vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, + vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_OFF_CODE_RECEIVE): vol.All(cv.ensure_list, [COMMAND_SCHEMA]), + vol.Optional(CONF_ON_CODE_RECEIVE): vol.All(cv.ensure_list, [COMMAND_SCHEMA]), + } +) + + +class PilightBaseDevice(RestoreEntity): + """Base class for pilight switches and lights.""" + + def __init__(self, hass, name, config): + """Initialize a device.""" + self._hass = hass + self._name = config.get(CONF_NAME, name) + self._is_on = False + self._code_on = config.get(CONF_ON_CODE) + self._code_off = config.get(CONF_OFF_CODE) + + code_on_receive = config.get(CONF_ON_CODE_RECEIVE, []) + code_off_receive = config.get(CONF_OFF_CODE_RECEIVE, []) + + self._code_on_receive = [] + self._code_off_receive = [] + + for code_list, conf in ( + (self._code_on_receive, code_on_receive), + (self._code_off_receive, code_off_receive), + ): + for code in conf: + echo = code.pop(CONF_ECHO, True) + code_list.append(_ReceiveHandle(code, echo)) + + if any(self._code_on_receive) or any(self._code_off_receive): + hass.bus.listen(EVENT, self._handle_code) + + self._brightness = 255 + + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + await super().async_added_to_hass() + state = await self.async_get_last_state() + if state: + self._is_on = state.state == STATE_ON + self._brightness = state.attributes.get("brightness") + + @property + def name(self): + """Get the name of the switch.""" + return self._name + + @property + def should_poll(self): + """No polling needed, state set when correct code is received.""" + return False + + @property + def assumed_state(self): + """Return True if unable to access real state of the entity.""" + return True + + @property + def is_on(self): + """Return true if switch is on.""" + return self._is_on + + def _handle_code(self, call): + """Check if received code by the pilight-daemon. + + If the code matches the receive on/off codes of this switch the switch + state is changed accordingly. + """ + # - True if off_code/on_code is contained in received code dict, not + # all items have to match. + # - Call turn on/off only once, even if more than one code is received + if any(self._code_on_receive): + for on_code in self._code_on_receive: + if on_code.match(call.data): + on_code.run(switch=self, turn_on=True) + break + + if any(self._code_off_receive): + for off_code in self._code_off_receive: + if off_code.match(call.data): + off_code.run(switch=self, turn_on=False) + break + + def set_state(self, turn_on, send_code=True, dimlevel=None): + """Set the state of the switch. + + This sets the state of the switch. If send_code is set to True, then + it will call the pilight.send service to actually send the codes + to the pilight daemon. + """ + if send_code: + if turn_on: + code = self._code_on + if dimlevel is not None: + code.update({"dimlevel": dimlevel}) + + self._hass.services.call(DOMAIN, SERVICE_NAME, code, blocking=True) + else: + self._hass.services.call( + DOMAIN, SERVICE_NAME, self._code_off, blocking=True + ) + + self._is_on = turn_on + self.schedule_update_ha_state() + + def turn_on(self, **kwargs): + """Turn the switch on by calling pilight.send service with on code.""" + self.set_state(turn_on=True) + + def turn_off(self, **kwargs): + """Turn the switch on by calling pilight.send service with off code.""" + self.set_state(turn_on=False) + + +class _ReceiveHandle: + def __init__(self, config, echo): + """Initialize the handle.""" + self.config_items = config.items() + self.echo = echo + + def match(self, code): + """Test if the received code matches the configured values. + + The received values have to be a subset of the configured options. + """ + return self.config_items <= code.items() + + def run(self, switch, turn_on): + """Change the state of the switch.""" + switch.set_state(turn_on=turn_on, send_code=self.echo) diff --git a/homeassistant/components/pilight/binary_sensor.py b/homeassistant/components/pilight/binary_sensor.py index a8f7d84b9b1..ae6d562725d 100644 --- a/homeassistant/components/pilight/binary_sensor.py +++ b/homeassistant/components/pilight/binary_sensor.py @@ -3,6 +3,7 @@ import datetime import logging import voluptuous as vol + from homeassistant.components import pilight from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( @@ -16,7 +17,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import track_point_in_time from homeassistant.util import dt as dt_util - _LOGGER = logging.getLogger(__name__) CONF_VARIABLE = "variable" diff --git a/homeassistant/components/pilight/const.py b/homeassistant/components/pilight/const.py new file mode 100644 index 00000000000..3aa53021d31 --- /dev/null +++ b/homeassistant/components/pilight/const.py @@ -0,0 +1,14 @@ +"""Consts used by pilight.""" + +CONF_DIMLEVEL_MAX = "dimlevel_max" +CONF_DIMLEVEL_MIN = "dimlevel_min" +CONF_ECHO = "echo" +CONF_OFF = "off" +CONF_OFF_CODE = "off_code" +CONF_OFF_CODE_RECEIVE = "off_code_receive" +CONF_ON = "on" +CONF_ON_CODE = "on_code" +CONF_ON_CODE_RECEIVE = "on_code_receive" +CONF_SYSTEMCODE = "systemcode" +CONF_UNIT = "unit" +CONF_UNITCODE = "unitcode" diff --git a/homeassistant/components/pilight/light.py b/homeassistant/components/pilight/light.py new file mode 100644 index 00000000000..49ce3d9a124 --- /dev/null +++ b/homeassistant/components/pilight/light.py @@ -0,0 +1,67 @@ +"""Support for switching devices via Pilight to on and off.""" +import logging + +import voluptuous as vol + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + Light, +) +from homeassistant.const import CONF_LIGHTS +import homeassistant.helpers.config_validation as cv + +from .base_class import SWITCHES_SCHEMA, PilightBaseDevice +from .const import CONF_DIMLEVEL_MAX, CONF_DIMLEVEL_MIN + +_LOGGER = logging.getLogger(__name__) + +LIGHTS_SCHEMA = SWITCHES_SCHEMA.extend( + { + vol.Optional(CONF_DIMLEVEL_MIN, default=0): cv.positive_int, + vol.Optional(CONF_DIMLEVEL_MAX, default=15): cv.positive_int, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Required(CONF_LIGHTS): vol.Schema({cv.string: LIGHTS_SCHEMA})} +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Pilight platform.""" + switches = config.get(CONF_LIGHTS) + devices = [] + + for dev_name, dev_config in switches.items(): + devices.append(PilightLight(hass, dev_name, dev_config)) + + add_entities(devices) + + +class PilightLight(PilightBaseDevice, Light): + """Representation of a Pilight switch.""" + + def __init__(self, hass, name, config): + """Initialize a switch.""" + super().__init__(hass, name, config) + self._dimlevel_min = config.get(CONF_DIMLEVEL_MIN) + self._dimlevel_max = config.get(CONF_DIMLEVEL_MAX) + + @property + def brightness(self): + """Return the brightness.""" + return self._brightness + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_BRIGHTNESS + + def turn_on(self, **kwargs): + """Turn the switch on by calling pilight.send service with on code.""" + self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) + dimlevel = int(self._brightness / (255 / self._dimlevel_max)) + + self.set_state(turn_on=True, dimlevel=dimlevel) diff --git a/homeassistant/components/pilight/manifest.json b/homeassistant/components/pilight/manifest.json index 7613f9e2a34..b2b2b08f7ff 100644 --- a/homeassistant/components/pilight/manifest.json +++ b/homeassistant/components/pilight/manifest.json @@ -2,9 +2,7 @@ "domain": "pilight", "name": "Pilight", "documentation": "https://www.home-assistant.io/integrations/pilight", - "requirements": [ - "pilight==0.1.1" - ], + "requirements": ["pilight==0.1.1"], "dependencies": [], - "codeowners": [] + "codeowners": ["@trekky12"] } diff --git a/homeassistant/components/pilight/sensor.py b/homeassistant/components/pilight/sensor.py index 006bebf74bb..e8c7b4bd4b6 100644 --- a/homeassistant/components/pilight/sensor.py +++ b/homeassistant/components/pilight/sensor.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_PAYLOAD -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity from homeassistant.components import pilight +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME, CONF_PAYLOAD, CONF_UNIT_OF_MEASUREMENT import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pilight/switch.py b/homeassistant/components/pilight/switch.py index fb95b91dfd8..0700b14e953 100644 --- a/homeassistant/components/pilight/switch.py +++ b/homeassistant/components/pilight/switch.py @@ -3,60 +3,14 @@ import logging import voluptuous as vol +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_SWITCHES import homeassistant.helpers.config_validation as cv -from homeassistant.components import pilight -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_NAME, - CONF_ID, - CONF_SWITCHES, - CONF_STATE, - CONF_PROTOCOL, - STATE_ON, -) -from homeassistant.helpers.restore_state import RestoreEntity + +from .base_class import SWITCHES_SCHEMA, PilightBaseDevice _LOGGER = logging.getLogger(__name__) -CONF_OFF_CODE = "off_code" -CONF_OFF_CODE_RECEIVE = "off_code_receive" -CONF_ON_CODE = "on_code" -CONF_ON_CODE_RECEIVE = "on_code_receive" -CONF_SYSTEMCODE = "systemcode" -CONF_UNIT = "unit" -CONF_UNITCODE = "unitcode" -CONF_ECHO = "echo" - -COMMAND_SCHEMA = vol.Schema( - { - vol.Optional(CONF_PROTOCOL): cv.string, - vol.Optional("on"): cv.positive_int, - vol.Optional("off"): cv.positive_int, - vol.Optional(CONF_UNIT): cv.positive_int, - vol.Optional(CONF_UNITCODE): cv.positive_int, - vol.Optional(CONF_ID): vol.Any(cv.positive_int, cv.string), - vol.Optional(CONF_STATE): cv.string, - vol.Optional(CONF_SYSTEMCODE): cv.positive_int, - }, - extra=vol.ALLOW_EXTRA, -) - -RECEIVE_SCHEMA = COMMAND_SCHEMA.extend({vol.Optional(CONF_ECHO): cv.boolean}) - -SWITCHES_SCHEMA = vol.Schema( - { - vol.Required(CONF_ON_CODE): COMMAND_SCHEMA, - vol.Required(CONF_OFF_CODE): COMMAND_SCHEMA, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_OFF_CODE_RECEIVE, default=[]): vol.All( - cv.ensure_list, [COMMAND_SCHEMA] - ), - vol.Optional(CONF_ON_CODE_RECEIVE, default=[]): vol.All( - cv.ensure_list, [COMMAND_SCHEMA] - ), - } -) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_SWITCHES): vol.Schema({cv.string: SWITCHES_SCHEMA})} ) @@ -67,138 +21,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): switches = config.get(CONF_SWITCHES) devices = [] - for dev_name, properties in switches.items(): - devices.append( - PilightSwitch( - hass, - properties.get(CONF_NAME, dev_name), - properties.get(CONF_ON_CODE), - properties.get(CONF_OFF_CODE), - properties.get(CONF_ON_CODE_RECEIVE), - properties.get(CONF_OFF_CODE_RECEIVE), - ) - ) + for dev_name, dev_config in switches.items(): + devices.append(PilightSwitch(hass, dev_name, dev_config)) add_entities(devices) -class _ReceiveHandle: - def __init__(self, config, echo): - """Initialize the handle.""" - self.config_items = config.items() - self.echo = echo - - def match(self, code): - """Test if the received code matches the configured values. - - The received values have to be a subset of the configured options. - """ - return self.config_items <= code.items() - - def run(self, switch, turn_on): - """Change the state of the switch.""" - switch.set_state(turn_on=turn_on, send_code=self.echo) - - -class PilightSwitch(SwitchDevice, RestoreEntity): +class PilightSwitch(PilightBaseDevice, SwitchDevice): """Representation of a Pilight switch.""" - - def __init__( - self, hass, name, code_on, code_off, code_on_receive, code_off_receive - ): - """Initialize the switch.""" - self._hass = hass - self._name = name - self._state = False - self._code_on = code_on - self._code_off = code_off - - self._code_on_receive = [] - self._code_off_receive = [] - - for code_list, conf in ( - (self._code_on_receive, code_on_receive), - (self._code_off_receive, code_off_receive), - ): - for code in conf: - echo = code.pop(CONF_ECHO, True) - code_list.append(_ReceiveHandle(code, echo)) - - if any(self._code_on_receive) or any(self._code_off_receive): - hass.bus.listen(pilight.EVENT, self._handle_code) - - async def async_added_to_hass(self): - """Call when entity about to be added to hass.""" - await super().async_added_to_hass() - state = await self.async_get_last_state() - if state: - self._state = state.state == STATE_ON - - @property - def name(self): - """Get the name of the switch.""" - return self._name - - @property - def should_poll(self): - """No polling needed, state set when correct code is received.""" - return False - - @property - def assumed_state(self): - """Return True if unable to access real state of the entity.""" - return True - - @property - def is_on(self): - """Return true if switch is on.""" - return self._state - - def _handle_code(self, call): - """Check if received code by the pilight-daemon. - - If the code matches the receive on/off codes of this switch the switch - state is changed accordingly. - """ - # - True if off_code/on_code is contained in received code dict, not - # all items have to match. - # - Call turn on/off only once, even if more than one code is received - if any(self._code_on_receive): - for on_code in self._code_on_receive: - if on_code.match(call.data): - on_code.run(switch=self, turn_on=True) - break - - if any(self._code_off_receive): - for off_code in self._code_off_receive: - if off_code.match(call.data): - off_code.run(switch=self, turn_on=False) - break - - def set_state(self, turn_on, send_code=True): - """Set the state of the switch. - - This sets the state of the switch. If send_code is set to True, then - it will call the pilight.send service to actually send the codes - to the pilight daemon. - """ - if send_code: - if turn_on: - self._hass.services.call( - pilight.DOMAIN, pilight.SERVICE_NAME, self._code_on, blocking=True - ) - else: - self._hass.services.call( - pilight.DOMAIN, pilight.SERVICE_NAME, self._code_off, blocking=True - ) - - self._state = turn_on - self.schedule_update_ha_state() - - def turn_on(self, **kwargs): - """Turn the switch on by calling pilight.send service with on code.""" - self.set_state(turn_on=True) - - def turn_off(self, **kwargs): - """Turn the switch on by calling pilight.send service with off code.""" - self.set_state(turn_on=False) diff --git a/homeassistant/components/ping/binary_sensor.py b/homeassistant/components/ping/binary_sensor.py index fe4c12d6738..4d9a99c678e 100644 --- a/homeassistant/components/ping/binary_sensor.py +++ b/homeassistant/components/ping/binary_sensor.py @@ -1,15 +1,15 @@ """Tracks the latency of a host by sending ICMP echo requests (ping).""" -import logging -import subprocess -import re -import sys from datetime import timedelta +import logging +import re +import subprocess +import sys import voluptuous as vol +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.const import CONF_HOST, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ping/device_tracker.py b/homeassistant/components/ping/device_tracker.py index 9bdc38e065b..c4d88f6061c 100644 --- a/homeassistant/components/ping/device_tracker.py +++ b/homeassistant/components/ping/device_tracker.py @@ -1,20 +1,19 @@ """Tracks devices by sending a ICMP echo request (ping).""" +from datetime import timedelta import logging import subprocess import sys -from datetime import timedelta import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant import const, util from homeassistant.components.device_tracker import PLATFORM_SCHEMA from homeassistant.components.device_tracker.const import ( CONF_SCAN_INTERVAL, SCAN_INTERVAL, SOURCE_TYPE_ROUTER, ) -from homeassistant import util -from homeassistant import const +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ping/manifest.json b/homeassistant/components/ping/manifest.json index 19e84365fac..2ec00271fff 100644 --- a/homeassistant/components/ping/manifest.json +++ b/homeassistant/components/ping/manifest.json @@ -1,8 +1,9 @@ { "domain": "ping", - "name": "Ping", + "name": "Ping (ICMP)", "documentation": "https://www.home-assistant.io/integrations/ping", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 51f55d4e851..3e71b54c9fa 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -4,7 +4,7 @@ import telnetlib import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, diff --git a/homeassistant/components/pjlink/manifest.json b/homeassistant/components/pjlink/manifest.json index 3c2b67e3425..a9b4a3bae86 100644 --- a/homeassistant/components/pjlink/manifest.json +++ b/homeassistant/components/pjlink/manifest.json @@ -1,10 +1,8 @@ { "domain": "pjlink", - "name": "Pjlink", + "name": "PJLink", "documentation": "https://www.home-assistant.io/integrations/pjlink", - "requirements": [ - "pypjlink2==1.2.0" - ], + "requirements": ["pypjlink2==1.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index ea35fe7fb75..e93e6e5fb20 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -1,9 +1,11 @@ """Support for controlling projector via the PJLink protocol.""" import logging +from pypjlink import MUTE_AUDIO, Projector +from pypjlink.projector import ProjectorError import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -90,7 +92,6 @@ class PjLinkDevice(MediaPlayerDevice): def projector(self): """Create PJLink Projector instance.""" - from pypjlink import Projector projector = Projector.from_address(self._host, self._port, self._encoding) projector.authenticate(self._password) @@ -98,7 +99,6 @@ class PjLinkDevice(MediaPlayerDevice): def update(self): """Get the latest state from the device.""" - from pypjlink.projector import ProjectorError with self.projector() as projector: try: @@ -171,8 +171,6 @@ class PjLinkDevice(MediaPlayerDevice): def mute_volume(self, mute): """Mute (true) of unmute (false) media player.""" with self.projector() as projector: - from pypjlink import MUTE_AUDIO - projector.set_mute(MUTE_AUDIO, mute) def select_source(self, source): diff --git a/homeassistant/components/plaato/.translations/da.json b/homeassistant/components/plaato/.translations/da.json index 12e95b25e0f..c4dc5ae178d 100644 --- a/homeassistant/components/plaato/.translations/da.json +++ b/homeassistant/components/plaato/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Din Home Assistant instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage meddelelser fra Plaato Airlock.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage meddelelser fra Plaato Airlock.", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "create_entry": { - "default": "For at sende begivenheder til Home Assistant skal du konfigurere webhook funktionen i Plaato Airlock.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n \n Se [dokumentationen]({docs_url}) for yderligere oplysninger." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Plaato Airlock.\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/ko.json b/homeassistant/components/plaato/.translations/ko.json index 50a51dff873..619fdcf736f 100644 --- a/homeassistant/components/plaato/.translations/ko.json +++ b/homeassistant/components/plaato/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Plaato Airlock \uc744 \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Plaato Airlock \uc744 \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Plaato Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/plaato/__init__.py b/homeassistant/components/plaato/__init__.py index 49b749b8de6..0dd57f75812 100644 --- a/homeassistant/components/plaato/__init__.py +++ b/homeassistant/components/plaato/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/plaato/config_flow.py b/homeassistant/components/plaato/config_flow.py index 59cb270c616..3c616c822fb 100644 --- a/homeassistant/components/plaato/config_flow.py +++ b/homeassistant/components/plaato/config_flow.py @@ -1,5 +1,6 @@ """Config flow for GPSLogger.""" from homeassistant.helpers import config_entry_flow + from .const import DOMAIN config_entry_flow.register_webhook_flow( diff --git a/homeassistant/components/plaato/manifest.json b/homeassistant/components/plaato/manifest.json index 658173d3db2..9655b91b13e 100644 --- a/homeassistant/components/plaato/manifest.json +++ b/homeassistant/components/plaato/manifest.json @@ -6,4 +6,4 @@ "dependencies": ["webhook"], "codeowners": ["@JohNan"], "requirements": [] -} \ No newline at end of file +} diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index f8e6a3e9fa7..e7c8033f2ac 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -2,8 +2,10 @@ import logging -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from . import ( diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index a516e06d55b..408c7d1bf36 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -5,7 +5,7 @@ import logging import voluptuous as vol -from homeassistant.components import group +from homeassistant.components.recorder.models import States from homeassistant.components.recorder.util import execute, session_scope from homeassistant.const import ( ATTR_TEMPERATURE, @@ -43,15 +43,15 @@ ATTR_MAX_BRIGHTNESS_HISTORY = "max_brightness" # to have a separate literal for it to avoid confusion. ATTR_DICT_OF_UNITS_OF_MEASUREMENT = "unit_of_measurement_dict" -CONF_MIN_BATTERY_LEVEL = "min_" + READING_BATTERY -CONF_MIN_TEMPERATURE = "min_" + READING_TEMPERATURE -CONF_MAX_TEMPERATURE = "max_" + READING_TEMPERATURE -CONF_MIN_MOISTURE = "min_" + READING_MOISTURE -CONF_MAX_MOISTURE = "max_" + READING_MOISTURE -CONF_MIN_CONDUCTIVITY = "min_" + READING_CONDUCTIVITY -CONF_MAX_CONDUCTIVITY = "max_" + READING_CONDUCTIVITY -CONF_MIN_BRIGHTNESS = "min_" + READING_BRIGHTNESS -CONF_MAX_BRIGHTNESS = "max_" + READING_BRIGHTNESS +CONF_MIN_BATTERY_LEVEL = f"min_{READING_BATTERY}" +CONF_MIN_TEMPERATURE = f"min_{READING_TEMPERATURE}" +CONF_MAX_TEMPERATURE = f"max_{READING_TEMPERATURE}" +CONF_MIN_MOISTURE = f"min_{READING_MOISTURE}" +CONF_MAX_MOISTURE = f"max_{READING_MOISTURE}" +CONF_MIN_CONDUCTIVITY = f"min_{READING_CONDUCTIVITY}" +CONF_MAX_CONDUCTIVITY = f"max_{READING_CONDUCTIVITY}" +CONF_MIN_BRIGHTNESS = f"min_{READING_BRIGHTNESS}" +CONF_MAX_BRIGHTNESS = f"max_{READING_BRIGHTNESS}" CONF_CHECK_DAYS = "check_days" CONF_SENSOR_BATTERY_LEVEL = READING_BATTERY @@ -100,8 +100,6 @@ PLANT_SCHEMA = vol.Schema( ) DOMAIN = "plant" -GROUP_NAME_ALL_PLANTS = "all plants" -ENTITY_ID_ALL_PLANTS = group.ENTITY_ID_FORMAT.format("all_plants") CONFIG_SCHEMA = vol.Schema({DOMAIN: {cv.string: PLANT_SCHEMA}}, extra=vol.ALLOW_EXTRA) @@ -113,7 +111,7 @@ ENABLE_LOAD_HISTORY = False async def async_setup(hass, config): """Set up the Plant component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_PLANTS) + component = EntityComponent(_LOGGER, DOMAIN, hass) entities = [] for plant_name, plant_config in config[DOMAIN].items(): @@ -288,7 +286,6 @@ class Plant(Entity): This only needs to be done once during startup. """ - from homeassistant.components.recorder.models import States start_date = datetime.now() - timedelta(days=self._conf_check_days) entity_id = self._readingmap.get(READING_BRIGHTNESS) diff --git a/homeassistant/components/plant/manifest.json b/homeassistant/components/plant/manifest.json index 721a57e7822..de5f0c1f880 100644 --- a/homeassistant/components/plant/manifest.json +++ b/homeassistant/components/plant/manifest.json @@ -1,13 +1,10 @@ { "domain": "plant", - "name": "Plant", + "name": "Plant Monitor", "documentation": "https://www.home-assistant.io/integrations/plant", "requirements": [], - "dependencies": [ - "group", - "zone" - ], - "codeowners": [ - "@ChristianKuehnel" - ] + "dependencies": ["group", "zone"], + "after_dependencies": ["recorder"], + "codeowners": ["@ChristianKuehnel"], + "quality_scale": "internal" } diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 99d5d4d1685..18dbbb840c3 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -4,8 +4,9 @@ "all_configured": "Alle linkede servere er allerede konfigureret", "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", - "discovery_no_file": "Der blev ikke fundet nogen legacy konfigurationsfil", + "discovery_no_file": "Der blev ikke fundet nogen \u00e6ldre konfigurationsfil", "invalid_import": "Importeret konfiguration er ugyldig", + "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" }, @@ -34,13 +35,13 @@ "title": "V\u00e6lg Plex-server" }, "start_website_auth": { - "description": "Forts\u00e6t for at autorisere p\u00e5 plex.tv.", - "title": "Tilslut Plex-server" + "description": "Forts\u00e6t for at godkende p\u00e5 plex.tv.", + "title": "Forbind Plex-server" }, "user": { "data": { "manual_setup": "Manuel ops\u00e6tning", - "token": "Plex token" + "token": "Plex-token" }, "description": "Indtast et Plex-token til automatisk ops\u00e6tning eller konfigurerer en server manuelt.", "title": "Tilslut Plex-server" @@ -53,7 +54,7 @@ "plex_mp_settings": { "data": { "show_all_controls": "Vis alle kontrolelementer", - "use_episode_art": "Brug episode kunst" + "use_episode_art": "Brug episodekunst" }, "description": "Indstillinger for Plex-medieafspillere" } diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 4b24e6c78a6..5d55a0b6c1e 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex wird konfiguriert", "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden", "invalid_import": "Die importierte Konfiguration ist ung\u00fcltig", + "non-interactive": "Nicht interaktiver Import", "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens", "unknown": "Aus unbekanntem Grund fehlgeschlagen" }, diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 261ca951490..53dd3228288 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex se est\u00e1 configurando", "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "non-interactive": "Importaci\u00f3n no interactiva", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" }, diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index f8e78945802..cf5a7946b9d 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", "discovery_no_file": "\ub808\uac70\uc2dc \uad6c\uc131 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "non-interactive": "\ube44 \ub300\ud654\ud615 \uac00\uc838\uc624\uae30", "token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json index c971ebb4762..515ee8798c7 100644 --- a/homeassistant/components/plex/.translations/nl.json +++ b/homeassistant/components/plex/.translations/nl.json @@ -6,6 +6,7 @@ "already_in_progress": "Plex wordt geconfigureerd", "discovery_no_file": "Geen legacy configuratiebestand gevonden", "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", + "non-interactive": "Niet-interactieve import", "token_request_timeout": "Time-out verkrijgen van token", "unknown": "Mislukt om onbekende reden" }, diff --git a/homeassistant/components/plex/.translations/pt-BR.json b/homeassistant/components/plex/.translations/pt-BR.json index 9a759e309c2..be97c7fdcb7 100644 --- a/homeassistant/components/plex/.translations/pt-BR.json +++ b/homeassistant/components/plex/.translations/pt-BR.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "non-interactive": "Importa\u00e7\u00e3o n\u00e3o interativa" + } + }, "options": { "step": { "plex_mp_settings": { diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 97103230288..89659769192 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -27,10 +27,10 @@ from homeassistant.helpers.dispatcher import ( ) from .const import ( - CONF_USE_EPISODE_ART, - CONF_SHOW_ALL_CONTROLS, CONF_SERVER, CONF_SERVER_IDENTIFIER, + CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 350f1b3d577..d38d13c847e 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -8,12 +8,12 @@ from plexauth import PlexAuth import requests.exceptions import voluptuous as vol -from homeassistant.components.http.view import HomeAssistantView -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries +from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL +from homeassistant.const import CONF_SSL, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import @@ -22,16 +22,16 @@ from .const import ( # pylint: disable=unused-import CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SERVER_IDENTIFIER, - CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, + CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, PLEX_SERVER_CONFIG, X_PLEX_DEVICE_NAME, - X_PLEX_VERSION, - X_PLEX_PRODUCT, X_PLEX_PLATFORM, + X_PLEX_PRODUCT, + X_PLEX_VERSION, ) from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 922a9c14288..a106c230ae4 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -1,17 +1,9 @@ { "domain": "plex", - "name": "Plex", + "name": "Plex Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", - "requirements": [ - "plexapi==3.3.0", - "plexauth==0.0.5", - "plexwebsocket==0.0.6" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@jjlawren" - ] + "requirements": ["plexapi==3.3.0", "plexauth==0.0.5", "plexwebsocket==0.0.6"], + "dependencies": ["http"], + "codeowners": ["@jjlawren"] } diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index ad5fb2f73f1..46b797976ab 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -300,7 +300,7 @@ class PlexMediaPlayer(MediaPlayerDevice): elif self._session_type == "movie": self._media_content_type = MEDIA_TYPE_MOVIE if self.session.year is not None and self._media_title is not None: - self._media_title += " (" + str(self.session.year) + ")" + self._media_title += f" ({self.session.year!s})" elif self._session_type == "track": self._media_content_type = MEDIA_TYPE_MUSIC diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 2a994b08a7b..2aed57946eb 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -86,6 +86,11 @@ class PlexSensor(Entity): """Return the unit this state is expressed in.""" return "Watching" + @property + def icon(self): + """Return the icon of the sensor.""" + return "mdi:plex" + @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py index afde7eae5d6..489e7d3f496 100644 --- a/homeassistant/components/plugwise/__init__.py +++ b/homeassistant/components/plugwise/__init__.py @@ -1 +1 @@ -"""Plugwise Climate (current only Anna) component for HomeAssistant.""" +"""Plugwise Climate (current only Anna) component for Home Assistant.""" diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index fa1ac86941b..67e94c70f5c 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -1,19 +1,18 @@ -"""Plugwise Climate component for HomeAssistant.""" +"""Plugwise Climate component for Home Assistant.""" import logging -import voluptuous as vol import haanna +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_HEAT, CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, - HVAC_MODE_AUTO, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -27,6 +26,7 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index e786b6a7f8e..ccea2a67ead 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -1,8 +1,8 @@ { "domain": "plugwise", - "name": "Plugwise", + "name": "Plugwise Anna", "documentation": "https://www.home-assistant.io/integrations/plugwise", "dependencies": [], - "codeowners": ["@laetificat","@CoMPaTech","@bouwew"], + "codeowners": ["@laetificat", "@CoMPaTech", "@bouwew"], "requirements": ["haanna==0.13.5"] } diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index 8c2da090269..e22f301bf38 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -1,10 +1,8 @@ { "domain": "plum_lightpad", - "name": "Plum lightpad", + "name": "Plum Lightpad", "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", - "requirements": [ - "plumlightpad==0.0.11" - ], + "requirements": ["plumlightpad==0.0.11"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pocketcasts/manifest.json b/homeassistant/components/pocketcasts/manifest.json index f72984f9fc0..c56b9474996 100644 --- a/homeassistant/components/pocketcasts/manifest.json +++ b/homeassistant/components/pocketcasts/manifest.json @@ -1,10 +1,8 @@ { "domain": "pocketcasts", - "name": "Pocketcasts", + "name": "Pocket Casts", "documentation": "https://www.home-assistant.io/integrations/pocketcasts", - "requirements": [ - "pocketcasts==0.1" - ], + "requirements": ["pocketcasts==0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/point/.translations/da.json b/homeassistant/components/point/.translations/da.json index 109bcbe6c37..4b6017ddd01 100644 --- a/homeassistant/components/point/.translations/da.json +++ b/homeassistant/components/point/.translations/da.json @@ -24,7 +24,7 @@ "flow_impl": "Udbyder" }, "description": "V\u00e6lg hvilken godkendelsesudbyder du vil godkende med Point.", - "title": "Godkendelses udbyder" + "title": "Godkendelsesudbyder" } }, "title": "Minut Point" diff --git a/homeassistant/components/point/manifest.json b/homeassistant/components/point/manifest.json index 4c29f37e67c..d68acc4832c 100644 --- a/homeassistant/components/point/manifest.json +++ b/homeassistant/components/point/manifest.json @@ -1,15 +1,10 @@ { "domain": "point", - "name": "Point", + "name": "Minut Point", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/point", - "requirements": [ - "pypoint==1.1.2" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@fredrike" - ] + "requirements": ["pypoint==1.1.2"], + "dependencies": ["webhook", "http"], + "codeowners": ["@fredrike"], + "quality_scale": "gold" } diff --git a/homeassistant/components/postnl/manifest.json b/homeassistant/components/postnl/manifest.json index c45eea0610d..3394d7b867f 100644 --- a/homeassistant/components/postnl/manifest.json +++ b/homeassistant/components/postnl/manifest.json @@ -1,10 +1,8 @@ { "domain": "postnl", - "name": "Postnl", + "name": "PostNL", "documentation": "https://www.home-assistant.io/integrations/postnl", - "requirements": [ - "postnl_api==1.2.2" - ], + "requirements": ["postnl_api==1.2.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/prezzibenzina/manifest.json b/homeassistant/components/prezzibenzina/manifest.json index 99a8a1e2701..2636e36ab14 100644 --- a/homeassistant/components/prezzibenzina/manifest.json +++ b/homeassistant/components/prezzibenzina/manifest.json @@ -1,10 +1,8 @@ { "domain": "prezzibenzina", - "name": "Prezzibenzina", + "name": "Prezzi Benzina", "documentation": "https://www.home-assistant.io/integrations/prezzibenzina", - "requirements": [ - "prezzibenzina-py==1.1.4" - ], + "requirements": ["prezzibenzina-py==1.1.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/proliphix/manifest.json b/homeassistant/components/proliphix/manifest.json index a035657a090..9014d9f8dc2 100644 --- a/homeassistant/components/proliphix/manifest.json +++ b/homeassistant/components/proliphix/manifest.json @@ -2,9 +2,7 @@ "domain": "proliphix", "name": "Proliphix", "documentation": "https://www.home-assistant.io/integrations/proliphix", - "requirements": [ - "proliphix==0.4.1" - ], + "requirements": ["proliphix==0.4.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index 309284e5f62..9632b5b8a43 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -2,11 +2,7 @@ "domain": "prometheus", "name": "Prometheus", "documentation": "https://www.home-assistant.io/integrations/prometheus", - "requirements": [ - "prometheus_client==0.7.1" - ], - "dependencies": [ - "http" - ], + "requirements": ["prometheus_client==0.7.1"], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/prowl/notify.py b/homeassistant/components/prowl/notify.py index ab6d1b498a0..d5167ebfdc9 100644 --- a/homeassistant/components/prowl/notify.py +++ b/homeassistant/components/prowl/notify.py @@ -5,10 +5,6 @@ import logging import async_timeout import voluptuous as vol -from homeassistant.const import CONF_API_KEY -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TITLE, @@ -16,6 +12,9 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://api.prowlapp.com/publicapi/" @@ -60,7 +59,7 @@ class ProwlNotificationService(BaseNotificationService): if response.status != 200 or "error" in result: _LOGGER.error( - "Prowl service returned http " "status %d, response %s", + "Prowl service returned http status %d, response %s", response.status, result, ) diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index 1f86958d08e..7e5f6436757 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -10,7 +10,6 @@ from homeassistant.helpers.event import track_state_change from homeassistant.util.distance import convert from homeassistant.util.location import distance - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -269,7 +268,7 @@ class Proximity(Entity): self.nearest = entity_name self.schedule_update_ha_state() _LOGGER.debug( - "proximity.%s update entity: distance=%s: direction=%s: " "device=%s", + "proximity.%s update entity: distance=%s: direction=%s: device=%s", self.friendly_name, round(dist_to_zone), direction_of_travel, diff --git a/homeassistant/components/proximity/manifest.json b/homeassistant/components/proximity/manifest.json index 708a3fb2616..4d5f42720e0 100644 --- a/homeassistant/components/proximity/manifest.json +++ b/homeassistant/components/proximity/manifest.json @@ -3,9 +3,7 @@ "name": "Proximity", "documentation": "https://www.home-assistant.io/integrations/proximity", "requirements": [], - "dependencies": [ - "device_tracker", - "zone" - ], - "codeowners": [] + "dependencies": ["device_tracker", "zone"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/proxmoxve/manifest.json b/homeassistant/components/proxmoxve/manifest.json index 9c03038a630..4781478eabe 100644 --- a/homeassistant/components/proxmoxve/manifest.json +++ b/homeassistant/components/proxmoxve/manifest.json @@ -1,8 +1,8 @@ { - "domain": "proxmoxve", - "name": "Proxmox VE", - "documentation": "https://www.home-assistant.io/integrations/proxmoxve", - "dependencies": [], - "codeowners": ["@k4ds3"], - "requirements": ["proxmoxer==1.0.3"] - } \ No newline at end of file + "domain": "proxmoxve", + "name": "Proxmox VE", + "documentation": "https://www.home-assistant.io/integrations/proxmoxve", + "dependencies": [], + "codeowners": ["@k4ds3"], + "requirements": ["proxmoxer==1.0.3"] +} diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 5344ea6fe83..06498e51ad3 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -1,6 +1,6 @@ { "domain": "proxy", - "name": "Proxy", + "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", "requirements": ["pillow==6.2.1"], "dependencies": [], diff --git a/homeassistant/components/ps4/.translations/da.json b/homeassistant/components/ps4/.translations/da.json index e9aca23bb43..cef13db3150 100644 --- a/homeassistant/components/ps4/.translations/da.json +++ b/homeassistant/components/ps4/.translations/da.json @@ -3,19 +3,19 @@ "abort": { "credential_error": "Fejl ved hentning af legitimationsoplysninger.", "devices_configured": "Alle de fundne enheder er allerede konfigureret.", - "no_devices_found": "Ingen PlayStation 4 enheder fundet p\u00e5 netv\u00e6rket.", + "no_devices_found": "Der blev ikke fundet nogen PlayStation 4-enheder p\u00e5 netv\u00e6rket.", "port_987_bind_error": "Kunne ikke binde til port 987. Se [dokumentationen](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger.", "port_997_bind_error": "Kunne ikke binde til port 997. Se [dokumentationen](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger." }, "error": { "credential_timeout": "Tjenesten for legitimationsoplysninger fik timeout. Tryk p\u00e5 send for at genstarte.", "login_failed": "Kunne ikke parre med PlayStation 4. Kontroller PIN er korrekt.", - "no_ipaddress": "Indtast IP adressen p\u00e5 den PlayStation 4 du gerne vil konfigurere.", + "no_ipaddress": "Indtast IP-adressen p\u00e5 den PlayStation 4, du gerne vil konfigurere.", "not_ready": "PlayStation 4 er ikke t\u00e6ndt eller tilsluttet til netv\u00e6rket." }, "step": { "creds": { - "description": "Legitimationsoplysninger er n\u00f8dvendige. Tryk p\u00e5 'Send' og derefter i PS4 2nd Screen App, v\u00e6lg opdater enheder og v\u00e6lg 'Home-Assistant' -enheden for at forts\u00e6tte.", + "description": "Der kr\u00e6ves legitimationsoplysninger. Tryk p\u00e5 'Indsend' og derefter i PS4 2. sk\u00e6rm-app, opdater enheder, og v\u00e6lg 'Home Assistant'-enhed for at forts\u00e6tte.", "title": "PlayStation 4" }, "link": { @@ -25,15 +25,15 @@ "name": "Navn", "region": "Omr\u00e5de" }, - "description": "Indtast dine PlayStation 4 oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4 konsol. G\u00e5 derefter til 'Indstillinger for mobilapp-forbindelse' og v\u00e6lg 'Tilf\u00f8j enhed'. Indtast den PIN der vises.", + "description": "Indtast dine PlayStation 4-oplysninger. For 'PIN' skal du navigere til 'Indstillinger' p\u00e5 din PlayStation 4-konsol. Naviger derefter til 'Mobile App Connection Settings' og v\u00e6lg 'Add Device'. Indtast den pinkode, der vises. Se [dokumentation](https://www.home-assistant.io/components/ps4/) for yderligere oplysninger.", "title": "PlayStation 4" }, "mode": { "data": { - "ip_address": "IP adresse (Efterlad tom, hvis du bruger Auto Discovery).", + "ip_address": "IP-adresse (lad det v\u00e6re tomt, hvis du bruger automatisk registrering).", "mode": "Konfigurationstilstand" }, - "description": "V\u00e6lg tilstand til konfiguration. IP-adressefeltet kan st\u00e5 tomt hvis du v\u00e6lger Auto Discovery, da enheder automatisk bliver fundet.", + "description": "V\u00e6lg tilstand for konfiguration. IP-adressefeltet kan v\u00e6re tomt, hvis du v\u00e6lger automatisk registrering, da enheder automatisk bliver fundet.", "title": "PlayStation 4" } }, diff --git a/homeassistant/components/ps4/.translations/ko.json b/homeassistant/components/ps4/.translations/ko.json index 25f64cd21e9..46bbd6b309c 100644 --- a/homeassistant/components/ps4/.translations/ko.json +++ b/homeassistant/components/ps4/.translations/ko.json @@ -2,7 +2,7 @@ "config": { "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.", + "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\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/manifest.json b/homeassistant/components/ps4/manifest.json index add52231a07..7a52a99e08b 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -1,13 +1,9 @@ { "domain": "ps4", - "name": "Ps4", + "name": "Sony PlayStation 4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": [ - "pyps4-2ndscreen==1.0.3" - ], + "requirements": ["pyps4-2ndscreen==1.0.4"], "dependencies": [], - "codeowners": [ - "@ktnrg45" - ] + "codeowners": ["@ktnrg45"] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 35cdbab2534..bea90fa2892 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -376,7 +376,7 @@ class PS4Device(MediaPlayerDevice): self._unique_id = format_unique_id(self._creds, status["host-id"]) async def async_will_remove_from_hass(self): - """Remove Entity from Hass.""" + """Remove Entity from Home Assistant.""" # Close TCP Transport. if self._ps4.connected: await self._ps4.close() diff --git a/homeassistant/components/ptvsd/manifest.json b/homeassistant/components/ptvsd/manifest.json index 2f8398531c7..dd3e6100632 100644 --- a/homeassistant/components/ptvsd/manifest.json +++ b/homeassistant/components/ptvsd/manifest.json @@ -1,10 +1,8 @@ { "domain": "ptvsd", - "name": "ptvsd", + "name": "PTVSD - Python Tools for Visual Studio Debug Server", "documentation": "https://www.home-assistant.io/integrations/ptvsd", - "requirements": [ - "ptvsd==4.2.8" - ], + "requirements": ["ptvsd==4.2.8"], "dependencies": [], "codeowners": ["@swamp-ig"] } diff --git a/homeassistant/components/pulseaudio_loopback/manifest.json b/homeassistant/components/pulseaudio_loopback/manifest.json index 19247e1a7d5..1c1a73ebb3f 100644 --- a/homeassistant/components/pulseaudio_loopback/manifest.json +++ b/homeassistant/components/pulseaudio_loopback/manifest.json @@ -1,6 +1,6 @@ { "domain": "pulseaudio_loopback", - "name": "Pulseaudio loopback", + "name": "PulseAudio Loopback", "documentation": "https://www.home-assistant.io/integrations/pulseaudio_loopback", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/pulseaudio_loopback/switch.py b/homeassistant/components/pulseaudio_loopback/switch.py index 618d54ab3ad..a10c5995d63 100644 --- a/homeassistant/components/pulseaudio_loopback/switch.py +++ b/homeassistant/components/pulseaudio_loopback/switch.py @@ -1,14 +1,14 @@ """Switch logic for loading/unloading pulseaudio loopback modules.""" +from datetime import timedelta import logging import re import socket -from datetime import timedelta import voluptuous as vol from homeassistant import util -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/push/camera.py b/homeassistant/components/push/camera.py index 9d53bd7b033..f78966253b7 100644 --- a/homeassistant/components/push/camera.py +++ b/homeassistant/components/push/camera.py @@ -1,22 +1,22 @@ """Camera platform that receives images through HTTP POST.""" -import logging import asyncio - from collections import deque from datetime import timedelta -import voluptuous as vol +import logging + import aiohttp import async_timeout +import voluptuous as vol from homeassistant.components.camera import ( - Camera, PLATFORM_SCHEMA, STATE_IDLE, STATE_RECORDING, + Camera, ) from homeassistant.components.camera.const import DOMAIN -from homeassistant.core import callback from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/push/manifest.json b/homeassistant/components/push/manifest.json index 161ea29b3b3..c0ad8263c66 100644 --- a/homeassistant/components/push/manifest.json +++ b/homeassistant/components/push/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/push", "requirements": [], "dependencies": ["webhook"], - "codeowners": [ - "@dgomes" - ] + "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/pushbullet/manifest.json b/homeassistant/components/pushbullet/manifest.json index f649e5467a4..bb19a77f175 100644 --- a/homeassistant/components/pushbullet/manifest.json +++ b/homeassistant/components/pushbullet/manifest.json @@ -2,9 +2,7 @@ "domain": "pushbullet", "name": "Pushbullet", "documentation": "https://www.home-assistant.io/integrations/pushbullet", - "requirements": [ - "pushbullet.py==0.11.0" - ], + "requirements": ["pushbullet.py==0.11.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 76c1e14e5a5..28cb08cc69c 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -2,14 +2,9 @@ import logging import mimetypes -from pushbullet import PushBullet -from pushbullet import InvalidKeyError -from pushbullet import PushError +from pushbullet import InvalidKeyError, PushBullet, PushError import voluptuous as vol -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -18,6 +13,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 600b38b6eaf..771ba55586c 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -2,13 +2,11 @@ import logging import threading -from pushbullet import PushBullet -from pushbullet import InvalidKeyError -from pushbullet import Listener +from pushbullet import InvalidKeyError, Listener, PushBullet import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/pushetta/manifest.json b/homeassistant/components/pushetta/manifest.json index 3000f9fd17b..6bcad44958b 100644 --- a/homeassistant/components/pushetta/manifest.json +++ b/homeassistant/components/pushetta/manifest.json @@ -2,9 +2,7 @@ "domain": "pushetta", "name": "Pushetta", "documentation": "https://www.home-assistant.io/integrations/pushetta", - "requirements": [ - "pushetta==1.0.15" - ], + "requirements": ["pushetta==1.0.15"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pushover/manifest.json b/homeassistant/components/pushover/manifest.json index 01c3fc270fb..3428e429b8c 100644 --- a/homeassistant/components/pushover/manifest.json +++ b/homeassistant/components/pushover/manifest.json @@ -2,9 +2,7 @@ "domain": "pushover", "name": "Pushover", "documentation": "https://www.home-assistant.io/integrations/pushover", - "requirements": [ - "python-pushover==0.4" - ], + "requirements": ["python-pushover==0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 3f78897838d..064ad91b6b9 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,12 +1,9 @@ """Pushover platform for notify component.""" import logging +from pushover import Client, InitError, RequestError import requests import voluptuous as vol -from pushover import InitError, Client, RequestError - -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( ATTR_DATA, @@ -16,6 +13,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/pushsafer/notify.py b/homeassistant/components/pushsafer/notify.py index 758a3390286..436191ab864 100644 --- a/homeassistant/components/pushsafer/notify.py +++ b/homeassistant/components/pushsafer/notify.py @@ -7,8 +7,6 @@ import requests from requests.auth import HTTPBasicAuth import voluptuous as vol -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -17,6 +15,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _RESOURCE = "https://www.pushsafer.com/api" diff --git a/homeassistant/components/pvoutput/manifest.json b/homeassistant/components/pvoutput/manifest.json index 7e6879de9a1..1cc1f7aa2f6 100644 --- a/homeassistant/components/pvoutput/manifest.json +++ b/homeassistant/components/pvoutput/manifest.json @@ -1,10 +1,8 @@ { "domain": "pvoutput", - "name": "Pvoutput", + "name": "PVOutput", "documentation": "https://www.home-assistant.io/integrations/pvoutput", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/pvoutput/sensor.py b/homeassistant/components/pvoutput/sensor.py index 90084ab7999..169086af3fc 100644 --- a/homeassistant/components/pvoutput/sensor.py +++ b/homeassistant/components/pvoutput/sensor.py @@ -1,22 +1,22 @@ """Support for getting collected information from PVOutput.""" -import logging from collections import namedtuple from datetime import timedelta +import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.rest.sensor import RestData +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - ATTR_TEMPERATURE, - CONF_API_KEY, - CONF_NAME, ATTR_DATE, + ATTR_TEMPERATURE, ATTR_TIME, ATTR_VOLTAGE, + CONF_API_KEY, + CONF_NAME, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) _ENDPOINT = "http://pvoutput.org/service/r2/getstatus.jsp" diff --git a/homeassistant/components/pyload/manifest.json b/homeassistant/components/pyload/manifest.json index 4cbcd8321b5..98ad640d3ff 100644 --- a/homeassistant/components/pyload/manifest.json +++ b/homeassistant/components/pyload/manifest.json @@ -1,6 +1,6 @@ { "domain": "pyload", - "name": "Pyload", + "name": "pyLoad", "documentation": "https://www.home-assistant.io/integrations/pyload", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index 8ffe1ece4a2..fd4461e3e1b 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -8,14 +8,14 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_SSL, CONF_HOST, + CONF_MONITORED_VARIABLES, CONF_NAME, - CONF_PORT, CONF_PASSWORD, + CONF_PORT, + CONF_SSL, CONF_USERNAME, CONTENT_TYPE_JSON, - CONF_MONITORED_VARIABLES, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index e36ac397c0f..0c5886e177c 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -175,6 +175,7 @@ def execute(hass, filename, source, data=None): builtins["sorted"] = sorted builtins["time"] = TimeWrapper() builtins["dt_util"] = dt_util + logger = logging.getLogger(f"{__name__}.{filename}") restricted_globals = { "__builtins__": builtins, "_print_": StubPrinter, @@ -184,14 +185,15 @@ def execute(hass, filename, source, data=None): "_getitem_": default_guarded_getitem, "_iter_unpack_sequence_": guarded_iter_unpack_sequence, "_unpack_sequence_": guarded_unpack_sequence, + "hass": hass, + "data": data or {}, + "logger": logger, } - logger = logging.getLogger(f"{__name__}.{filename}") - local = {"hass": hass, "data": data or {}, "logger": logger} try: _LOGGER.info("Executing %s: %s", filename, data) # pylint: disable=exec-used - exec(compiled.code, restricted_globals, local) + exec(compiled.code, restricted_globals) except ScriptError as err: logger.error("Error executing script: %s", err) except Exception as err: # pylint: disable=broad-except @@ -223,7 +225,7 @@ class TimeWrapper: if not TimeWrapper.warned: TimeWrapper.warned = True _LOGGER.warning( - "Using time.sleep can reduce the performance of " "Home Assistant" + "Using time.sleep can reduce the performance of Home Assistant" ) time.sleep(*args, **kwargs) diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 2e5c1208210..f16213790c2 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -1,10 +1,9 @@ { "domain": "python_script", - "name": "Python script", + "name": "Python Scripts", "documentation": "https://www.home-assistant.io/integrations/python_script", - "requirements": [ - "restrictedpython==5.0" - ], + "requirements": ["restrictedpython==5.0"], "dependencies": [], - "codeowners": [] -} \ No newline at end of file + "codeowners": [], + "quality_scale": "internal" +} diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index c41d4ba46d3..f1cd556f5d6 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -1,10 +1,8 @@ { "domain": "qbittorrent", - "name": "Qbittorrent", + "name": "qBittorrent", "documentation": "https://www.home-assistant.io/integrations/qbittorrent", - "requirements": [ - "python-qbittorrent==0.3.1" - ], + "requirements": ["python-qbittorrent==0.4.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 0299277059b..9544d74b1cd 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -107,7 +107,7 @@ class QBittorrentSensor(Entity): def update(self): """Get the latest data from qBittorrent and updates the state.""" try: - data = self.client.sync() + data = self.client.sync_main_data() self._available = True except RequestException: _LOGGER.error("Connection lost") diff --git a/homeassistant/components/qld_bushfire/manifest.json b/homeassistant/components/qld_bushfire/manifest.json index c113e6034cd..fb05d1638c3 100644 --- a/homeassistant/components/qld_bushfire/manifest.json +++ b/homeassistant/components/qld_bushfire/manifest.json @@ -2,11 +2,7 @@ "domain": "qld_bushfire", "name": "Queensland Bushfire Alert", "documentation": "https://www.home-assistant.io/integrations/qld_bushfire", - "requirements": [ - "georss_qld_bushfire_alert_client==0.3" - ], + "requirements": ["georss_qld_bushfire_alert_client==0.3"], "dependencies": [], - "codeowners": [ - "@exxamalte" - ] -} \ No newline at end of file + "codeowners": ["@exxamalte"] +} diff --git a/homeassistant/components/qnap/manifest.json b/homeassistant/components/qnap/manifest.json index a34c49645ce..6720120b5e2 100644 --- a/homeassistant/components/qnap/manifest.json +++ b/homeassistant/components/qnap/manifest.json @@ -1,12 +1,8 @@ { "domain": "qnap", - "name": "Qnap", + "name": "QNAP", "documentation": "https://www.home-assistant.io/integrations/qnap", - "requirements": [ - "qnapstats==0.2.7" - ], + "requirements": ["qnapstats==0.2.7"], "dependencies": [], - "codeowners": [ - "@colinodell" - ] + "codeowners": ["@colinodell"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 1f8caa3753a..6ea6db621fb 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -1,11 +1,8 @@ { "domain": "qrcode", - "name": "Qrcode", + "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": [ - "pillow==6.2.1", - "pyzbar==0.1.7" - ], + "requirements": ["pillow==6.2.1", "pyzbar==0.1.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index 97eb8eedfd3..58151fa02ce 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -54,7 +54,7 @@ class QuantumGatewayDeviceScanner(DeviceScanner): _LOGGER.error("Unable to connect to gateway. Check host.") if not self.success_init: - _LOGGER.error("Unable to login to gateway. Check password and " "host.") + _LOGGER.error("Unable to login to gateway. Check password and host.") def scan_devices(self): """Scan for new devices and return a list of found MACs.""" diff --git a/homeassistant/components/quantum_gateway/manifest.json b/homeassistant/components/quantum_gateway/manifest.json index da2fc20510f..ca930dc9ca4 100644 --- a/homeassistant/components/quantum_gateway/manifest.json +++ b/homeassistant/components/quantum_gateway/manifest.json @@ -1,12 +1,8 @@ { "domain": "quantum_gateway", - "name": "Quantum gateway", + "name": "Quantum Gateway", "documentation": "https://www.home-assistant.io/integrations/quantum_gateway", - "requirements": [ - "quantum-gateway==0.0.5" - ], + "requirements": ["quantum-gateway==0.0.5"], "dependencies": [], - "codeowners": [ - "@cisasteelersfan" - ] + "codeowners": ["@cisasteelersfan"] } diff --git a/homeassistant/components/qwikswitch/manifest.json b/homeassistant/components/qwikswitch/manifest.json index 6d2944282ba..836bc611b83 100644 --- a/homeassistant/components/qwikswitch/manifest.json +++ b/homeassistant/components/qwikswitch/manifest.json @@ -1,12 +1,8 @@ { "domain": "qwikswitch", - "name": "Qwikswitch", + "name": "QwikSwitch QSUSB", "documentation": "https://www.home-assistant.io/integrations/qwikswitch", - "requirements": [ - "pyqwikswitch==0.93" - ], + "requirements": ["pyqwikswitch==0.93"], "dependencies": [], - "codeowners": [ - "@kellerza" - ] + "codeowners": ["@kellerza"] } diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 4d67175ecd5..1b24f4e0071 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -1,13 +1,13 @@ """Integration with the Rachio Iro sprinkler system controller.""" import asyncio import logging +import secrets from typing import Optional from aiohttp import web from rachiopy import Rachio import voluptuous as vol -from homeassistant.auth.util import generate_secret from homeassistant.components.http import HomeAssistantView from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API from homeassistant.helpers import config_validation as cv, discovery @@ -115,7 +115,7 @@ def setup(hass, config) -> bool: # Get the URL of this server custom_url = config[DOMAIN].get(CONF_CUSTOM_URL) hass_url = hass.config.api.base_url if custom_url is None else custom_url - rachio.webhook_auth = generate_secret() + rachio.webhook_auth = secrets.token_hex() rachio.webhook_url = hass_url + WEBHOOK_PATH # Get the API user diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 79e3677d65e..fae640f9262 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -2,9 +2,7 @@ "domain": "rachio", "name": "Rachio", "documentation": "https://www.home-assistant.io/integrations/rachio", - "requirements": [ - "rachiopy==0.1.3" - ], - "dependencies": [], + "requirements": ["rachiopy==0.1.3"], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index a007dd673ac..d6bc3d6d579 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,32 +1,32 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" import logging -import voluptuous as vol import radiotherm +import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_OFF, + FAN_ON, HVAC_MODE_AUTO, 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, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, PRECISION_HALVES, - TEMP_FAHRENHEIT, STATE_ON, + TEMP_FAHRENHEIT, ) -from homeassistant.util import dt as dt_util import homeassistant.helpers.config_validation as cv +from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -40,9 +40,9 @@ OPERATION_LIST = [HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] CT30_FAN_OPERATION_LIST = [STATE_ON, HVAC_MODE_AUTO] CT80_FAN_OPERATION_LIST = [STATE_ON, STATE_CIRCULATE, HVAC_MODE_AUTO] -# Mappings from radiotherm json data codes to and from HASS state +# Mappings from radiotherm json data codes to and from Home Assistant state # flags. CODE is the thermostat integer code and these map to and -# from HASS state flags. +# from Home Assistant state flags. # Programmed temperature mode of the thermostat. CODE_TO_TEMP_MODE = { @@ -220,7 +220,7 @@ class RadioThermostat(ClimateDevice): """Update and validate the data from the thermostat.""" # Radio thermostats are very slow, and sometimes don't respond # very quickly. So we need to keep the number of calls to them - # to a bare minimum or we'll hit the HASS 10 sec warning. We + # to a bare minimum or we'll hit the Home Assistant 10 sec warning. We # have to make one call to /tstat to get temps but we'll try and # keep the other calls to a minimum. Even with this, these # thermostats tend to time out sometimes when they're actively diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index c3f8079cccd..34aebee3e1d 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -1,10 +1,8 @@ { "domain": "radiotherm", - "name": "Radiotherm", + "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", - "requirements": [ - "radiotherm==2.0.0" - ], + "requirements": ["radiotherm==2.0.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index bb421c725ca..4705ba30298 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -1,12 +1,8 @@ { "domain": "rainbird", - "name": "Rainbird", + "name": "Rain Bird", "documentation": "https://www.home-assistant.io/integrations/rainbird", - "requirements": [ - "pyrainbird==0.4.1" - ], + "requirements": ["pyrainbird==0.4.1"], "dependencies": [], - "codeowners": [ - "@konikvranik" - ] + "codeowners": ["@konikvranik"] } diff --git a/homeassistant/components/raincloud/manifest.json b/homeassistant/components/raincloud/manifest.json index 612fc32c014..79d197969d4 100644 --- a/homeassistant/components/raincloud/manifest.json +++ b/homeassistant/components/raincloud/manifest.json @@ -1,10 +1,8 @@ { "domain": "raincloud", - "name": "Raincloud", + "name": "Melnor RainCloud", "documentation": "https://www.home-assistant.io/integrations/raincloud", - "requirements": [ - "raincloudy==0.0.7" - ], + "requirements": ["raincloudy==0.0.7"], "dependencies": [], "codeowners": ["@vanstinator"] } diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index c5503976d3f..59e4947f9bb 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -2,9 +2,7 @@ "domain": "rainforest_eagle", "name": "Rainforest Eagle-200", "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", - "requirements": [ - "eagle200_reader==0.2.1" - ], + "requirements": ["eagle200_reader==0.2.1"], "dependencies": [], "codeowners": ["@gtdiehl"] } diff --git a/homeassistant/components/rainmachine/.translations/da.json b/homeassistant/components/rainmachine/.translations/da.json index 61d29894fe2..34f4fff4ed0 100644 --- a/homeassistant/components/rainmachine/.translations/da.json +++ b/homeassistant/components/rainmachine/.translations/da.json @@ -8,7 +8,7 @@ "user": { "data": { "ip_address": "V\u00e6rtsnavn eller IP-adresse", - "password": "Password", + "password": "Adgangskode", "port": "Port" }, "title": "Udfyld dine oplysninger" diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 2f7e6e005d4..1fe98482211 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -76,7 +76,7 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): @property def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return "{0}_{1}".format( self.rainmachine.device_mac.replace(":", ""), self._sensor_type ) @@ -96,7 +96,9 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): async def async_update(self): """Update the state.""" if self._sensor_type == TYPE_FLOW_SENSOR: - self._state = self.rainmachine.data[PROVISION_SETTINGS].get("useFlowSensor") + self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( + "useFlowSensor" + ) elif self._sensor_type == TYPE_FREEZE: self._state = self.rainmachine.data[RESTRICTIONS_CURRENT]["freeze"] elif self._sensor_type == TYPE_FREEZE_PROTECTION: diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index 37db2b31355..2c8a770137d 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -1,13 +1,9 @@ { "domain": "rainmachine", - "name": "Rainmachine", + "name": "RainMachine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rainmachine", - "requirements": [ - "regenmaschine==1.5.1" - ], + "requirements": ["regenmaschine==1.5.1"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 8433ca9630f..399e86b7db1 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -72,7 +72,7 @@ class RainMachineSensor(RainMachineEntity): @property def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return "{0}_{1}".format( self.rainmachine.device_mac.replace(":", ""), self._sensor_type ) @@ -97,14 +97,14 @@ class RainMachineSensor(RainMachineEntity): async def async_update(self): """Update the sensor's state.""" if self._sensor_type == TYPE_FLOW_SENSOR_CLICK_M3: - self._state = self.rainmachine.data[PROVISION_SETTINGS].get( + self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( "flowSensorClicksPerCubicMeter" ) elif self._sensor_type == TYPE_FLOW_SENSOR_CONSUMED_LITERS: - clicks = self.rainmachine.data[PROVISION_SETTINGS].get( + clicks = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( "flowSensorWateringClicks" ) - clicks_per_m3 = self.rainmachine.data[PROVISION_SETTINGS].get( + clicks_per_m3 = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( "flowSensorClicksPerCubicMeter" ) @@ -113,11 +113,11 @@ class RainMachineSensor(RainMachineEntity): else: self._state = None elif self._sensor_type == TYPE_FLOW_SENSOR_START_INDEX: - self._state = self.rainmachine.data[PROVISION_SETTINGS].get( + self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( "flowSensorStartIndex" ) elif self._sensor_type == TYPE_FLOW_SENSOR_WATERING_CLICKS: - self._state = self.rainmachine.data[PROVISION_SETTINGS].get( + self._state = self.rainmachine.data[PROVISION_SETTINGS]["system"].get( "flowSensorWateringClicks" ) elif self._sensor_type == TYPE_FREEZE_TEMP: diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 69c0e4da52d..36c5eefb3d6 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -143,7 +143,7 @@ class RainMachineSwitch(RainMachineEntity, SwitchDevice): @property def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return "{0}_{1}_{2}".format( self.rainmachine.device_mac.replace(":", ""), self._switch_type, diff --git a/homeassistant/components/random/manifest.json b/homeassistant/components/random/manifest.json index cbbaa562abd..bd265ef4759 100644 --- a/homeassistant/components/random/manifest.json +++ b/homeassistant/components/random/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/random", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 8b7ea0a38d7..fb544d3ebcc 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -50,7 +50,7 @@ def log_message(source, *parts): """Build log message.""" message = source.__class__.__name__ for part in parts: - message += ": " + str(part) + message += f": {part!s}" return message diff --git a/homeassistant/components/raspihats/manifest.json b/homeassistant/components/raspihats/manifest.json index b241cd6db15..f26da599cef 100644 --- a/homeassistant/components/raspihats/manifest.json +++ b/homeassistant/components/raspihats/manifest.json @@ -2,10 +2,7 @@ "domain": "raspihats", "name": "Raspihats", "documentation": "https://www.home-assistant.io/integrations/raspihats", - "requirements": [ - "raspihats==2.2.3", - "smbus-cffi==0.5.1" - ], + "requirements": ["raspihats==2.2.3", "smbus-cffi==0.5.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/raspihats/switch.py b/homeassistant/components/raspihats/switch.py index b99a84bdae9..8a083dbe2c9 100644 --- a/homeassistant/components/raspihats/switch.py +++ b/homeassistant/components/raspihats/switch.py @@ -127,7 +127,7 @@ class I2CHatSwitch(ToggleEntity): state = self.I2C_HATS_MANAGER.read_dq(self._address, self._channel) return state != self._invert_logic except I2CHatsException as ex: - _LOGGER.error(self._log_message("Is ON check failed, " + str(ex))) + _LOGGER.error(self._log_message(f"Is ON check failed, {ex!s}")) return False def turn_on(self, **kwargs): @@ -137,7 +137,7 @@ class I2CHatSwitch(ToggleEntity): self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) self.schedule_update_ha_state() except I2CHatsException as ex: - _LOGGER.error(self._log_message("Turn ON failed, " + str(ex))) + _LOGGER.error(self._log_message(f"Turn ON failed, {ex!s}")) def turn_off(self, **kwargs): """Turn the device off.""" @@ -146,4 +146,4 @@ class I2CHatSwitch(ToggleEntity): self.I2C_HATS_MANAGER.write_dq(self._address, self._channel, state) self.schedule_update_ha_state() except I2CHatsException as ex: - _LOGGER.error(self._log_message("Turn OFF failed:, " + str(ex))) + _LOGGER.error(self._log_message(f"Turn OFF failed, {ex!s}")) diff --git a/homeassistant/components/raspyrfm/manifest.json b/homeassistant/components/raspyrfm/manifest.json index f1330c2abe5..20e3768705e 100644 --- a/homeassistant/components/raspyrfm/manifest.json +++ b/homeassistant/components/raspyrfm/manifest.json @@ -1,10 +1,8 @@ { "domain": "raspyrfm", - "name": "Raspyrfm", + "name": "RaspyRFM", "documentation": "https://www.home-assistant.io/integrations/raspyrfm", - "requirements": [ - "raspyrfm-client==1.2.8" - ], + "requirements": ["raspyrfm-client==1.2.8"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/recollect_waste/manifest.json b/homeassistant/components/recollect_waste/manifest.json index 396afe30dcf..634f6ad0c61 100644 --- a/homeassistant/components/recollect_waste/manifest.json +++ b/homeassistant/components/recollect_waste/manifest.json @@ -1,10 +1,8 @@ { "domain": "recollect_waste", - "name": "Recollect waste", + "name": "ReCollect Waste", "documentation": "https://www.home-assistant.io/integrations/recollect_waste", - "requirements": [ - "recollect-waste==1.0.1" - ], + "requirements": ["recollect-waste==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 10b1d04304f..ab56a5fc33b 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -5,18 +5,19 @@ import concurrent.futures from datetime import datetime, timedelta import logging import queue +from sqlite3 import Connection import threading import time from typing import Any, Dict, Optional -from sqlite3 import Connection -import voluptuous as vol -from sqlalchemy import exc, create_engine +from sqlalchemy import create_engine, exc from sqlalchemy.engine import Engine from sqlalchemy.event import listens_for from sqlalchemy.orm import scoped_session, sessionmaker from sqlalchemy.pool import StaticPool +import voluptuous as vol +from homeassistant.components import persistent_notification from homeassistant.const import ( ATTR_ENTITY_ID, CONF_DOMAINS, @@ -29,7 +30,6 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, ) -from homeassistant.components import persistent_notification from homeassistant.core import CoreState, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter @@ -228,7 +228,7 @@ class Recorder(threading.Thread): _LOGGER.debug("Connected to recorder database") except Exception as err: # pylint: disable=broad-except _LOGGER.error( - "Error during connection setup: %s (retrying " "in %s seconds)", + "Error during connection setup: %s (retrying in %s seconds)", err, CONNECT_RETRY_WAIT, ) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 4ad000866db..4fd727ad450 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,9 +2,8 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": [ - "sqlalchemy==1.3.11" - ], + "requirements": ["sqlalchemy==1.3.12"], "dependencies": [], - "codeowners": [] -} \ No newline at end of file + "codeowners": [], + "quality_scale": "internal" +} diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 33a01ea1ac0..3a5ef2729be 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -6,7 +6,7 @@ from sqlalchemy import Table, text from sqlalchemy.engine import reflection from sqlalchemy.exc import OperationalError, SQLAlchemyError -from .models import SchemaChanges, SCHEMA_VERSION, Base +from .models import SCHEMA_VERSION, Base, SchemaChanges from .util import session_scope _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index b512bfc8204..f3e80a9a739 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -1,6 +1,6 @@ """Models for SQLAlchemy.""" -import json from datetime import datetime +import json import logging from sqlalchemy import ( @@ -17,9 +17,9 @@ from sqlalchemy import ( from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.session import Session -import homeassistant.util.dt as dt_util from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util # SQLAlchemy Schema # pylint: disable=invalid-name diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 089476245fe..b4b1f612fac 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -5,8 +5,8 @@ import logging from sqlalchemy.exc import SQLAlchemyError import homeassistant.util.dt as dt_util -from .models import Events, States +from .models import Events, States from .util import session_scope _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/recswitch/manifest.json b/homeassistant/components/recswitch/manifest.json index b11eca2b088..85765815619 100644 --- a/homeassistant/components/recswitch/manifest.json +++ b/homeassistant/components/recswitch/manifest.json @@ -1,10 +1,8 @@ { "domain": "recswitch", - "name": "Recswitch", + "name": "Ankuoo REC Switch", "documentation": "https://www.home-assistant.io/integrations/recswitch", - "requirements": [ - "pyrecswitch==1.0.2" - ], + "requirements": ["pyrecswitch==1.0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/reddit/manifest.json b/homeassistant/components/reddit/manifest.json index 8ab4b686da7..cb1cd032614 100644 --- a/homeassistant/components/reddit/manifest.json +++ b/homeassistant/components/reddit/manifest.json @@ -2,9 +2,7 @@ "domain": "reddit", "name": "Reddit", "documentation": "https://www.home-assistant.io/integrations/reddit", - "requirements": [ - "praw==6.4.0" - ], + "requirements": ["praw==6.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/reddit/sensor.py b/homeassistant/components/reddit/sensor.py index 82f622b968e..ed24dfe47df 100644 --- a/homeassistant/components/reddit/sensor.py +++ b/homeassistant/components/reddit/sensor.py @@ -6,7 +6,7 @@ import praw import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_MAXIMUM +from homeassistant.const import CONF_MAXIMUM, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index f1f82999188..b5140110570 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -2,9 +2,7 @@ "domain": "rejseplanen", "name": "Rejseplanen", "documentation": "https://www.home-assistant.io/integrations/rejseplanen", - "requirements": [ - "rjpl==0.3.5" - ], + "requirements": ["rjpl==0.3.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index fdfbdfd5cdc..ec70c9d4329 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -17,7 +17,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "remember_the_milk" DEFAULT_NAME = DOMAIN -GROUP_NAME_RTM = "remember the milk accounts" CONF_SHARED_SECRET = "shared_secret" CONF_ID_MAP = "id_map" @@ -50,7 +49,7 @@ SERVICE_SCHEMA_COMPLETE_TASK = vol.Schema({vol.Required(CONF_ID): cv.string}) def setup(hass, config): """Set up the Remember the milk component.""" - component = EntityComponent(_LOGGER, DOMAIN, hass, group_name=GROUP_NAME_RTM) + component = EntityComponent(_LOGGER, DOMAIN, hass) stored_rtm_config = RememberTheMilkConfiguration(hass) for rtm_config in config[DOMAIN]: @@ -166,7 +165,7 @@ class RememberTheMilkConfiguration: self._config = json.load(config_file) except ValueError: _LOGGER.error( - "Failed to load configuration file, creating a " "new one: %s", + "Failed to load configuration file, creating a new one: %s", self._config_file_path, ) self._config = dict() @@ -258,7 +257,7 @@ class RememberTheMilk(Entity): valid = self._rtm_api.token_valid() if not valid: _LOGGER.error( - "Token for account %s is invalid. You need to " "register again!", + "Token for account %s is invalid. You need to register again!", self.name, ) self._rtm_config.delete_token(self._name) @@ -306,14 +305,14 @@ class RememberTheMilk(Entity): timeline=timeline, ) _LOGGER.debug( - "Updated task with id '%s' in account " "%s to name %s", + "Updated task with id '%s' in account %s to name %s", hass_id, self.name, task_name, ) except RtmRequestFailedException as rtm_exception: _LOGGER.error( - "Error creating new Remember The Milk task for " "account %s: %s", + "Error creating new Remember The Milk task for account %s: %s", self._name, rtm_exception, ) @@ -347,7 +346,7 @@ class RememberTheMilk(Entity): ) except RtmRequestFailedException as rtm_exception: _LOGGER.error( - "Error creating new Remember The Milk task for " "account %s: %s", + "Error creating new Remember The Milk task for account %s: %s", self._name, rtm_exception, ) diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 6ec9dc6f8f4..cbbc385557c 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -1,11 +1,8 @@ { "domain": "remember_the_milk", - "name": "Remember the milk", + "name": "Remember The Milk", "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", - "requirements": [ - "RtmAPI==0.7.2", - "httplib2==0.10.3" - ], + "requirements": ["RtmAPI==0.7.2", "httplib2==0.10.3"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index af653165ee3..2abd5844001 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -5,23 +5,21 @@ import logging import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - STATE_ON, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, ) -from homeassistant.components import group +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) - +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs @@ -39,11 +37,8 @@ ATTR_TIMEOUT = "timeout" DOMAIN = "remote" SCAN_INTERVAL = timedelta(seconds=30) -ENTITY_ID_ALL_REMOTES = group.ENTITY_ID_FORMAT.format("all_remotes") ENTITY_ID_FORMAT = DOMAIN + ".{}" -GROUP_NAME_ALL_REMOTES = "all remotes" - MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) SERVICE_SEND_COMMAND = "send_command" @@ -62,17 +57,14 @@ REMOTE_SERVICE_ACTIVITY_SCHEMA = make_entity_service_schema( @bind_hass -def is_on(hass, entity_id=None): +def is_on(hass, entity_id): """Return if the remote is on based on the statemachine.""" - entity_id = entity_id or ENTITY_ID_ALL_REMOTES return hass.states.is_state(entity_id, STATE_ON) async def async_setup(hass, config): """Track states and offer events for remotes.""" - component = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_REMOTES - ) + component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) component.async_register_entity_service( diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index 5b9e70ebcf0..24616bc5947 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -3,8 +3,6 @@ "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", "requirements": [], - "dependencies": [ - "group" - ], + "dependencies": ["group"], "codeowners": [] } diff --git a/homeassistant/components/remote_rpi_gpio/manifest.json b/homeassistant/components/remote_rpi_gpio/manifest.json index c3b93346916..4c5f63c7dea 100644 --- a/homeassistant/components/remote_rpi_gpio/manifest.json +++ b/homeassistant/components/remote_rpi_gpio/manifest.json @@ -1,10 +1,8 @@ { - "domain": "remote_rpi_gpio", - "name": "remote_rpi_gpio", - "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", - "requirements": [ - "gpiozero==1.5.1" - ], - "dependencies": [], - "codeowners": [] + "domain": "remote_rpi_gpio", + "name": "remote_rpi_gpio", + "documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio", + "requirements": ["gpiozero==1.5.1"], + "dependencies": [], + "codeowners": [] } diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json index a894285f729..d24a8ed03e3 100644 --- a/homeassistant/components/repetier/manifest.json +++ b/homeassistant/components/repetier/manifest.json @@ -1,10 +1,8 @@ { "domain": "repetier", - "name": "Repetier Server", + "name": "Repetier-Server", "documentation": "https://www.home-assistant.io/integrations/repetier", - "requirements": [ - "pyrepetier==3.0.5" - ], + "requirements": ["pyrepetier==3.0.5"], "dependencies": [], "codeowners": ["@MTrab"] } diff --git a/homeassistant/components/rest/manifest.json b/homeassistant/components/rest/manifest.json index 0b197d104ed..8c8b7f39609 100644 --- a/homeassistant/components/rest/manifest.json +++ b/homeassistant/components/rest/manifest.json @@ -1,6 +1,6 @@ { "domain": "rest", - "name": "Rest", + "name": "RESTful", "documentation": "https://www.home-assistant.io/integrations/rest", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/rest/notify.py b/homeassistant/components/rest/notify.py index f2bfcf3aba7..4f3de14b731 100644 --- a/homeassistant/components/rest/notify.py +++ b/homeassistant/components/rest/notify.py @@ -4,6 +4,14 @@ import logging import requests import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_AUTHENTICATION, CONF_HEADERS, @@ -18,15 +26,6 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import ( - ATTR_TARGET, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - ATTR_MESSAGE, - PLATFORM_SCHEMA, - BaseNotificationService, -) - CONF_DATA = "data" CONF_DATA_TEMPLATE = "data_template" CONF_MESSAGE_PARAMETER_NAME = "message_param_name" diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 6fdf5ce7221..51120cb350c 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -1,34 +1,34 @@ """Support for RESTful API sensors.""" -import logging import json +import logging -import voluptuous as vol import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, DEVICE_CLASSES_SCHEMA +from homeassistant.components.sensor import DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA from homeassistant.const import ( CONF_AUTHENTICATION, + CONF_DEVICE_CLASS, CONF_FORCE_UPDATE, CONF_HEADERS, - CONF_NAME, CONF_METHOD, + CONF_NAME, CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, CONF_RESOURCE_TEMPLATE, + CONF_TIMEOUT, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, - CONF_TIMEOUT, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, - CONF_DEVICE_CLASS, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rest/switch.py b/homeassistant/components/rest/switch.py index a02a8507194..fe409a46be7 100644 --- a/homeassistant/components/rest/switch.py +++ b/homeassistant/components/rest/switch.py @@ -6,15 +6,15 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( CONF_HEADERS, + CONF_METHOD, CONF_NAME, + CONF_PASSWORD, CONF_RESOURCE, CONF_TIMEOUT, - CONF_METHOD, CONF_USERNAME, - CONF_PASSWORD, CONF_VERIFY_SSL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/homeassistant/components/rest_command/manifest.json b/homeassistant/components/rest_command/manifest.json index 238191a9343..9611eea23c0 100644 --- a/homeassistant/components/rest_command/manifest.json +++ b/homeassistant/components/rest_command/manifest.json @@ -1,6 +1,6 @@ { "domain": "rest_command", - "name": "Rest command", + "name": "RESTful Command", "documentation": "https://www.home-assistant.io/integrations/rest_command", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index b3e1d2b16b7..2e5875b9d08 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -19,7 +19,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import get_deprecated from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -33,12 +32,9 @@ ATTR_EVENT = "event" ATTR_STATE = "state" CONF_ALIASES = "aliases" -CONF_ALIASSES = "aliasses" CONF_GROUP_ALIASES = "group_aliases" -CONF_GROUP_ALIASSES = "group_aliasses" CONF_GROUP = "group" CONF_NOGROUP_ALIASES = "nogroup_aliases" -CONF_NOGROUP_ALIASSES = "nogroup_aliasses" CONF_DEVICE_DEFAULTS = "device_defaults" CONF_DEVICE_ID = "device_id" CONF_DEVICES = "devices" @@ -563,18 +559,3 @@ class SwitchableRflinkDevice(RflinkCommand, RestoreEntity): def async_turn_off(self, **kwargs): """Turn the device off.""" return self._async_handle_command("turn_off") - - -DEPRECATED_CONFIG_OPTIONS = [CONF_ALIASSES, CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASSES] -REPLACEMENT_CONFIG_OPTIONS = [CONF_ALIASES, CONF_GROUP_ALIASES, CONF_NOGROUP_ALIASES] - - -def remove_deprecated(config): - """Remove deprecated config options from device config.""" - for index, deprecated_option in enumerate(DEPRECATED_CONFIG_OPTIONS): - if deprecated_option in config: - replacement_option = REPLACEMENT_CONFIG_OPTIONS[index] - # generate deprecation warning - get_deprecated(config, replacement_option, deprecated_option) - # remove old config value replacing new one - config[replacement_option] = config.pop(deprecated_option) diff --git a/homeassistant/components/rflink/light.py b/homeassistant/components/rflink/light.py index 682d45f8f42..db616b92fc4 100644 --- a/homeassistant/components/rflink/light.py +++ b/homeassistant/components/rflink/light.py @@ -14,23 +14,19 @@ import homeassistant.helpers.config_validation as cv from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, - CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, - CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DATA_DEVICE_REGISTER, DEVICE_DEFAULTS_SCHEMA, EVENT_KEY_COMMAND, EVENT_KEY_ID, SwitchableRflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -65,14 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FIRE_EVENT): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), vol.Optional(CONF_GROUP, default=True): cv.boolean, - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_GROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_NOGROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), } ) }, @@ -131,7 +119,6 @@ def devices_from_config(domain_config): entity_class = entity_class_for_type(entity_type) device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - remove_deprecated(device_config) is_hybrid = entity_class is HybridRflinkLight diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json index bda260bdff2..28aea1adc31 100644 --- a/homeassistant/components/rflink/manifest.json +++ b/homeassistant/components/rflink/manifest.json @@ -1,10 +1,8 @@ { "domain": "rflink", - "name": "Rflink", + "name": "RFLink", "documentation": "https://www.home-assistant.io/integrations/rflink", - "requirements": [ - "rflink==0.0.46" - ], + "requirements": ["rflink==0.0.50"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index aa0ef4f9c62..bc736a1ede6 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -15,7 +15,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_AUTOMATIC_ADD, CONF_DEVICES, DATA_DEVICE_REGISTER, @@ -27,7 +26,6 @@ from . import ( SIGNAL_HANDLE_EVENT, TMP_ENTITY, RflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -52,8 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_ALIASES, default=[]): vol.All( cv.ensure_list, [cv.string] ), - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), } ) }, @@ -80,7 +76,6 @@ def devices_from_config(domain_config): config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type( config[CONF_SENSOR_TYPE] ) - remove_deprecated(config) device = RflinkSensor(device_id, **config) devices.append(device) diff --git a/homeassistant/components/rflink/switch.py b/homeassistant/components/rflink/switch.py index c9173acc1a5..8e0ce9a0c8e 100644 --- a/homeassistant/components/rflink/switch.py +++ b/homeassistant/components/rflink/switch.py @@ -9,19 +9,15 @@ import homeassistant.helpers.config_validation as cv from . import ( CONF_ALIASES, - CONF_ALIASSES, CONF_DEVICE_DEFAULTS, CONF_DEVICES, CONF_FIRE_EVENT, CONF_GROUP, CONF_GROUP_ALIASES, - CONF_GROUP_ALIASSES, CONF_NOGROUP_ALIASES, - CONF_NOGROUP_ALIASSES, CONF_SIGNAL_REPETITIONS, DEVICE_DEFAULTS_SCHEMA, SwitchableRflinkDevice, - remove_deprecated, ) _LOGGER = logging.getLogger(__name__) @@ -47,14 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FIRE_EVENT): cv.boolean, vol.Optional(CONF_SIGNAL_REPETITIONS): vol.Coerce(int), vol.Optional(CONF_GROUP, default=True): cv.boolean, - # deprecated config options - vol.Optional(CONF_ALIASSES): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_GROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_NOGROUP_ALIASSES): vol.All( - cv.ensure_list, [cv.string] - ), } ) }, @@ -68,7 +56,6 @@ def devices_from_config(domain_config): devices = [] for device_id, config in domain_config[CONF_DEVICES].items(): device_config = dict(domain_config[CONF_DEVICE_DEFAULTS], **config) - remove_deprecated(device_config) device = RflinkSwitch(device_id, **device_config) devices.append(device) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 6465dc36326..a88594dccea 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -4,7 +4,6 @@ import logging import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, @@ -26,6 +25,14 @@ from . import ( CONF_DEVICES, CONF_FIRE_EVENT, CONF_OFF_DELAY, + RECEIVED_EVT_SUBSCRIBERS, + RFX_DEVICES, + apply_received_command, + find_possible_pt2262_device, + get_pt2262_cmd, + get_pt2262_device, + get_pt2262_deviceid, + get_rfx_object, ) _LOGGER = logging.getLogger(__name__) @@ -58,16 +65,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] for packet_id, entity in config[CONF_DEVICES].items(): - event = rfxtrx.get_rfx_object(packet_id) + event = get_rfx_object(packet_id) device_id = slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: + if device_id in RFX_DEVICES: continue if entity.get(CONF_DATA_BITS) is not None: _LOGGER.debug( "Masked device id: %s", - rfxtrx.get_pt2262_deviceid(device_id, entity.get(CONF_DATA_BITS)), + get_pt2262_deviceid(device_id, entity.get(CONF_DATA_BITS)), ) _LOGGER.debug( @@ -88,7 +95,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) device.hass = hass sensors.append(device) - rfxtrx.RFX_DEVICES[device_id] = device + RFX_DEVICES[device_id] = device add_entities(sensors) @@ -99,10 +106,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_id = slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: - sensor = rfxtrx.RFX_DEVICES[device_id] - else: - sensor = rfxtrx.get_pt2262_device(device_id) + sensor = RFX_DEVICES.get(device_id, get_pt2262_device(device_id)) if sensor is None: # Add the entity if not exists and automatic_add is True @@ -110,7 +114,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return if event.device.packettype == 0x13: - poss_dev = rfxtrx.find_possible_pt2262_device(device_id) + poss_dev = find_possible_pt2262_device(device_id) if poss_dev is not None: poss_id = slugify(poss_dev.event.device.id_string.lower()) _LOGGER.debug("Found possible matching device ID: %s", poss_id) @@ -118,7 +122,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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 + RFX_DEVICES[device_id] = sensor add_entities([sensor]) _LOGGER.info( "Added binary sensor %s (Device ID: %s Class: %s Sub: %s)", @@ -140,12 +144,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if sensor.is_lighting4: if sensor.data_bits is not None: - cmd = rfxtrx.get_pt2262_cmd(device_id, sensor.data_bits) + cmd = get_pt2262_cmd(device_id, sensor.data_bits) sensor.apply_cmd(int(cmd, 16)) else: sensor.update_state(True) else: - rfxtrx.apply_received_command(event) + apply_received_command(event) if ( sensor.is_on @@ -163,8 +167,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # Subscribe to main RFXtrx events - if binary_sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) + if binary_sensor_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) class RfxtrxBinarySensor(BinarySensorDevice): @@ -195,7 +199,7 @@ class RfxtrxBinarySensor(BinarySensorDevice): self._cmd_off = cmd_off if data_bits is not None: - self._masked_id = rfxtrx.get_pt2262_deviceid( + self._masked_id = get_pt2262_deviceid( event.device.id_string.lower(), data_bits ) else: diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 7aff22bd124..e1eb6ae77f5 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -2,10 +2,10 @@ import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_OPEN from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -13,6 +13,11 @@ from . import ( CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -35,7 +40,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx cover.""" - covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) + covers = get_devices_from_config(config, RfxtrxCover) add_entities(covers) def cover_update(event): @@ -47,20 +52,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxCover) + new_device = get_new_device(event, config, RfxtrxCover) if new_device: add_entities([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if cover_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(cover_update) + if cover_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(cover_update) -class RfxtrxCover(rfxtrx.RfxtrxDevice, CoverDevice): +class RfxtrxCover(RfxtrxDevice, CoverDevice, RestoreEntity): """Representation of a RFXtrx cover.""" + async def async_added_to_hass(self): + """Restore RFXtrx cover device state (OPEN/CLOSE).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_OPEN + @property def should_poll(self): """Return the polling state. No polling available in RFXtrx cover.""" diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index a745a11388a..437cce89c49 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -4,15 +4,15 @@ import logging import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, ) -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -20,6 +20,11 @@ from . import ( CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) _LOGGER = logging.getLogger(__name__) @@ -46,7 +51,7 @@ SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) + lights = get_devices_from_config(config, RfxtrxLight) add_entities(lights) def light_update(event): @@ -57,20 +62,35 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxLight) + new_device = get_new_device(event, config, RfxtrxLight) if new_device: add_entities([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if light_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(light_update) + if light_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(light_update) -class RfxtrxLight(rfxtrx.RfxtrxDevice, Light): +class RfxtrxLight(RfxtrxDevice, Light, RestoreEntity): """Representation of a RFXtrx light.""" + async def async_added_to_hass(self): + """Restore RFXtrx device state (ON/OFF).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + + # Restore the brightness of dimmable devices + if ( + old_state is not None + and old_state.attributes.get(ATTR_BRIGHTNESS) is not None + ): + self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) + @property def brightness(self): """Return the brightness of this light between 0..255.""" diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index e26ceb7ef57..a3c4d2fcb72 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -1,12 +1,8 @@ { "domain": "rfxtrx", - "name": "Rfxtrx", + "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": [ - "pyRFXtrx==0.24" - ], + "requirements": ["pyRFXtrx==0.25"], "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 5429943a7a6..3f74ff18695 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -4,7 +4,6 @@ import logging from RFXtrx import SensorEvent import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -19,6 +18,9 @@ from . import ( CONF_DEVICES, CONF_FIRE_EVENT, DATA_TYPES, + RECEIVED_EVT_SUBSCRIBERS, + RFX_DEVICES, + get_rfx_object, ) _LOGGER = logging.getLogger(__name__) @@ -46,9 +48,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" sensors = [] for packet_id, entity_info in config[CONF_DEVICES].items(): - event = rfxtrx.get_rfx_object(packet_id) + event = get_rfx_object(packet_id) device_id = "sensor_{}".format(slugify(event.device.id_string.lower())) - if device_id in rfxtrx.RFX_DEVICES: + if device_id in RFX_DEVICES: continue _LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME]) @@ -66,7 +68,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) sensors.append(new_sensor) sub_sensors[_data_type] = new_sensor - rfxtrx.RFX_DEVICES[device_id] = sub_sensors + RFX_DEVICES[device_id] = sub_sensors add_entities(sensors) def sensor_update(event): @@ -76,8 +78,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_id = "sensor_" + slugify(event.device.id_string.lower()) - if device_id in rfxtrx.RFX_DEVICES: - sensors = rfxtrx.RFX_DEVICES[device_id] + if device_id in RFX_DEVICES: + sensors = RFX_DEVICES[device_id] for data_type in sensors: # Some multi-sensor devices send individual messages for each # of their sensors. Update only if event contains the @@ -108,11 +110,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_sensor = RfxtrxSensor(event, pkt_id, data_type) sub_sensors = {} sub_sensors[new_sensor.data_type] = new_sensor - rfxtrx.RFX_DEVICES[device_id] = sub_sensors + RFX_DEVICES[device_id] = sub_sensors add_entities([new_sensor]) - if sensor_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) + if sensor_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) class RfxtrxSensor(Entity): diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 6d91b261a4f..05e4a37ab44 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -4,10 +4,10 @@ import logging import RFXtrx as rfxtrxmod import voluptuous as vol -from homeassistant.components import rfxtrx from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_NAME +from homeassistant.const import CONF_NAME, STATE_ON from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.restore_state import RestoreEntity from . import ( CONF_AUTOMATIC_ADD, @@ -15,6 +15,11 @@ from . import ( CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, + RECEIVED_EVT_SUBSCRIBERS, + RfxtrxDevice, + apply_received_command, + get_devices_from_config, + get_new_device, ) _LOGGER = logging.getLogger(__name__) @@ -40,7 +45,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the RFXtrx platform.""" # Add switch from config file - switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch) + switches = get_devices_from_config(config, RfxtrxSwitch) add_entities_callback(switches) def switch_update(event): @@ -52,20 +57,28 @@ def setup_platform(hass, config, add_entities_callback, discovery_info=None): ): return - new_device = rfxtrx.get_new_device(event, config, RfxtrxSwitch) + new_device = get_new_device(event, config, RfxtrxSwitch) if new_device: add_entities_callback([new_device]) - rfxtrx.apply_received_command(event) + apply_received_command(event) # Subscribe to main RFXtrx events - if switch_update not in rfxtrx.RECEIVED_EVT_SUBSCRIBERS: - rfxtrx.RECEIVED_EVT_SUBSCRIBERS.append(switch_update) + if switch_update not in RECEIVED_EVT_SUBSCRIBERS: + RECEIVED_EVT_SUBSCRIBERS.append(switch_update) -class RfxtrxSwitch(rfxtrx.RfxtrxDevice, SwitchDevice): +class RfxtrxSwitch(RfxtrxDevice, SwitchDevice, RestoreEntity): """Representation of a RFXtrx switch.""" + async def async_added_to_hass(self): + """Restore RFXtrx switch device state (ON/OFF).""" + await super().async_added_to_hass() + + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + def turn_on(self, **kwargs): """Turn the device on.""" self._send_command("turn_on") diff --git a/homeassistant/components/ring/.translations/da.json b/homeassistant/components/ring/.translations/da.json new file mode 100644 index 00000000000..45aebd1ebd5 --- /dev/null +++ b/homeassistant/components/ring/.translations/da.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret" + }, + "error": { + "invalid_auth": "Ugyldig godkendelse", + "unknown": "Uventet fejl" + }, + "step": { + "2fa": { + "data": { + "2fa": "Tofaktorkode" + }, + "title": "Tofaktorgodkendelse" + }, + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "title": "Log ind med Ring-konto" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/.translations/en.json b/homeassistant/components/ring/.translations/en.json new file mode 100644 index 00000000000..54caa8f96e7 --- /dev/null +++ b/homeassistant/components/ring/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "2fa": { + "data": { + "2fa": "Two-factor code" + }, + "title": "Two-factor authentication" + }, + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Sign-in with Ring account" + } + }, + "title": "Ring" + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/__init__.py b/homeassistant/components/ring/__init__.py index a68749b2c67..7b4fbb15b30 100644 --- a/homeassistant/components/ring/__init__.py +++ b/homeassistant/components/ring/__init__.py @@ -1,15 +1,21 @@ """Support for Ring Doorbell/Chimes.""" +import asyncio from datetime import timedelta +from functools import partial import logging +from pathlib import Path +from typing import Optional -from requests.exceptions import ConnectTimeout, HTTPError -from ring_doorbell import Ring +from oauthlib.oauth2 import AccessDeniedError +from ring_doorbell import Auth, Ring import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, __version__ +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.async_ import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) @@ -18,24 +24,17 @@ ATTRIBUTION = "Data provided by Ring.com" NOTIFICATION_ID = "ring_notification" NOTIFICATION_TITLE = "Ring Setup" -DATA_RING_DOORBELLS = "ring_doorbells" -DATA_RING_STICKUP_CAMS = "ring_stickup_cams" -DATA_RING_CHIMES = "ring_chimes" - DOMAIN = "ring" -DEFAULT_CACHEDB = ".ring_cache.pickle" DEFAULT_ENTITY_NAMESPACE = "ring" -SIGNAL_UPDATE_RING = "ring_update" -SCAN_INTERVAL = timedelta(seconds=10) +PLATFORMS = ("binary_sensor", "light", "sensor", "switch", "camera") CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + vol.Optional(DOMAIN): vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, } ) }, @@ -43,55 +42,255 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up the Ring component.""" - conf = config[DOMAIN] - username = conf[CONF_USERNAME] - password = conf[CONF_PASSWORD] - scan_interval = conf[CONF_SCAN_INTERVAL] + if DOMAIN not in config: + return True + + def legacy_cleanup(): + """Clean up old tokens.""" + old_cache = Path(hass.config.path(".ring_cache.pickle")) + if old_cache.is_file(): + old_cache.unlink() + + await hass.async_add_executor_job(legacy_cleanup) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "username": config[DOMAIN]["username"], + "password": config[DOMAIN]["password"], + }, + ) + ) + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + + def token_updater(token): + """Handle from sync context when token is updated.""" + run_callback_threadsafe( + hass.loop, + partial( + hass.config_entries.async_update_entry, + entry, + data={**entry.data, "token": token}, + ), + ).result() + + auth = Auth(f"HomeAssistant/{__version__}", entry.data["token"], token_updater) + ring = Ring(auth) try: - cache = hass.config.path(DEFAULT_CACHEDB) - ring = Ring(username=username, password=password, cache_file=cache) - if not ring.is_connected: - return False - hass.data[DATA_RING_CHIMES] = chimes = ring.chimes - hass.data[DATA_RING_DOORBELLS] = doorbells = ring.doorbells - hass.data[DATA_RING_STICKUP_CAMS] = stickup_cams = ring.stickup_cams - - ring_devices = chimes + doorbells + stickup_cams - - except (ConnectTimeout, HTTPError) as ex: - _LOGGER.error("Unable to connect to Ring service: %s", str(ex)) - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) + await hass.async_add_executor_job(ring.update_data) + except AccessDeniedError: + _LOGGER.error("Access token is no longer valid. Please set up Ring again") return False - def service_hub_refresh(service): - hub_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + "api": ring, + "devices": ring.devices(), + "device_data": GlobalDataUpdater( + hass, entry.entry_id, ring, "update_devices", timedelta(minutes=1) + ), + "dings_data": GlobalDataUpdater( + hass, entry.entry_id, ring, "update_dings", timedelta(seconds=10) + ), + "history_data": DeviceDataUpdater( + hass, + entry.entry_id, + ring, + lambda device: device.history(limit=10), + timedelta(minutes=1), + ), + "health_data": DeviceDataUpdater( + hass, + entry.entry_id, + ring, + lambda device: device.update_health_data(), + timedelta(minutes=1), + ), + } - def timer_hub_refresh(event_time): - hub_refresh() + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) - def hub_refresh(): - """Call ring to refresh information.""" - _LOGGER.debug("Updating Ring Hub component") + if hass.services.has_service(DOMAIN, "update"): + return True - for camera in ring_devices: - _LOGGER.debug("Updating camera %s", camera.name) - camera.update() - - dispatcher_send(hass, SIGNAL_UPDATE_RING) + async def async_refresh_all(_): + """Refresh all ring data.""" + for info in hass.data[DOMAIN].values(): + await info["device_data"].async_refresh_all() + await info["dings_data"].async_refresh_all() + await hass.async_add_executor_job(info["history_data"].refresh_all) + await hass.async_add_executor_job(info["health_data"].refresh_all) # register service - hass.services.register(DOMAIN, "update", service_hub_refresh) - - # register scan interval for ring - track_time_interval(hass, timer_hub_refresh, scan_interval) + hass.services.async_register(DOMAIN, "update", async_refresh_all) return True + + +async def async_unload_entry(hass, entry): + """Unload Ring entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if not unload_ok: + return False + + hass.data[DOMAIN].pop(entry.entry_id) + + if len(hass.data[DOMAIN]) != 0: + return True + + # Last entry unloaded, clean up service + hass.services.async_remove(DOMAIN, "update") + + return True + + +class GlobalDataUpdater: + """Data storage for single API endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry_id: str, + ring: Ring, + update_method: str, + update_interval: timedelta, + ): + """Initialize global data updater.""" + self.hass = hass + self.config_entry_id = config_entry_id + self.ring = ring + self.update_method = update_method + self.update_interval = update_interval + self.listeners = [] + self._unsub_interval = None + + @callback + def async_add_listener(self, update_callback): + """Listen for data updates.""" + # This is the first listener, set up interval. + if not self.listeners: + self._unsub_interval = async_track_time_interval( + self.hass, self.async_refresh_all, self.update_interval + ) + + self.listeners.append(update_callback) + + @callback + def async_remove_listener(self, update_callback): + """Remove data update.""" + self.listeners.remove(update_callback) + + if not self.listeners: + self._unsub_interval() + self._unsub_interval = None + + async def async_refresh_all(self, _now: Optional[int] = None) -> None: + """Time to update.""" + if not self.listeners: + return + + try: + await self.hass.async_add_executor_job( + getattr(self.ring, self.update_method) + ) + except AccessDeniedError: + _LOGGER.error("Ring access token is no longer valid. Set up Ring again") + await self.hass.config_entries.async_unload(self.config_entry_id) + return + + for update_callback in self.listeners: + update_callback() + + +class DeviceDataUpdater: + """Data storage for device data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry_id: str, + ring: Ring, + update_method: str, + update_interval: timedelta, + ): + """Initialize device data updater.""" + self.hass = hass + self.config_entry_id = config_entry_id + self.ring = ring + self.update_method = update_method + self.update_interval = update_interval + self.devices = {} + self._unsub_interval = None + + async def async_track_device(self, device, update_callback): + """Track a device.""" + if not self.devices: + self._unsub_interval = async_track_time_interval( + self.hass, self.refresh_all, self.update_interval + ) + + if device.device_id not in self.devices: + self.devices[device.device_id] = { + "device": device, + "update_callbacks": [update_callback], + "data": None, + } + # Store task so that other concurrent requests can wait for us to finish and + # data be available. + self.devices[device.device_id]["task"] = asyncio.current_task() + self.devices[device.device_id][ + "data" + ] = await self.hass.async_add_executor_job(self.update_method, device) + self.devices[device.device_id].pop("task") + else: + self.devices[device.device_id]["update_callbacks"].append(update_callback) + # If someone is currently fetching data as part of the initialization, wait for them + if "task" in self.devices[device.device_id]: + await self.devices[device.device_id]["task"] + + update_callback(self.devices[device.device_id]["data"]) + + @callback + def async_untrack_device(self, device, update_callback): + """Untrack a device.""" + self.devices[device.device_id]["update_callbacks"].remove(update_callback) + + if not self.devices[device.device_id]["update_callbacks"]: + self.devices.pop(device.device_id) + + if not self.devices: + self._unsub_interval() + self._unsub_interval = None + + def refresh_all(self, _=None): + """Refresh all registered devices.""" + for info in self.devices.values(): + try: + data = info["data"] = self.update_method(info["device"]) + except AccessDeniedError: + _LOGGER.error("Ring access token is no longer valid. Set up Ring again") + self.hass.add_job( + self.hass.config_entries.async_unload(self.config_entry_id) + ) + return + + for update_callback in info["update_callbacks"]: + self.hass.loop.call_soon_threadsafe(update_callback, data) diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 86d26ec25b4..7b20ff948d1 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -1,23 +1,12 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging -import voluptuous as vol +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_ENTITY_NAMESPACE, - CONF_MONITORED_CONDITIONS, -) -import homeassistant.helpers.config_validation as cv - -from . import ( - ATTRIBUTION, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, - DEFAULT_ENTITY_NAMESPACE, -) +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -25,55 +14,80 @@ SCAN_INTERVAL = timedelta(seconds=10) # Sensor types: Name, category, device_class SENSOR_TYPES = { - "ding": ["Ding", ["doorbell"], "occupancy"], - "motion": ["Motion", ["doorbell", "stickup_cams"], "motion"], + "ding": ["Ding", ["doorbots", "authorized_doorbots"], "occupancy"], + "motion": ["Motion", ["doorbots", "authorized_doorbots", "stickup_cams"], "motion"], } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } -) - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for a Ring device.""" - ring_doorbells = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Ring binary sensors from a config entry.""" + ring = hass.data[DOMAIN][config_entry.entry_id]["api"] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] sensors = [] - for device in ring_doorbells: # ring.doorbells is doing I/O - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - if "doorbell" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, device, sensor_type)) - for device in ring_stickup_cams: # ring.stickup_cams is doing I/O - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingBinarySensor(hass, device, sensor_type)) + for device_type in ("doorbots", "authorized_doorbots", "stickup_cams"): + for sensor_type in SENSOR_TYPES: + if device_type not in SENSOR_TYPES[sensor_type][1]: + continue - add_entities(sensors, True) + for device in devices[device_type]: + sensors.append( + RingBinarySensor(config_entry.entry_id, ring, device, sensor_type) + ) + + async_add_entities(sensors) -class RingBinarySensor(BinarySensorDevice): +class RingBinarySensor(RingEntityMixin, BinarySensorDevice): """A binary sensor implementation for Ring device.""" - def __init__(self, hass, data, sensor_type): + _active_alert = None + + def __init__(self, config_entry_id, ring, device, sensor_type): """Initialize a sensor for Ring device.""" - super().__init__() + super().__init__(config_entry_id, device) + self._ring = ring self._sensor_type = sensor_type - self._data = data self._name = "{0} {1}".format( - self._data.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(sensor_type)[0] ) - self._device_class = SENSOR_TYPES.get(self._sensor_type)[2] + self._device_class = SENSOR_TYPES.get(sensor_type)[2] self._state = None - self._unique_id = f"{self._data.id}-{self._sensor_type}" + self._unique_id = f"{device.id}-{sensor_type}" + self._update_alert() + + async def async_added_to_hass(self): + """Register callbacks.""" + await super().async_added_to_hass() + self.ring_objects["dings_data"].async_add_listener(self._dings_update_callback) + self._dings_update_callback() + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + await super().async_will_remove_from_hass() + self.ring_objects["dings_data"].async_remove_listener( + self._dings_update_callback + ) + + @callback + def _dings_update_callback(self): + """Call update method.""" + self._update_alert() + self.async_write_ha_state() + + @callback + def _update_alert(self): + """Update active alert.""" + self._active_alert = next( + ( + alert + for alert in self._ring.active_alerts() + if alert["kind"] == self._sensor_type + and alert["doorbot_id"] == self._device.id + ), + None, + ) @property def name(self): @@ -83,7 +97,7 @@ class RingBinarySensor(BinarySensorDevice): @property def is_on(self): """Return True if the binary sensor is on.""" - return self._state + return self._active_alert is not None @property def device_class(self): @@ -98,27 +112,14 @@ class RingBinarySensor(BinarySensorDevice): @property def device_state_attributes(self): """Return the state attributes.""" - attrs = {} - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION + attrs = super().device_state_attributes - attrs["device_id"] = self._data.id - attrs["firmware"] = self._data.firmware - attrs["timezone"] = self._data.timezone + if self._active_alert is None: + return attrs - if self._data.alert and self._data.alert_expires_at: - attrs["expires_at"] = self._data.alert_expires_at - attrs["state"] = self._data.alert.get("state") + attrs["state"] = self._active_alert["state"] + attrs["expires_at"] = datetime.fromtimestamp( + self._active_alert.get("now") + self._active_alert.get("expires_in") + ).isoformat() return attrs - - def update(self): - """Get the latest data and updates the state.""" - self._data.check_alerts() - - if self._data.alert: - if self._sensor_type == self._data.alert.get( - "kind" - ) and self._data.account_id == self._data.alert.get("doorbot_id"): - self._state = True - else: - self._state = False diff --git a/homeassistant/components/ring/camera.py b/homeassistant/components/ring/camera.py index 1d2fe6ff67b..07d87c85714 100644 --- a/homeassistant/components/ring/camera.py +++ b/homeassistant/components/ring/camera.py @@ -1,102 +1,86 @@ """This component provides support to the Ring Door Bell camera.""" import asyncio from datetime import timedelta +from itertools import chain import logging from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame -import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import Camera from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util -from . import ( - ATTRIBUTION, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, - NOTIFICATION_ID, - SIGNAL_UPDATE_RING, -) - -CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +from . import ATTRIBUTION, DOMAIN +from .entity import RingEntityMixin FORCE_REFRESH_INTERVAL = timedelta(minutes=45) _LOGGER = logging.getLogger(__name__) -NOTIFICATION_TITLE = "Ring Camera Setup" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_FFMPEG_ARGUMENTS): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Ring Door Bell and StickUp Camera.""" - ring_doorbell = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] cams = [] - cams_no_plan = [] - for camera in ring_doorbell + ring_stickup_cams: - if camera.has_subscription: - cams.append(RingCam(hass, camera, config)) - else: - cams_no_plan.append(camera) + for camera in chain( + devices["doorbots"], devices["authorized_doorbots"], devices["stickup_cams"] + ): + if not camera.has_subscription: + continue - # show notification for all cameras without an active subscription - if cams_no_plan: - cameras = str(", ".join([camera.name for camera in cams_no_plan])) + cams.append(RingCam(config_entry.entry_id, hass.data[DATA_FFMPEG], camera)) - err_msg = ( - """A Ring Protect Plan is required for the""" - """ following cameras: {}.""".format(cameras) - ) - - _LOGGER.error(err_msg) - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(err_msg), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - - add_entities(cams, True) - return True + async_add_entities(cams) -class RingCam(Camera): +class RingCam(RingEntityMixin, Camera): """An implementation of a Ring Door Bell camera.""" - def __init__(self, hass, camera, device_info): + def __init__(self, config_entry_id, ffmpeg, device): """Initialize a Ring Door Bell camera.""" - super().__init__() - self._camera = camera - self._hass = hass - self._name = self._camera.name - self._ffmpeg = hass.data[DATA_FFMPEG] - self._ffmpeg_arguments = device_info.get(CONF_FFMPEG_ARGUMENTS) - self._last_video_id = self._camera.last_recording_id - self._video_url = self._camera.recording_url(self._last_video_id) + super().__init__(config_entry_id, device) + + self._name = self._device.name + self._ffmpeg = ffmpeg + self._last_event = None + self._last_video_id = None + self._video_url = None self._utcnow = dt_util.utcnow() - self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + self._expires_at = self._utcnow - FORCE_REFRESH_INTERVAL async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) + await super().async_added_to_hass() + + await self.ring_objects["history_data"].async_track_device( + self._device, self._history_update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + await super().async_will_remove_from_hass() + + self.ring_objects["history_data"].async_untrack_device( + self._device, self._history_update_callback + ) @callback - def _update_callback(self): + def _history_update_callback(self, history_data): """Call update method.""" - self.async_schedule_update_ha_state(True) - _LOGGER.debug("Updating Ring camera %s (callback)", self.name) + if history_data: + self._last_event = history_data[0] + self.async_schedule_update_ha_state(True) + else: + self._last_event = None + self._last_video_id = None + self._video_url = None + self._expires_at = self._utcnow + self.async_write_ha_state() @property def name(self): @@ -106,47 +90,36 @@ class RingCam(Camera): @property def unique_id(self): """Return a unique ID.""" - return self._camera.id + return self._device.id @property def device_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, - "device_id": self._camera.id, - "firmware": self._camera.firmware, - "kind": self._camera.kind, - "timezone": self._camera.timezone, - "type": self._camera.family, "video_url": self._video_url, "last_video_id": self._last_video_id, } async def async_camera_image(self): """Return a still image response from the camera.""" - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) if self._video_url is None: return image = await asyncio.shield( - ffmpeg.get_image( - self._video_url, - output_format=IMAGE_JPEG, - extra_cmd=self._ffmpeg_arguments, - ) + ffmpeg.get_image(self._video_url, output_format=IMAGE_JPEG,) ) return image async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - if self._video_url is None: return stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) - await stream.open_camera(self._video_url, extra_cmd=self._ffmpeg_arguments) + await stream.open_camera(self._video_url) try: stream_reader = await stream.get_reader() @@ -159,34 +132,25 @@ class RingCam(Camera): finally: await stream.close() - @property - def should_poll(self): - """Return False, updates are controlled via the hub.""" - return False - - def update(self): + async def async_update(self): """Update camera entity and refresh attributes.""" - _LOGGER.debug("Checking if Ring DoorBell needs to refresh video_url") - - self._utcnow = dt_util.utcnow() - - try: - last_event = self._camera.history(limit=1)[0] - except (IndexError, TypeError): + if self._last_event is None: return - last_recording_id = last_event["id"] - video_status = last_event["recording"]["status"] + if self._last_event["recording"]["status"] != "ready": + return - if video_status == "ready" and ( - self._last_video_id != last_recording_id or self._utcnow >= self._expires_at + if ( + self._last_video_id == self._last_event["id"] + and self._utcnow <= self._expires_at ): + return - video_url = self._camera.recording_url(last_recording_id) - if video_url: - _LOGGER.info("Ring DoorBell properties refreshed") + video_url = await self.hass.async_add_executor_job( + self._device.recording_url, self._last_event["id"] + ) - # update attributes if new video or if URL has expired - self._last_video_id = last_recording_id - self._video_url = video_url - self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow + if video_url: + self._last_video_id = self._last_event["id"] + self._video_url = video_url + self._expires_at = FORCE_REFRESH_INTERVAL + self._utcnow diff --git a/homeassistant/components/ring/config_flow.py b/homeassistant/components/ring/config_flow.py new file mode 100644 index 00000000000..a25e0283753 --- /dev/null +++ b/homeassistant/components/ring/config_flow.py @@ -0,0 +1,91 @@ +"""Config flow for Ring integration.""" +import logging + +from oauthlib.oauth2 import AccessDeniedError, MissingTokenError +from ring_doorbell import Auth +import voluptuous as vol + +from homeassistant import config_entries, const, core, exceptions + +from . import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + + auth = Auth(f"HomeAssistant/{const.__version__}") + + try: + token = await hass.async_add_executor_job( + auth.fetch_token, data["username"], data["password"], data.get("2fa"), + ) + except MissingTokenError: + raise Require2FA + except AccessDeniedError: + raise InvalidAuth + + return token + + +class RingConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Ring.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + user_pass = None + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + token = await validate_input(self.hass, user_input) + await self.async_set_unique_id(user_input["username"]) + + return self.async_create_entry( + title=user_input["username"], + data={"username": user_input["username"], "token": token}, + ) + except Require2FA: + self.user_pass = user_input + + return await self.async_step_2fa() + + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({"username": str, "password": str}), + errors=errors, + ) + + async def async_step_2fa(self, user_input=None): + """Handle 2fa step.""" + if user_input: + return await self.async_step_user({**self.user_pass, **user_input}) + + return self.async_show_form( + step_id="2fa", data_schema=vol.Schema({"2fa": str}), + ) + + async def async_step_import(self, user_input): + """Handle import.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + return await self.async_step_user(user_input) + + +class Require2FA(exceptions.HomeAssistantError): + """Error to indicate we require 2FA.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/ring/entity.py b/homeassistant/components/ring/entity.py new file mode 100644 index 00000000000..6eb87cb8f9b --- /dev/null +++ b/homeassistant/components/ring/entity.py @@ -0,0 +1,53 @@ +"""Base class for Ring entity.""" +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.core import callback + +from . import ATTRIBUTION, DOMAIN + + +class RingEntityMixin: + """Base implementation for Ring device.""" + + def __init__(self, config_entry_id, device): + """Initialize a sensor for Ring device.""" + super().__init__() + self._config_entry_id = config_entry_id + self._device = device + + async def async_added_to_hass(self): + """Register callbacks.""" + self.ring_objects["device_data"].async_add_listener(self._update_callback) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + self.ring_objects["device_data"].async_remove_listener(self._update_callback) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_write_ha_state() + + @property + def ring_objects(self): + """Return the Ring API objects.""" + return self.hass.data[DOMAIN][self._config_entry_id] + + @property + def should_poll(self): + """Return False, updates are controlled via the hub.""" + return False + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def device_info(self): + """Return device info.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "model": self._device.model, + "manufacturer": "Ring", + } diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py index bc86e5b5fd1..86ef55af16d 100644 --- a/homeassistant/components/ring/light.py +++ b/homeassistant/components/ring/light.py @@ -4,10 +4,10 @@ import logging from homeassistant.components.light import Light from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -23,37 +23,37 @@ ON_STATE = "on" OFF_STATE = "off" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the lights for the Ring devices.""" - cameras = hass.data[DATA_RING_STICKUP_CAMS] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] + lights = [] - for device in cameras: + for device in devices["stickup_cams"]: if device.has_capability("light"): - lights.append(RingLight(device)) + lights.append(RingLight(config_entry.entry_id, device)) - add_entities(lights, True) + async_add_entities(lights) -class RingLight(Light): +class RingLight(RingEntityMixin, Light): """Creates a switch to turn the ring cameras light on and off.""" - def __init__(self, device): + def __init__(self, config_entry_id, device): """Initialize the light.""" - self._device = device - self._unique_id = self._device.id - self._light_on = False + super().__init__(config_entry_id, device) + self._unique_id = device.id + self._light_on = device.lights == ON_STATE self._no_updates_until = dt_util.utcnow() - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) - @callback def _update_callback(self): """Call update method.""" - _LOGGER.debug("Updating Ring light %s (callback)", self.name) - self.async_schedule_update_ha_state(True) + if self._no_updates_until > dt_util.utcnow(): + return + + self._light_on = self._device.lights == ON_STATE + self.async_write_ha_state() @property def name(self): @@ -65,22 +65,17 @@ class RingLight(Light): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Update controlled via the hub.""" - return False - @property def is_on(self): """If the switch is currently on or off.""" return self._light_on def _set_light(self, new_state): - """Update light state, and causes HASS to correctly update.""" + """Update light state, and causes Home Assistant to correctly update.""" self._device.lights = new_state self._light_on = new_state == ON_STATE self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY - self.async_schedule_update_ha_state(True) + self.async_schedule_update_ha_state() def turn_on(self, **kwargs): """Turn the light on for 30 seconds.""" @@ -89,11 +84,3 @@ class RingLight(Light): def turn_off(self, **kwargs): """Turn the light off.""" self._set_light(OFF_STATE) - - def update(self): - """Update current state of the light.""" - if self._no_updates_until > dt_util.utcnow(): - _LOGGER.debug("Skipping update...") - return - - self._light_on = self._device.lights == ON_STATE diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 6fc57244deb..d46f12af511 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -2,7 +2,8 @@ "domain": "ring", "name": "Ring", "documentation": "https://www.home-assistant.io/integrations/ring", - "requirements": ["ring_doorbell==0.2.8"], + "requirements": ["ring_doorbell==0.6.0"], "dependencies": ["ffmpeg"], - "codeowners": [] + "codeowners": ["@balloob"], + "config_flow": true } diff --git a/homeassistant/components/ring/sensor.py b/homeassistant/components/ring/sensor.py index b54c750664e..2b921dddd2f 100644 --- a/homeassistant/components/ring/sensor.py +++ b/homeassistant/components/ring/sensor.py @@ -1,136 +1,60 @@ """This component provides HA sensor support for Ring Door Bell/Chimes.""" import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_ENTITY_NAMESPACE, - CONF_MONITORED_CONDITIONS, -) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level -from . import ( - ATTRIBUTION, - DATA_RING_CHIMES, - DATA_RING_DOORBELLS, - DATA_RING_STICKUP_CAMS, - DEFAULT_ENTITY_NAMESPACE, - SIGNAL_UPDATE_RING, -) +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) -# Sensor types: Name, category, units, icon, kind -SENSOR_TYPES = { - "battery": ["Battery", ["doorbell", "stickup_cams"], "%", "battery-50", None], - "last_activity": [ - "Last Activity", - ["doorbell", "stickup_cams"], - None, - "history", - None, - ], - "last_ding": ["Last Ding", ["doorbell"], None, "history", "ding"], - "last_motion": [ - "Last Motion", - ["doorbell", "stickup_cams"], - None, - "history", - "motion", - ], - "volume": [ - "Volume", - ["chime", "doorbell", "stickup_cams"], - None, - "bell-ring", - None, - ], - "wifi_signal_category": [ - "WiFi Signal Category", - ["chime", "doorbell", "stickup_cams"], - None, - "wifi", - None, - ], - "wifi_signal_strength": [ - "WiFi Signal Strength", - ["chime", "doorbell", "stickup_cams"], - "dBm", - "wifi", - None, - ], -} -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional( - CONF_ENTITY_NAMESPACE, default=DEFAULT_ENTITY_NAMESPACE - ): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for a Ring device.""" - ring_chimes = hass.data[DATA_RING_CHIMES] - ring_doorbells = hass.data[DATA_RING_DOORBELLS] - ring_stickup_cams = hass.data[DATA_RING_STICKUP_CAMS] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] + + # Makes a ton of requests. We will make this a config entry option in the future + wifi_enabled = False sensors = [] - for device in ring_chimes: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - if "chime" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) - for device in ring_doorbells: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - if "doorbell" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) + for device_type in ("chimes", "doorbots", "authorized_doorbots", "stickup_cams"): + for sensor_type in SENSOR_TYPES: + if device_type not in SENSOR_TYPES[sensor_type][1]: + continue - for device in ring_stickup_cams: - for sensor_type in config[CONF_MONITORED_CONDITIONS]: - if "stickup_cams" in SENSOR_TYPES[sensor_type][1]: - sensors.append(RingSensor(hass, device, sensor_type)) + if not wifi_enabled and sensor_type.startswith("wifi_"): + continue - add_entities(sensors, True) - return True + for device in devices[device_type]: + if device_type == "battery" and device.battery_life is None: + continue + + sensors.append( + SENSOR_TYPES[sensor_type][6]( + config_entry.entry_id, device, sensor_type + ) + ) + + async_add_entities(sensors) -class RingSensor(Entity): +class RingSensor(RingEntityMixin, Entity): """A sensor implementation for Ring device.""" - def __init__(self, hass, data, sensor_type): + def __init__(self, config_entry_id, device, sensor_type): """Initialize a sensor for Ring device.""" - super().__init__() + super().__init__(config_entry_id, device) self._sensor_type = sensor_type - self._data = data self._extra = None - self._icon = "mdi:{}".format(SENSOR_TYPES.get(self._sensor_type)[3]) - self._kind = SENSOR_TYPES.get(self._sensor_type)[4] + self._icon = "mdi:{}".format(SENSOR_TYPES.get(sensor_type)[3]) + self._kind = SENSOR_TYPES.get(sensor_type)[4] self._name = "{0} {1}".format( - self._data.name, SENSOR_TYPES.get(self._sensor_type)[0] + self._device.name, SENSOR_TYPES.get(sensor_type)[0] ) - self._state = None - self._tz = str(hass.config.time_zone) - self._unique_id = f"{self._data.id}-{self._sensor_type}" - - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) - - @callback - def _update_callback(self): - """Call update method.""" - self.async_schedule_update_ha_state(True) + self._unique_id = f"{device.id}-{sensor_type}" @property def should_poll(self): @@ -145,7 +69,11 @@ class RingSensor(Entity): @property def state(self): """Return the state of the sensor.""" - return self._state + if self._sensor_type == "volume": + return self._device.volume + + if self._sensor_type == "battery": + return self._device.battery_life @property def unique_id(self): @@ -153,32 +81,16 @@ class RingSensor(Entity): return self._unique_id @property - def device_state_attributes(self): - """Return the state attributes.""" - attrs = {} - - attrs[ATTR_ATTRIBUTION] = ATTRIBUTION - attrs["device_id"] = self._data.id - attrs["firmware"] = self._data.firmware - attrs["kind"] = self._data.kind - attrs["timezone"] = self._data.timezone - attrs["type"] = self._data.family - attrs["wifi_name"] = self._data.wifi_name - - if self._extra and self._sensor_type.startswith("last_"): - attrs["created_at"] = self._extra["created_at"] - attrs["answered"] = self._extra["answered"] - attrs["recording_status"] = self._extra["recording"]["status"] - attrs["category"] = self._extra["kind"] - - return attrs + def device_class(self): + """Return sensor device class.""" + return SENSOR_TYPES[self._sensor_type][5] @property def icon(self): """Icon to use in the frontend, if any.""" - if self._sensor_type == "battery" and self._state is not None: + if self._sensor_type == "battery" and self._device.battery_life is not None: return icon_for_battery_level( - battery_level=int(self._state), charging=False + battery_level=self._device.battery_life, charging=False ) return self._icon @@ -187,29 +99,168 @@ class RingSensor(Entity): """Return the units of measurement.""" return SENSOR_TYPES.get(self._sensor_type)[2] - def update(self): - """Get the latest data and updates the state.""" - _LOGGER.debug("Updating data from %s sensor", self._name) - if self._sensor_type == "volume": - self._state = self._data.volume +class HealthDataRingSensor(RingSensor): + """Ring sensor that relies on health data.""" - if self._sensor_type == "battery": - self._state = self._data.battery_life + async def async_added_to_hass(self): + """Register callbacks.""" + await super().async_added_to_hass() - if self._sensor_type.startswith("last_"): - history = self._data.history( - limit=5, timezone=self._tz, kind=self._kind, enforce_limit=True - ) - if history: - self._extra = history[0] - created_at = self._extra["created_at"] - self._state = "{0:0>2}:{1:0>2}".format( - created_at.hour, created_at.minute - ) + await self.ring_objects["health_data"].async_track_device( + self._device, self._health_update_callback + ) + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + await super().async_will_remove_from_hass() + + self.ring_objects["health_data"].async_untrack_device( + self._device, self._health_update_callback + ) + + @callback + def _health_update_callback(self, _health_data): + """Call update method.""" + self.async_write_ha_state() + + @property + def state(self): + """Return the state of the sensor.""" if self._sensor_type == "wifi_signal_category": - self._state = self._data.wifi_signal_category + return self._device.wifi_signal_category if self._sensor_type == "wifi_signal_strength": - self._state = self._data.wifi_signal_strength + return self._device.wifi_signal_strength + + +class HistoryRingSensor(RingSensor): + """Ring sensor that relies on history data.""" + + _latest_event = None + + async def async_added_to_hass(self): + """Register callbacks.""" + await super().async_added_to_hass() + + await self.ring_objects["history_data"].async_track_device( + self._device, self._history_update_callback + ) + + async def async_will_remove_from_hass(self): + """Disconnect callbacks.""" + await super().async_will_remove_from_hass() + + self.ring_objects["history_data"].async_untrack_device( + self._device, self._history_update_callback + ) + + @callback + def _history_update_callback(self, history_data): + """Call update method.""" + if not history_data: + return + + found = None + if self._kind is None: + found = history_data[0] + else: + for entry in history_data: + if entry["kind"] == self._kind: + found = entry + break + + if not found: + return + + self._latest_event = found + self.async_write_ha_state() + + @property + def state(self): + """Return the state of the sensor.""" + if self._latest_event is None: + return None + + return self._latest_event["created_at"].isoformat() + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = super().device_state_attributes + + if self._latest_event: + attrs["created_at"] = self._latest_event["created_at"] + attrs["answered"] = self._latest_event["answered"] + attrs["recording_status"] = self._latest_event["recording"]["status"] + attrs["category"] = self._latest_event["kind"] + + return attrs + + +# Sensor types: Name, category, units, icon, kind, device_class, class +SENSOR_TYPES = { + "battery": [ + "Battery", + ["doorbots", "authorized_doorbots", "stickup_cams"], + "%", + None, + None, + "battery", + RingSensor, + ], + "last_activity": [ + "Last Activity", + ["doorbots", "authorized_doorbots", "stickup_cams"], + None, + "history", + None, + "timestamp", + HistoryRingSensor, + ], + "last_ding": [ + "Last Ding", + ["doorbots", "authorized_doorbots"], + None, + "history", + "ding", + "timestamp", + HistoryRingSensor, + ], + "last_motion": [ + "Last Motion", + ["doorbots", "authorized_doorbots", "stickup_cams"], + None, + "history", + "motion", + "timestamp", + HistoryRingSensor, + ], + "volume": [ + "Volume", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + None, + "bell-ring", + None, + None, + RingSensor, + ], + "wifi_signal_category": [ + "WiFi Signal Category", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + None, + "wifi", + None, + None, + HealthDataRingSensor, + ], + "wifi_signal_strength": [ + "WiFi Signal Strength", + ["chimes", "doorbots", "authorized_doorbots", "stickup_cams"], + "dBm", + "wifi", + None, + "signal_strength", + HealthDataRingSensor, + ], +} diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json new file mode 100644 index 00000000000..6dff7c00ba6 --- /dev/null +++ b/homeassistant/components/ring/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Ring", + "step": { + "user": { + "title": "Sign-in with Ring account", + "data": { + "username": "Username", + "password": "Password" + } + }, + "2fa": { + "title": "Two-factor authentication", + "data": { + "2fa": "Two-factor code" + } + } + }, + "error": { + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py index 16fc4a6717f..65eed83d98e 100644 --- a/homeassistant/components/ring/switch.py +++ b/homeassistant/components/ring/switch.py @@ -4,10 +4,10 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.dt as dt_util -from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING +from . import DOMAIN +from .entity import RingEntityMixin _LOGGER = logging.getLogger(__name__) @@ -22,36 +22,27 @@ SIREN_ICON = "mdi:alarm-bell" SKIP_UPDATES_DELAY = timedelta(seconds=5) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the switches for the Ring devices.""" - cameras = hass.data[DATA_RING_STICKUP_CAMS] + devices = hass.data[DOMAIN][config_entry.entry_id]["devices"] switches = [] - for device in cameras: + + for device in devices["stickup_cams"]: if device.has_capability("siren"): - switches.append(SirenSwitch(device)) + switches.append(SirenSwitch(config_entry.entry_id, device)) - add_entities(switches, True) + async_add_entities(switches) -class BaseRingSwitch(SwitchDevice): +class BaseRingSwitch(RingEntityMixin, SwitchDevice): """Represents a switch for controlling an aspect of a ring device.""" - def __init__(self, device, device_type): + def __init__(self, config_entry_id, device, device_type): """Initialize the switch.""" - self._device = device + super().__init__(config_entry_id, device) self._device_type = device_type self._unique_id = f"{self._device.id}-{self._device_type}" - async def async_added_to_hass(self): - """Register callbacks.""" - async_dispatcher_connect(self.hass, SIGNAL_UPDATE_RING, self._update_callback) - - @callback - def _update_callback(self): - """Call update method.""" - _LOGGER.debug("Updating Ring sensor %s (callback)", self.name) - self.async_schedule_update_ha_state(True) - @property def name(self): """Name of the device.""" @@ -62,23 +53,27 @@ class BaseRingSwitch(SwitchDevice): """Return a unique ID.""" return self._unique_id - @property - def should_poll(self): - """Update controlled via the hub.""" - return False - class SirenSwitch(BaseRingSwitch): """Creates a switch to turn the ring cameras siren on and off.""" - def __init__(self, device): + def __init__(self, config_entry_id, device): """Initialize the switch for a device with a siren.""" - super().__init__(device, "siren") + super().__init__(config_entry_id, device, "siren") self._no_updates_until = dt_util.utcnow() - self._siren_on = False + self._siren_on = device.siren > 0 + + @callback + def _update_callback(self): + """Call update method.""" + if self._no_updates_until > dt_util.utcnow(): + return + + self._siren_on = self._device.siren > 0 + self.async_write_ha_state() def _set_switch(self, new_state): - """Update switch state, and causes HASS to correctly update.""" + """Update switch state, and causes Home Assistant to correctly update.""" self._device.siren = new_state self._siren_on = new_state > 0 self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY @@ -101,10 +96,3 @@ class SirenSwitch(BaseRingSwitch): def icon(self): """Return the icon.""" return SIREN_ICON - - def update(self): - """Update state of the siren.""" - if self._no_updates_until > dt_util.utcnow(): - _LOGGER.debug("Skipping update...") - return - self._siren_on = self._device.siren > 0 diff --git a/homeassistant/components/ripple/manifest.json b/homeassistant/components/ripple/manifest.json index b8aa5c74302..38a193e4a91 100644 --- a/homeassistant/components/ripple/manifest.json +++ b/homeassistant/components/ripple/manifest.json @@ -2,9 +2,7 @@ "domain": "ripple", "name": "Ripple", "documentation": "https://www.home-assistant.io/integrations/ripple", - "requirements": [ - "python-ripple-api==0.0.3" - ], + "requirements": ["python-ripple-api==0.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index ed33caa1264..eae73a2de42 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -1,12 +1,8 @@ { "domain": "rmvtransport", - "name": "Rmvtransport", + "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", - "requirements": [ - "PyRMVtransport==0.2.9" - ], + "requirements": ["PyRMVtransport==0.2.9"], "dependencies": [], - "codeowners": [ - "@cgtobi" - ] + "codeowners": ["@cgtobi"] } diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 190274518cd..8df1191a420 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -1,14 +1,14 @@ """Support for departure information for Rhein-Main public transport.""" import asyncio -import logging from datetime import timedelta +import logging from RMVtransport import RMVtransport from RMVtransport.rmvtransport import RMVtransportApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/rocketchat/manifest.json b/homeassistant/components/rocketchat/manifest.json index 924a9b86d47..a3fa11609e5 100644 --- a/homeassistant/components/rocketchat/manifest.json +++ b/homeassistant/components/rocketchat/manifest.json @@ -1,10 +1,8 @@ { "domain": "rocketchat", - "name": "Rocketchat", + "name": "Rocket.Chat", "documentation": "https://www.home-assistant.io/integrations/rocketchat", - "requirements": [ - "rocketchat-API==0.6.1" - ], + "requirements": ["rocketchat-API==0.6.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 1b5f07eb87a..b92a95af9d7 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -2,9 +2,7 @@ "domain": "roku", "name": "Roku", "documentation": "https://www.home-assistant.io/integrations/roku", - "requirements": [ - "roku==4.0.0" - ], + "requirements": ["roku==4.0.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index a785d7b18ff..f3ae60ecbea 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -120,7 +120,7 @@ class RokuDevice(MediaPlayerDevice): @property def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return self._device_info.serial_num @property diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 92ed16406db..0b674588dc6 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -1,12 +1,8 @@ { "domain": "roomba", - "name": "Roomba", + "name": "iRobot Roomba", "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": [ - "roombapy==1.4.2" - ], + "requirements": ["roombapy==1.4.2"], "dependencies": [], - "codeowners": [ - "@pschmitt" - ] + "codeowners": ["@pschmitt"] } diff --git a/homeassistant/components/route53/manifest.json b/homeassistant/components/route53/manifest.json index 307132aa01b..62edeafe735 100644 --- a/homeassistant/components/route53/manifest.json +++ b/homeassistant/components/route53/manifest.json @@ -1,11 +1,8 @@ { "domain": "route53", - "name": "Route53", + "name": "AWS Route53", "documentation": "https://www.home-assistant.io/integrations/route53", - "requirements": [ - "boto3==1.9.252", - "ipify==1.0.0" - ], + "requirements": ["boto3==1.9.252", "ipify==1.0.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index e033e82d922..0099513dea7 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -1,10 +1,8 @@ { "domain": "rova", - "name": "Rova", + "name": "ROVA", "documentation": "https://www.home-assistant.io/integrations/rova", - "requirements": [ - "rova==0.1.0" - ], + "requirements": ["rova==0.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index 86a04829c75..a2dafae9317 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -30,8 +30,8 @@ SCAN_INTERVAL = timedelta(hours=12) SENSOR_TYPES = { "bio": ["gft", "Biowaste", "mdi:recycle"], "paper": ["papier", "Paper", "mdi:recycle"], - "plastic": ["plasticplus", "PET", "mdi:recycle"], - "residual": ["rest", "Residual", "mdi:recycle"], + "plastic": ["pmd", "PET", "mdi:recycle"], + "residual": ["restafval", "Residual", "mdi:recycle"], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/rpi_camera/camera.py b/homeassistant/components/rpi_camera/camera.py index d486f406e41..bf04e0ef492 100644 --- a/homeassistant/components/rpi_camera/camera.py +++ b/homeassistant/components/rpi_camera/camera.py @@ -1,14 +1,14 @@ """Camera platform that has a Raspberry Pi camera.""" -import os -import subprocess import logging +import os import shutil +import subprocess from tempfile import NamedTemporaryFile import voluptuous as vol -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_FILE_PATH, EVENT_HOMEASSISTANT_STOP +from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.const import CONF_FILE_PATH, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rpi_camera/manifest.json b/homeassistant/components/rpi_camera/manifest.json index d5443787d39..dd777790db2 100644 --- a/homeassistant/components/rpi_camera/manifest.json +++ b/homeassistant/components/rpi_camera/manifest.json @@ -1,6 +1,6 @@ { "domain": "rpi_camera", - "name": "Rpi camera", + "name": "Raspberry Pi Camera", "documentation": "https://www.home-assistant.io/integrations/rpi_camera", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/rpi_gpio/__init__.py b/homeassistant/components/rpi_gpio/__init__.py index ed7eefbb1fe..b2c3cf6b7bb 100644 --- a/homeassistant/components/rpi_gpio/__init__.py +++ b/homeassistant/components/rpi_gpio/__init__.py @@ -18,7 +18,7 @@ def setup(hass, config): GPIO.cleanup() def prepare_gpio(event): - """Stuff to do when home assistant starts.""" + """Stuff to do when Home Assistant starts.""" hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 4d3ea4da010..7b0f8e0f282 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -1,10 +1,8 @@ { "domain": "rpi_gpio", - "name": "Rpi gpio", + "name": "Raspberry Pi GPIO", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", - "requirements": [ - "RPi.GPIO==0.7.0" - ], + "requirements": ["RPi.GPIO==0.7.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rpi_gpio/switch.py b/homeassistant/components/rpi_gpio/switch.py index e9a46978eaf..03cb9f083ce 100644 --- a/homeassistant/components/rpi_gpio/switch.py +++ b/homeassistant/components/rpi_gpio/switch.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components import rpi_gpio +from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rpi_gpio_pwm/manifest.json b/homeassistant/components/rpi_gpio_pwm/manifest.json index ccff3d3357f..688cad8324e 100644 --- a/homeassistant/components/rpi_gpio_pwm/manifest.json +++ b/homeassistant/components/rpi_gpio_pwm/manifest.json @@ -1,10 +1,8 @@ { "domain": "rpi_gpio_pwm", - "name": "Rpi gpio pwm", + "name": "pigpio Daemon PWM LED", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio_pwm", - "requirements": [ - "pwmled==1.4.1" - ], + "requirements": ["pwmled==1.4.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rpi_pfio/__init__.py b/homeassistant/components/rpi_pfio/__init__.py index 72be34e0f45..d02b2bc5610 100644 --- a/homeassistant/components/rpi_pfio/__init__.py +++ b/homeassistant/components/rpi_pfio/__init__.py @@ -22,7 +22,7 @@ def setup(hass, config): PFIO.deinit() def prepare_pfio(event): - """Stuff to do when home assistant starts.""" + """Stuff to do when Home Assistant starts.""" hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_pfio) hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_pfio) diff --git a/homeassistant/components/rpi_pfio/manifest.json b/homeassistant/components/rpi_pfio/manifest.json index fd0a0a99f58..de2d70cc3a8 100644 --- a/homeassistant/components/rpi_pfio/manifest.json +++ b/homeassistant/components/rpi_pfio/manifest.json @@ -1,11 +1,8 @@ { "domain": "rpi_pfio", - "name": "Rpi pfio", + "name": "PiFace Digital I/O (PFIO)", "documentation": "https://www.home-assistant.io/integrations/rpi_pfio", - "requirements": [ - "pifacecommon==4.2.2", - "pifacedigitalio==3.0.5" - ], + "requirements": ["pifacecommon==4.2.2", "pifacedigitalio==3.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rpi_rf/manifest.json b/homeassistant/components/rpi_rf/manifest.json index 5914f65ef69..defb18cfa98 100644 --- a/homeassistant/components/rpi_rf/manifest.json +++ b/homeassistant/components/rpi_rf/manifest.json @@ -1,10 +1,8 @@ { "domain": "rpi_rf", - "name": "Rpi rf", + "name": "Raspberry Pi RF", "documentation": "https://www.home-assistant.io/integrations/rpi_rf", - "requirements": [ - "rpi-rf==0.9.7" - ], + "requirements": ["rpi-rf==0.9.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/rss_feed_template/__init__.py b/homeassistant/components/rss_feed_template/__init__.py index 440482ac31a..69bc9474267 100644 --- a/homeassistant/components/rss_feed_template/__init__.py +++ b/homeassistant/components/rss_feed_template/__init__.py @@ -1,7 +1,7 @@ """Support to export sensor values via RSS feed.""" from html import escape -from aiohttp import web +from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView diff --git a/homeassistant/components/rss_feed_template/manifest.json b/homeassistant/components/rss_feed_template/manifest.json index 7bd92876456..0dfab289920 100644 --- a/homeassistant/components/rss_feed_template/manifest.json +++ b/homeassistant/components/rss_feed_template/manifest.json @@ -1,10 +1,9 @@ { "domain": "rss_feed_template", - "name": "Rss feed template", + "name": "RSS Feed Template", "documentation": "https://www.home-assistant.io/integrations/rss_feed_template", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/rtorrent/manifest.json b/homeassistant/components/rtorrent/manifest.json index 899ee3a8ae8..67fd57fe170 100644 --- a/homeassistant/components/rtorrent/manifest.json +++ b/homeassistant/components/rtorrent/manifest.json @@ -1,6 +1,6 @@ { "domain": "rtorrent", - "name": "Rtorrent", + "name": "rTorrent", "documentation": "https://www.home-assistant.io/integrations/rtorrent", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index ed16331e912..4ae272ca9bd 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -6,14 +6,14 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_URL, - CONF_NAME, CONF_MONITORED_VARIABLES, + CONF_NAME, + CONF_URL, STATE_IDLE, ) -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 9404153f4e5..38ca2095cfb 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -1,10 +1,8 @@ { "domain": "russound_rio", - "name": "Russound rio", + "name": "Russound RIO", "documentation": "https://www.home-assistant.io/integrations/russound_rio", - "requirements": [ - "russound_rio==0.1.7" - ], + "requirements": ["russound_rio==0.1.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index fdcd308618a..c954553160e 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -4,7 +4,7 @@ import logging from russound_rio import Russound import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_SELECT_SOURCE, diff --git a/homeassistant/components/russound_rnet/manifest.json b/homeassistant/components/russound_rnet/manifest.json index 687dd05b61d..bb417122f86 100644 --- a/homeassistant/components/russound_rnet/manifest.json +++ b/homeassistant/components/russound_rnet/manifest.json @@ -1,10 +1,8 @@ { "domain": "russound_rnet", - "name": "Russound rnet", + "name": "Russound RNET", "documentation": "https://www.home-assistant.io/integrations/russound_rnet", - "requirements": [ - "russound==0.1.9" - ], + "requirements": ["russound==0.1.9"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index e69c227f148..78cfd4aa1f0 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -1,10 +1,8 @@ { "domain": "sabnzbd", - "name": "Sabnzbd", + "name": "SABnzbd", "documentation": "https://www.home-assistant.io/integrations/sabnzbd", - "requirements": [ - "pysabnzbd==1.1.0" - ], + "requirements": ["pysabnzbd==1.1.0"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index 02d83916d50..f1c50d5dbe3 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -1,12 +1,8 @@ { "domain": "saj", - "name": "SAJ", + "name": "SAJ Solar Inverter", "documentation": "https://www.home-assistant.io/integrations/saj", - "requirements": [ - "pysaj==0.0.14" - ], + "requirements": ["pysaj==0.0.14"], "dependencies": [], - "codeowners": [ - "@fredericvl" - ] + "codeowners": ["@fredericvl"] } diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index 52ae3640a7f..704e9996d2d 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -9,8 +9,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PASSWORD, CONF_NAME, + CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, DEVICE_CLASS_POWER, diff --git a/homeassistant/components/samsungtv/.translations/da.json b/homeassistant/components/samsungtv/.translations/da.json new file mode 100644 index 00000000000..594127688c2 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/da.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Dette Samsung-tv er allerede konfigureret.", + "already_in_progress": "Samsung-tv-konfiguration er allerede i gang.", + "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv.", + "not_found": "Der blev ikke fundet nogen underst\u00f8ttede Samsung-tv-enheder p\u00e5 netv\u00e6rket.", + "not_supported": "Dette Samsung TV underst\u00f8ttes i \u00f8jeblikket ikke." + }, + "step": { + "confirm": { + "description": "Vil du konfigurere Samsung-tv {model}? Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse. Manuelle konfigurationer for dette tv vil blive overskrevet.", + "title": "Samsung-tv" + }, + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse", + "name": "Navn" + }, + "description": "Indtast dine Samsung-tv-oplysninger. Hvis du aldrig har oprettet forbindelse til Home Assistant f\u00f8r, b\u00f8r du se en popup p\u00e5 dit tv, der beder om godkendelse.", + "title": "Samsung-tv" + } + }, + "title": "Samsung-tv" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/en.json b/homeassistant/components/samsungtv/.translations/en.json new file mode 100644 index 00000000000..24ab81c007c --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "This Samsung TV is already configured.", + "already_in_progress": "Samsung TV configuration is already in progress.", + "auth_missing": "Home Assistant is not authenticated to connect to this Samsung TV.", + "not_found": "No supported Samsung TV devices found on the network.", + "not_supported": "This Samsung TV devices is currently not supported." + }, + "step": { + "confirm": { + "description": "Do you want to set up Samsung TV {model}? If you never connected Home Assistant before you should see a popup on your TV asking for authentication. Manual configurations for this TV will be overwritten.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host or IP address", + "name": "Name" + }, + "description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authentication.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/es.json b/homeassistant/components/samsungtv/.translations/es.json new file mode 100644 index 00000000000..3535d4bc65f --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Este televisor Samsung ya est\u00e1 configurado.", + "already_in_progress": "La configuraci\u00f3n del televisor Samsung ya est\u00e1 en progreso.", + "auth_missing": "Home Assistant no est\u00e1 autenticado para conectarse a este televisor Samsung.", + "not_found": "No se encontraron televisiones Samsung compatibles en la red.", + "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar el televisor Samsung {model} ? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "name": "Nombre" + }, + "description": "Introduce la informaci\u00f3n de tu televisor Samsung. Si nunca conect\u00f3 Home Assistant antes de ver una ventana emergente en su televisor pidiendo autenticaci\u00f3n.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/it.json b/homeassistant/components/samsungtv/.translations/it.json new file mode 100644 index 00000000000..c783db24720 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Questo Samsung TV \u00e8 gi\u00e0 configurato.", + "already_in_progress": "La configurazione di Samsung TV \u00e8 gi\u00e0 in corso.", + "auth_missing": "Home Assistant non \u00e8 autenticato per connettersi a questo Samsung TV.", + "not_found": "Nessun dispositivo Samsung TV supportato trovato sulla rete.", + "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." + }, + "step": { + "confirm": { + "description": "Vuoi configurare Samsung TV {model} ? Se non hai mai collegato Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione. Le configurazioni manuali per questo televisore verranno sovrascritte.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host o indirizzo IP", + "name": "Nome" + }, + "description": "Inserisci le informazioni del tuo Samsung TV. Se non hai mai connesso Home Assistant dovresti vedere un popup sul televisore in cui viene richiesta l'autenticazione.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/lb.json b/homeassistant/components/samsungtv/.translations/lb.json new file mode 100644 index 00000000000..fe1f02e55ea --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebs Samsung TV ass scho konfigur\u00e9iert.", + "already_in_progress": "Konfiguratioun fir d\u00ebs Samsung TV ass schonn am gaang.", + "auth_missing": "Home Assistant ass net authentifiz\u00e9iert fir sech mat d\u00ebsem Samsung TV ze verbannen.", + "not_found": "Keng \u00ebnnerst\u00ebtzte Samsung TV am Netzwierk fonnt.", + "not_supported": "D\u00ebsen Samsung TV Modell g\u00ebtt momentan net \u00ebnnerst\u00ebtzt" + }, + "step": { + "confirm": { + "description": "W\u00ebllt dir de Samsung TV {model} ariichten?. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen. Manuell Konfiguratioun g\u00ebtt iwwerschriwwen.", + "title": "Samsnung TV" + }, + "user": { + "data": { + "host": "Numm oder IP Adresse", + "name": "Numm" + }, + "description": "Gitt \u00e4r Samsung TV Informatiounen un. Falls dir Home Assistant nach ni domat verbonnen hutt misst den TV eng Meldung mat enger Authentifiz\u00e9ierung uweisen.", + "title": "Samsnung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/no.json b/homeassistant/components/samsungtv/.translations/no.json new file mode 100644 index 00000000000..dcd437642b2 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Samsung TV-en er allerede konfigurert.", + "already_in_progress": "Samsung TV-konfigurasjon p\u00e5g\u00e5r allerede.", + "auth_missing": "Home Assistant er ikke autentisert for \u00e5 koble til denne Samsung TV-en.", + "not_found": "Ingen st\u00f8ttede Samsung TV-enheter funnet i nettverket.", + "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." + }, + "step": { + "confirm": { + "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Vert eller IP-adresse", + "name": "Navn" + }, + "description": "Skriv inn Samsung TV-informasjonen din. Hvis du aldri koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", + "title": "Samsung TV" + } + }, + "title": "Samsung TV" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/ru.json b/homeassistant/components/samsungtv/.translations/ru.json new file mode 100644 index 00000000000..d5dd11a1b80 --- /dev/null +++ b/homeassistant/components/samsungtv/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "not_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", + "not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung {model}? \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043d\u044b\u0435 \u0432\u0440\u0443\u0447\u043d\u0443\u044e, \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u044b.", + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + }, + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0435 Samsung. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e\u0442 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0440\u0430\u043d\u0435\u0435 \u043d\u0435 \u0431\u044b\u043b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a Home Assistant, \u043d\u0430 \u044d\u043a\u0440\u0430\u043d\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0434\u043e\u043b\u0436\u043d\u043e \u043f\u043e\u044f\u0432\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u043f\u043b\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u043e\u043a\u043d\u043e \u0441 \u0437\u0430\u043f\u0440\u043e\u0441\u043e\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + } + }, + "title": "\u0422\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 Samsung" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 405d757cbef..d8db31db728 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -1,11 +1,8 @@ { "domain": "samsungtv", - "name": "Samsung TV", + "name": "Samsung Smart TV", "documentation": "https://www.home-assistant.io/integrations/samsungtv", - "requirements": [ - "samsungctl[websocket]==0.7.1", - "wakeonlan==1.1.6" - ], + "requirements": ["samsungctl[websocket]==0.7.1", "wakeonlan==1.1.6"], "dependencies": [], "codeowners": ["@escoand"] } diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 2488d5ab913..fd900fedec1 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -3,15 +3,15 @@ import asyncio from datetime import timedelta import socket -from samsungctl import exceptions as samsung_exceptions, Remote as SamsungRemote +from samsungctl import Remote as SamsungRemote, exceptions as samsung_exceptions import voluptuous as vol import wakeonlan from websocket import WebSocketException from homeassistant.components.media_player import ( - MediaPlayerDevice, - PLATFORM_SCHEMA, DEVICE_CLASS_TV, + PLATFORM_SCHEMA, + MediaPlayerDevice, ) from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, @@ -344,7 +344,7 @@ class SamsungTVDevice(MediaPlayerDevice): return for digit in media_id: - await self.hass.async_add_job(self.send_key, "KEY_" + digit) + await self.hass.async_add_job(self.send_key, f"KEY_{digit}") await asyncio.sleep(KEY_PRESS_TIMEOUT, self.hass.loop) await self.hass.async_add_job(self.send_key, "KEY_ENTER") diff --git a/homeassistant/components/satel_integra/__init__.py b/homeassistant/components/satel_integra/__init__.py index 1972eefd6b5..0b007f63e01 100644 --- a/homeassistant/components/satel_integra/__init__.py +++ b/homeassistant/components/satel_integra/__init__.py @@ -62,9 +62,7 @@ PARTITION_SCHEMA = vol.Schema( def is_alarm_code_necessary(value): """Check if alarm code must be configured.""" if value.get(CONF_SWITCHABLE_OUTPUTS) and CONF_DEVICE_CODE not in value: - raise vol.Invalid( - "You need to specify alarm " " code to use switchable_outputs" - ) + raise vol.Invalid("You need to specify alarm code to use switchable_outputs") return value @@ -152,7 +150,7 @@ async def async_setup(hass, config): @callback def alarm_status_update_callback(): - """Send status update received from alarm to home assistant.""" + """Send status update received from alarm to Home Assistant.""" _LOGGER.debug("Sending request to update panel state") async_dispatcher_send(hass, SIGNAL_PANEL_MESSAGE) diff --git a/homeassistant/components/satel_integra/manifest.json b/homeassistant/components/satel_integra/manifest.json index dbbfebeaccb..e9bbd9623c0 100644 --- a/homeassistant/components/satel_integra/manifest.json +++ b/homeassistant/components/satel_integra/manifest.json @@ -1,10 +1,8 @@ { "domain": "satel_integra", - "name": "Satel integra", + "name": "Satel Integra", "documentation": "https://www.home-assistant.io/integrations/satel_integra", - "requirements": [ - "satel_integra==0.3.4" - ], + "requirements": ["satel_integra==0.3.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/satel_integra/switch.py b/homeassistant/components/satel_integra/switch.py index c20f30cf871..9233b3d152d 100644 --- a/homeassistant/components/satel_integra/switch.py +++ b/homeassistant/components/satel_integra/switch.py @@ -69,14 +69,14 @@ class SatelIntegraSwitch(SwitchDevice): async def async_turn_on(self, **kwargs): """Turn the device on.""" - _LOGGER.debug("Switch: %s status: %s," " turning on", self._name, self._state) + _LOGGER.debug("Switch: %s status: %s, turning on", self._name, self._state) await self._satel.set_output(self._code, self._device_number, True) self.async_schedule_update_ha_state() async def async_turn_off(self, **kwargs): """Turn the device off.""" _LOGGER.debug( - "Switch name: %s status: %s," " turning off", self._name, self._state + "Switch name: %s status: %s, turning off", self._name, self._state ) await self._satel.set_output(self._code, self._device_number, False) self.async_schedule_update_ha_state() diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 63a64f34fe9..75ec2bfd875 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -4,12 +4,11 @@ import logging import voluptuous as vol -from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON +from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent - # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "scene" diff --git a/homeassistant/components/scene/manifest.json b/homeassistant/components/scene/manifest.json index b80e4de5041..1b0361680f4 100644 --- a/homeassistant/components/scene/manifest.json +++ b/homeassistant/components/scene/manifest.json @@ -1,10 +1,9 @@ { "domain": "scene", - "name": "Scene", + "name": "Scenes", "documentation": "https://www.home-assistant.io/integrations/scene", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 5fdcca372b9..e0800cdef27 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -2,11 +2,7 @@ "domain": "scrape", "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", - "requirements": [ - "beautifulsoup4==4.8.1" - ], + "requirements": ["beautifulsoup4==4.8.2"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 0bfb7351c88..13d99a0cb8f 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -2,27 +2,27 @@ import logging from bs4 import BeautifulSoup -import voluptuous as vol from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.rest.sensor import RestData +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_AUTHENTICATION, + CONF_HEADERS, CONF_NAME, + CONF_PASSWORD, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, + CONF_USERNAME, CONF_VALUE_TEMPLATE, CONF_VERIFY_SSL, - CONF_USERNAME, - CONF_HEADERS, - CONF_PASSWORD, - CONF_AUTHENTICATION, HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION, ) -from homeassistant.helpers.entity import Entity from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index cb9cb5194ba..1d180b54cfd 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -6,23 +6,22 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - SERVICE_TOGGLE, - SERVICE_RELOAD, - STATE_ON, + ATTR_NAME, CONF_ALIAS, EVENT_SCRIPT_STARTED, - ATTR_NAME, + SERVICE_RELOAD, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, ) -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity import ToggleEntity -from homeassistant.helpers.entity_component import EntityComponent import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema -from homeassistant.helpers.service import async_set_service_schema - +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.script import Script +from homeassistant.helpers.service import async_set_service_schema +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -39,8 +38,6 @@ CONF_SEQUENCE = "sequence" ENTITY_ID_FORMAT = DOMAIN + ".{}" -GROUP_NAME_ALL_SCRIPTS = "all scripts" - SCRIPT_ENTRY_SCHEMA = vol.Schema( { CONF_ALIAS: cv.string, @@ -74,9 +71,7 @@ def is_on(hass, entity_id): async def async_setup(hass, config): """Load the scripts from the configuration.""" - component = EntityComponent( - _LOGGER, DOMAIN, hass, group_name=GROUP_NAME_ALL_SCRIPTS - ) + component = EntityComponent(_LOGGER, DOMAIN, hass) await _async_process_config(hass, config, component) @@ -218,7 +213,7 @@ class ScriptEntity(ToggleEntity): self.script.async_stop() async def async_will_remove_from_hass(self): - """Stop script and remove service when it will be removed from HASS.""" + """Stop script and remove service when it will be removed from Home Assistant.""" if self.script.is_running: self.script.async_stop() diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index 51ce17c500a..dac37110172 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -1,12 +1,9 @@ { "domain": "script", - "name": "Script", + "name": "Scripts", "documentation": "https://www.home-assistant.io/integrations/script", "requirements": [], - "dependencies": [ - "group" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["group"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/scsgate/manifest.json b/homeassistant/components/scsgate/manifest.json index b6334272f23..a25dc2f8803 100644 --- a/homeassistant/components/scsgate/manifest.json +++ b/homeassistant/components/scsgate/manifest.json @@ -1,10 +1,8 @@ { "domain": "scsgate", - "name": "Scsgate", + "name": "SCSGate", "documentation": "https://www.home-assistant.io/integrations/scsgate", - "requirements": [ - "scsgate==0.1.0" - ], + "requirements": ["scsgate==0.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/season/manifest.json b/homeassistant/components/season/manifest.json index 445c2bc3827..ca4edaf76a9 100644 --- a/homeassistant/components/season/manifest.json +++ b/homeassistant/components/season/manifest.json @@ -2,9 +2,8 @@ "domain": "season", "name": "Season", "documentation": "https://www.home-assistant.io/integrations/season", - "requirements": [ - "ephem==3.7.7.0" - ], + "requirements": ["ephem==3.7.7.0"], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index f9bd8cc31b4..8a87205d4b7 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -1,10 +1,8 @@ { "domain": "sendgrid", - "name": "Sendgrid", + "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": [ - "sendgrid==6.1.0" - ], + "requirements": ["sendgrid==6.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/sendgrid/notify.py b/homeassistant/components/sendgrid/notify.py index f16758a5355..6dbf4d5c2b7 100644 --- a/homeassistant/components/sendgrid/notify.py +++ b/homeassistant/components/sendgrid/notify.py @@ -1,17 +1,8 @@ """SendGrid notification service.""" import logging -import voluptuous as vol - from sendgrid import SendGridAPIClient - -from homeassistant.const import ( - CONF_API_KEY, - CONF_RECIPIENT, - CONF_SENDER, - CONTENT_TYPE_TEXT_PLAIN, -) -import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.components.notify import ( ATTR_TITLE, @@ -19,6 +10,13 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import ( + CONF_API_KEY, + CONF_RECIPIENT, + CONF_SENDER, + CONTENT_TYPE_TEXT_PLAIN, +) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 0112ca28469..e27d4bb72f6 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,9 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": [ - "sense_energy==0.7.0" - ], + "requirements": ["sense_energy==0.7.0"], "dependencies": [], "codeowners": ["@kbickar"] } diff --git a/homeassistant/components/sensehat/manifest.json b/homeassistant/components/sensehat/manifest.json index deec8c23ab7..a56c1c57765 100644 --- a/homeassistant/components/sensehat/manifest.json +++ b/homeassistant/components/sensehat/manifest.json @@ -1,10 +1,8 @@ { "domain": "sensehat", - "name": "Sensehat", + "name": "Sense HAT", "documentation": "https://www.home-assistant.io/integrations/sensehat", - "requirements": [ - "sense-hat==2.2.0" - ], + "requirements": ["sense-hat==2.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/sensibo/climate.py b/homeassistant/components/sensibo/climate.py index a14bdb49133..08e2212e2a2 100644 --- a/homeassistant/components/sensibo/climate.py +++ b/homeassistant/components/sensibo/climate.py @@ -5,16 +5,16 @@ import logging import aiohttp import async_timeout -import voluptuous as vol import pysensibo +import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, @@ -65,7 +65,7 @@ _FETCH_FIELDS = ",".join( "temperatureUnit", ] ) -_INITIAL_FETCH_FIELDS = "id," + _FETCH_FIELDS +_INITIAL_FETCH_FIELDS = f"id,{_FETCH_FIELDS}" FIELD_TO_FLAG = { "fanLevel": SUPPORT_FAN_MODE, diff --git a/homeassistant/components/sensibo/manifest.json b/homeassistant/components/sensibo/manifest.json index 9b49f442d15..bcc89e76a69 100644 --- a/homeassistant/components/sensibo/manifest.json +++ b/homeassistant/components/sensibo/manifest.json @@ -2,11 +2,7 @@ "domain": "sensibo", "name": "Sensibo", "documentation": "https://www.home-assistant.io/integrations/sensibo", - "requirements": [ - "pysensibo==1.0.3" - ], + "requirements": ["pysensibo==1.0.3"], "dependencies": [], - "codeowners": [ - "@andrey-git" - ] + "codeowners": ["@andrey-git"] } diff --git a/homeassistant/components/sensor/.translations/da.json b/homeassistant/components/sensor/.translations/da.json index df9b9935dc1..3febed8ac09 100644 --- a/homeassistant/components/sensor/.translations/da.json +++ b/homeassistant/components/sensor/.translations/da.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} batteriniveau", - "is_humidity": "{entity_name} fugtighed", - "is_illuminance": "{entity_name} belysningsstyrke", - "is_power": "{entity_name} str\u00f8m", - "is_pressure": "{entity_name} tryk", - "is_signal_strength": "{entity_name} signalstyrke", - "is_temperature": "{entity_name} temperatur", - "is_timestamp": "{entity_name} tidsstempel", - "is_value": "{entity_name} v\u00e6rdi" + "is_battery_level": "Aktuelt {entity_name}-batteriniveau", + "is_humidity": "Aktuel {entity_name}-luftfugtighed", + "is_illuminance": "Aktuel {entity_name}-lysstyrke", + "is_power": "Aktuel {entity_name}-str\u00f8m", + "is_pressure": "Aktuelt {entity_name}-lufttryk", + "is_signal_strength": "Aktuel {entity_name}-signalstyrke", + "is_temperature": "Aktuel {entity_name}-temperatur", + "is_timestamp": "Aktuel {entity_name}-tidsstempel", + "is_value": "Aktuel {entity_name}-v\u00e6rdi" }, "trigger_type": { - "battery_level": "{entity_name} batteriniveau", - "humidity": "{entity_name} fugtighed", - "illuminance": "{entity_name} belysningsstyrke", - "power": "{entity_name} str\u00f8m", - "pressure": "{entity_name} tryk", - "signal_strength": "{entity_name} signalstyrke", - "temperature": "{entity_name} temperatur", - "timestamp": "{entity_name} tidsstempel", - "value": "{entity_name} v\u00e6rdi" + "battery_level": "{entity_name} batteriniveau \u00e6ndres", + "humidity": "{entity_name} luftfugtighed \u00e6ndres", + "illuminance": "{entity_name} lysstyrke \u00e6ndres", + "power": "{entity_name} str\u00f8m \u00e6ndres", + "pressure": "{entity_name} lufttryk \u00e6ndres", + "signal_strength": "{entity_name} signalstyrke \u00e6ndres", + "temperature": "{entity_name} temperatur \u00e6ndres", + "timestamp": "{entity_name} tidsstempel \u00e6ndres", + "value": "{entity_name} v\u00e6rdi \u00e6ndres" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json index 7b8ef36efe1..3c957c715d9 100644 --- a/homeassistant/components/sensor/.translations/es.json +++ b/homeassistant/components/sensor/.translations/es.json @@ -17,7 +17,7 @@ "illuminance": "Cambios de luminosidad de {entity_name}", "power": "Cambios de potencia de {entity_name}", "pressure": "Cambios de presi\u00f3n de {entity_name}", - "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name} ", + "signal_strength": "cambios de la intensidad de se\u00f1al de {entity_name}", "temperature": "{entity_name} cambios de temperatura", "timestamp": "{entity_name} cambios de fecha y hora", "value": "Cambios de valor de la {entity_name}" diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index 1ce2592410d..cc35f93cb51 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -4,23 +4,23 @@ "is_battery_level": "Niveau de la batterie de {entity_name}", "is_humidity": "Humidit\u00e9 de {entity_name}", "is_illuminance": "\u00c9clairement de {entity_name}", - "is_power": "{entity_name} puissance", - "is_pressure": "{entity_name} pression", - "is_signal_strength": "{entity_name} force du signal", - "is_temperature": "La temp\u00e9rature de {entity_name}", - "is_timestamp": "{entity_name} horodatage", - "is_value": "La valeur actuelle de {entity_name} " + "is_power": "Puissance de {entity_name}", + "is_pressure": "Pression de {entity_name}", + "is_signal_strength": "Force du signal de {entity_name}", + "is_temperature": "Temp\u00e9rature de {entity_name}", + "is_timestamp": "Horodatage de {entity_name}", + "is_value": "La valeur actuelle de {entity_name}" }, "trigger_type": { - "battery_level": "Le niveau de la batterie de {entity_name}", - "humidity": "L'humidit\u00e9 de {entity_name}", - "illuminance": "L'\u00e9clairement de {entity_name}", - "power": "{entity_name} puissance", - "pressure": "{entity_name} pression", - "signal_strength": "{entity_name} force du signal", - "temperature": "La temp\u00e9rature de {entity_name}", - "timestamp": "{entity_name} horodatage", - "value": "Changements de valeur de {entity_name} " + "battery_level": "{entity_name} modification du niveau de batterie", + "humidity": "{entity_name} modification de l'humidit\u00e9", + "illuminance": "{entity_name} modification de l'\u00e9clairement", + "power": "{entity_name} modification de la puissance", + "pressure": "{entity_name} modification de la pression", + "signal_strength": "{entity_name} modification de la force du signal", + "temperature": "{entity_name} modification de temp\u00e9rature", + "timestamp": "{entity_name} modification d'horodatage", + "value": "Changements de valeur de {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json index cb643bbdd29..7c6bed1e033 100644 --- a/homeassistant/components/sensor/.translations/it.json +++ b/homeassistant/components/sensor/.translations/it.json @@ -14,12 +14,12 @@ "trigger_type": { "battery_level": "variazioni del livello di batteria di {entity_name} ", "humidity": "variazioni di umidit\u00e0 di {entity_name} ", - "illuminance": "variazioni dell'illuminazione di {entity_name} ", + "illuminance": "variazioni dell'illuminazione di {entity_name}", "power": "variazioni di alimentazione di {entity_name}", "pressure": "variazioni della pressione di {entity_name}", - "signal_strength": "variazioni della potenza del segnale di {entity_name} ", - "temperature": "variazioni di temperatura di {entity_name} ", - "timestamp": "variazioni di data e ora di {entity_name} ", + "signal_strength": "variazioni della potenza del segnale di {entity_name}", + "temperature": "variazioni di temperatura di {entity_name}", + "timestamp": "variazioni di data e ora di {entity_name}", "value": "{entity_name} valori cambiati" } } diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json index 0e74f3f4f89..7716cc016c3 100644 --- a/homeassistant/components/sensor/.translations/ko.json +++ b/homeassistant/components/sensor/.translations/ko.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", - "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4", - "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4", - "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825", - "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825", - "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4", - "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4", - "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01", - "is_value": "\ud604\uc7ac {entity_name} \uac12" + "is_battery_level": "\ud604\uc7ac {entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 ~ \uc774\uba74", + "is_humidity": "\ud604\uc7ac {entity_name} \uc2b5\ub3c4\uac00 ~ \uc774\uba74", + "is_illuminance": "\ud604\uc7ac {entity_name} \uc870\ub3c4\uac00 ~ \uc774\uba74", + "is_power": "\ud604\uc7ac {entity_name} \uc18c\ube44 \uc804\ub825\uc774 ~ \uc774\uba74", + "is_pressure": "\ud604\uc7ac {entity_name} \uc555\ub825\uc774 ~ \uc774\uba74", + "is_signal_strength": "\ud604\uc7ac {entity_name} \uc2e0\ud638 \uac15\ub3c4\uac00 ~ \uc774\uba74", + "is_temperature": "\ud604\uc7ac {entity_name} \uc628\ub3c4\uac00 ~ \uc774\uba74", + "is_timestamp": "\ud604\uc7ac {entity_name} \uc2dc\uac01\uc774 ~ \uc774\uba74", + "is_value": "\ud604\uc7ac {entity_name} \uac12\uc774 ~ \uc774\uba74" }, "trigger_type": { - "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubcc0\ud654", - "humidity": "{entity_name} \uc2b5\ub3c4 \ubcc0\ud654", - "illuminance": "{entity_name} \uc870\ub3c4 \ubcc0\ud654", - "power": "{entity_name} \uc18c\ube44 \uc804\ub825 \ubcc0\ud654", - "pressure": "{entity_name} \uc555\ub825 \ubcc0\ud654", - "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4 \ubcc0\ud654", - "temperature": "{entity_name} \uc628\ub3c4 \ubcc0\ud654", - "timestamp": "{entity_name} \uc2dc\uac01 \ubcc0\ud654", - "value": "{entity_name} \uac12 \ubcc0\ud654" + "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubc14\ub014 \ub54c", + "humidity": "{entity_name} \uc2b5\ub3c4\uac00 \ubc14\ub014 \ub54c", + "illuminance": "{entity_name} \uc870\ub3c4\uac00 \ubc14\ub014 \ub54c", + "power": "{entity_name} \uc18c\ube44 \uc804\ub825\uc774 \ubc14\ub014 \ub54c", + "pressure": "{entity_name} \uc555\ub825\uc774 \ubc14\ub014 \ub54c", + "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4\uac00 \ubc14\ub014 \ub54c", + "temperature": "{entity_name} \uc628\ub3c4\uac00 \ubc14\ub014 \ub54c", + "timestamp": "{entity_name} \uc2dc\uac01\uc774 \ubc14\ub014 \ub54c", + "value": "{entity_name} \uac12\uc774 \ubc14\ub014 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json index 1c0fc108510..010f835cf3d 100644 --- a/homeassistant/components/sensor/.translations/no.json +++ b/homeassistant/components/sensor/.translations/no.json @@ -4,7 +4,7 @@ "is_battery_level": "Gjeldende {entity_name} batteriniv\u00e5", "is_humidity": "Gjeldende {entity_name} fuktighet", "is_illuminance": "Gjeldende {entity_name} belysningsstyrke", - "is_power": "Gjeldende {entity_name} str\u00f8m", + "is_power": "Gjeldende {entity_name}-effekt", "is_pressure": "Gjeldende {entity_name} trykk", "is_signal_strength": "Gjeldende {entity_name} signalstyrke", "is_temperature": "Gjeldende {entity_name} temperatur", @@ -15,7 +15,7 @@ "battery_level": "{entity_name} batteriniv\u00e5 endres", "humidity": "{entity_name} fuktighets endringer", "illuminance": "{entity_name} belysningsstyrke endringer", - "power": "{entity_name} str\u00f8m endringer", + "power": "{entity_name} effektendringer", "pressure": "{entity_name} trykk endringer", "signal_strength": "{entity_name} signalstyrkeendringer", "temperature": "{entity_name} temperaturendringer", diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 53e4b0ffcf7..83711a07759 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -21,7 +21,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity_component import EntityComponent - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 259fb5dbab9..7417765f9f4 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -1,11 +1,11 @@ """Provides device conditions for sensors.""" from typing import Dict, List + import voluptuous as vol from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.core import HomeAssistant from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -22,16 +22,16 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, async_get_registry, ) -from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.typing import ConfigType from . import DOMAIN - # mypy: allow-untyped-defs, no-check-untyped-defs DEVICE_CLASS_NONE = "none" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 73e55340da9..1af8a5e4ab0 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -23,12 +23,11 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, ) -from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_registry import async_entries_for_device from . import DOMAIN - # mypy: allow-untyped-defs, no-check-untyped-defs DEVICE_CLASS_NONE = "none" diff --git a/homeassistant/components/sensor/manifest.json b/homeassistant/components/sensor/manifest.json index 1813d782d31..b57022b963c 100644 --- a/homeassistant/components/sensor/manifest.json +++ b/homeassistant/components/sensor/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sensor", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/sentry/.translations/ca.json b/homeassistant/components/sentry/.translations/ca.json new file mode 100644 index 00000000000..cf0ca26fdd3 --- /dev/null +++ b/homeassistant/components/sentry/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry ja est\u00e0 configurat" + }, + "error": { + "bad_dsn": "DSN inv\u00e0lid", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "description": "Introdueix el DSN de Sentry", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/da.json b/homeassistant/components/sentry/.translations/da.json new file mode 100644 index 00000000000..7377dfd8aea --- /dev/null +++ b/homeassistant/components/sentry/.translations/da.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry er allerede konfigureret" + }, + "error": { + "bad_dsn": "Ugyldigt DSN", + "unknown": "Uventet fejl" + }, + "step": { + "user": { + "description": "Indtast dit Sentry-DSN", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/de.json b/homeassistant/components/sentry/.translations/de.json new file mode 100644 index 00000000000..c1cd6496220 --- /dev/null +++ b/homeassistant/components/sentry/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry ist bereits konfiguriert" + }, + "error": { + "bad_dsn": "Ung\u00fcltiger DSN", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "description": "Geben Sie Ihren Sentry-DSN ein", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/en.json b/homeassistant/components/sentry/.translations/en.json new file mode 100644 index 00000000000..4d37438ff3f --- /dev/null +++ b/homeassistant/components/sentry/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry is already configured" + }, + "error": { + "bad_dsn": "Invalid DSN", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "description": "Enter your Sentry DSN", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/es.json b/homeassistant/components/sentry/.translations/es.json new file mode 100644 index 00000000000..7951076a95e --- /dev/null +++ b/homeassistant/components/sentry/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry ya est\u00e1 configurado" + }, + "error": { + "bad_dsn": "DSN no v\u00e1lido", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "description": "Introduzca su DSN Sentry", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/it.json b/homeassistant/components/sentry/.translations/it.json new file mode 100644 index 00000000000..4d0cd3178e7 --- /dev/null +++ b/homeassistant/components/sentry/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry \u00e8 gi\u00e0 configurato" + }, + "error": { + "bad_dsn": "DSN non valido", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "description": "Inserisci il tuo DSN Sentry", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/ko.json b/homeassistant/components/sentry/.translations/ko.json new file mode 100644 index 00000000000..b0dde032b73 --- /dev/null +++ b/homeassistant/components/sentry/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "bad_dsn": "DSN \uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "description": "Sentry DSN \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/lb.json b/homeassistant/components/sentry/.translations/lb.json new file mode 100644 index 00000000000..e91f57a1585 --- /dev/null +++ b/homeassistant/components/sentry/.translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry ass scho konfigur\u00e9iert" + }, + "error": { + "bad_dsn": "Ong\u00eblteg DSN", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "description": "Gitt \u00e4r Sentry DSN un", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/no.json b/homeassistant/components/sentry/.translations/no.json new file mode 100644 index 00000000000..79bb5f6cf87 --- /dev/null +++ b/homeassistant/components/sentry/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry er allerede konfigurert" + }, + "error": { + "bad_dsn": "Ugyldig datakildenavn (DSN)", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "description": "Fyll inn din Sentry DNS", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/pl.json b/homeassistant/components/sentry/.translations/pl.json new file mode 100644 index 00000000000..4bb7abbc328 --- /dev/null +++ b/homeassistant/components/sentry/.translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry jest ju\u017c skonfigurowane" + }, + "error": { + "bad_dsn": "Nieprawid\u0142owy DSN", + "unknown": "Niespodziewany b\u0142\u0105d" + }, + "step": { + "user": { + "description": "Wprowad\u017a DSN Sentry", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/pt-BR.json b/homeassistant/components/sentry/.translations/pt-BR.json new file mode 100644 index 00000000000..bf8bc5ef72e --- /dev/null +++ b/homeassistant/components/sentry/.translations/pt-BR.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "O Sentry j\u00e1 est\u00e1 configurado" + }, + "error": { + "bad_dsn": "DSN inv\u00e1lido", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "description": "Digite seu DSN Sentry" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/ru.json b/homeassistant/components/sentry/.translations/ru.json new file mode 100644 index 00000000000..ada10db6c82 --- /dev/null +++ b/homeassistant/components/sentry/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "bad_dsn": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 DSN.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448 DSN Sentry", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/sl.json b/homeassistant/components/sentry/.translations/sl.json new file mode 100644 index 00000000000..b99381722ad --- /dev/null +++ b/homeassistant/components/sentry/.translations/sl.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry je \u017ee nastavljen" + }, + "error": { + "bad_dsn": "Neveljaven DSN", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "description": "Vpi\u0161ite va\u0161 Sentry DSN", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/zh-Hant.json b/homeassistant/components/sentry/.translations/zh-Hant.json new file mode 100644 index 00000000000..f1599b58be6 --- /dev/null +++ b/homeassistant/components/sentry/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "bad_dsn": "DSN \u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "description": "\u8f38\u5165 Sentry DSN", + "title": "Sentry" + } + }, + "title": "Sentry" + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/__init__.py b/homeassistant/components/sentry/__init__.py new file mode 100644 index 00000000000..9c73de34af8 --- /dev/null +++ b/homeassistant/components/sentry/__init__.py @@ -0,0 +1,56 @@ +"""The sentry integration.""" +import logging + +import sentry_sdk +from sentry_sdk.integrations.logging import LoggingIntegration +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_validation as cv + +from .const import CONF_DSN, CONF_ENVIRONMENT, DOMAIN + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + {vol.Required(CONF_DSN): cv.string, CONF_ENVIRONMENT: cv.string} + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Sentry component.""" + conf = config.get(DOMAIN) + if conf is not None: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Sentry from a config entry.""" + conf = entry.data + + hass.data[DOMAIN] = conf + + # https://docs.sentry.io/platforms/python/logging/ + sentry_logging = LoggingIntegration( + level=logging.INFO, # Capture info and above as breadcrumbs + event_level=logging.ERROR, # Send errors as events + ) + + sentry_sdk.init( + dsn=conf.get(CONF_DSN), + environment=conf.get(CONF_ENVIRONMENT), + integrations=[sentry_logging], + ) + + return True diff --git a/homeassistant/components/sentry/config_flow.py b/homeassistant/components/sentry/config_flow.py new file mode 100644 index 00000000000..194aa527d63 --- /dev/null +++ b/homeassistant/components/sentry/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow for sentry integration.""" +import logging + +from sentry_sdk.utils import BadDsn, Dsn +import voluptuous as vol + +from homeassistant import config_entries, core + +from .const import CONF_DSN, DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_DSN): str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the DSN input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + # validate the dsn + Dsn(data["dsn"]) + + return {"title": "Sentry"} + + +class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Sentry config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a user config flow.""" + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=info["title"], data=user_input) + except BadDsn: + errors["base"] = "bad_dsn" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) diff --git a/homeassistant/components/sentry/const.py b/homeassistant/components/sentry/const.py new file mode 100644 index 00000000000..1f799eb7fb9 --- /dev/null +++ b/homeassistant/components/sentry/const.py @@ -0,0 +1,6 @@ +"""Constants for the sentry integration.""" + +DOMAIN = "sentry" + +CONF_DSN = "dsn" +CONF_ENVIRONMENT = "environment" diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json new file mode 100644 index 00000000000..6a7428f7ea1 --- /dev/null +++ b/homeassistant/components/sentry/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "sentry", + "name": "Sentry", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/sentry", + "requirements": ["sentry-sdk==0.13.5"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@dcramer"] +} diff --git a/homeassistant/components/sentry/strings.json b/homeassistant/components/sentry/strings.json new file mode 100644 index 00000000000..8d5042731ca --- /dev/null +++ b/homeassistant/components/sentry/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Sentry", + "step": { + "user": { + "title": "Sentry", + "description": "Enter your Sentry DSN" + } + }, + "error": { + "unknown": "Unexpected error", + "bad_dsn": "Invalid DSN" + }, + "abort": { + "already_configured": "Sentry is already configured" + } + } +} diff --git a/homeassistant/components/serial/manifest.json b/homeassistant/components/serial/manifest.json index 61da7b4ff30..fa536a4c508 100644 --- a/homeassistant/components/serial/manifest.json +++ b/homeassistant/components/serial/manifest.json @@ -2,11 +2,7 @@ "domain": "serial", "name": "Serial", "documentation": "https://www.home-assistant.io/integrations/serial", - "requirements": [ - "pyserial-asyncio==0.4" - ], + "requirements": ["pyserial-asyncio==0.4"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/serial_pm/manifest.json b/homeassistant/components/serial_pm/manifest.json index b326481c55b..9e8f39ced79 100644 --- a/homeassistant/components/serial_pm/manifest.json +++ b/homeassistant/components/serial_pm/manifest.json @@ -1,10 +1,8 @@ { "domain": "serial_pm", - "name": "Serial pm", + "name": "Serial Particulate Matter", "documentation": "https://www.home-assistant.io/integrations/serial_pm", - "requirements": [ - "pmsensor==0.4" - ], + "requirements": ["pmsensor==0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/sesame/manifest.json b/homeassistant/components/sesame/manifest.json index f689cd46858..720e33b9cd9 100644 --- a/homeassistant/components/sesame/manifest.json +++ b/homeassistant/components/sesame/manifest.json @@ -2,9 +2,7 @@ "domain": "sesame", "name": "Sesame Smart Lock", "documentation": "https://www.home-assistant.io/integrations/sesame", - "requirements": [ - "pysesame2==1.0.1" - ], + "requirements": ["pysesame2==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 6e375da0322..672679b7254 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -1,6 +1,6 @@ { "domain": "seven_segments", - "name": "Seven segments", + "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", "requirements": ["pillow==6.2.1"], "dependencies": [], diff --git a/homeassistant/components/seventeentrack/manifest.json b/homeassistant/components/seventeentrack/manifest.json index 6a5ac165291..c5082b8c05f 100644 --- a/homeassistant/components/seventeentrack/manifest.json +++ b/homeassistant/components/seventeentrack/manifest.json @@ -1,12 +1,8 @@ { "domain": "seventeentrack", - "name": "Seventeentrack", + "name": "17TRACK", "documentation": "https://www.home-assistant.io/integrations/seventeentrack", - "requirements": [ - "py17track==2.2.2" - ], + "requirements": ["py17track==2.2.2"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/seventeentrack/sensor.py b/homeassistant/components/seventeentrack/sensor.py index 167f4347c0c..000019abb51 100644 --- a/homeassistant/components/seventeentrack/sensor.py +++ b/homeassistant/components/seventeentrack/sensor.py @@ -45,7 +45,7 @@ ENTITY_ID_TEMPLATE = "sensor.seventeentrack_package_{0}" NOTIFICATION_DELIVERED_ID = "package_delivered_{0}" NOTIFICATION_DELIVERED_TITLE = "Package {0} delivered" NOTIFICATION_DELIVERED_MESSAGE = ( - "Package Delivered: {0}
" + "Visit 17.track for more information: " + "Package Delivered: {0}
Visit 17.track for more information: " "https://t.17track.net/track#nums={1}" ) @@ -129,7 +129,7 @@ class SeventeenTrackSummarySensor(Entity): @property def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return "summary_{0}_{1}".format(self._data.account_id, slugify(self._status)) @property @@ -212,7 +212,7 @@ class SeventeenTrackPackageSensor(Entity): @property def unique_id(self): - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return UNIQUE_ID_TEMPLATE.format(self._data.account_id, self._tracking_number) async def async_update(self): diff --git a/homeassistant/components/shell_command/__init__.py b/homeassistant/components/shell_command/__init__.py index 42057407814..89a1a20e8e4 100644 --- a/homeassistant/components/shell_command/__init__.py +++ b/homeassistant/components/shell_command/__init__.py @@ -5,8 +5,8 @@ import shlex import voluptuous as vol -from homeassistant.exceptions import TemplateError from homeassistant.core import ServiceCall +from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.typing import ConfigType, HomeAssistantType diff --git a/homeassistant/components/shell_command/manifest.json b/homeassistant/components/shell_command/manifest.json index ca354ff2331..e05824a87a9 100644 --- a/homeassistant/components/shell_command/manifest.json +++ b/homeassistant/components/shell_command/manifest.json @@ -1,10 +1,9 @@ { "domain": "shell_command", - "name": "Shell command", + "name": "Shell Command", "documentation": "https://www.home-assistant.io/integrations/shell_command", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/shiftr/manifest.json b/homeassistant/components/shiftr/manifest.json index b228909a59e..dc9ec618a79 100644 --- a/homeassistant/components/shiftr/manifest.json +++ b/homeassistant/components/shiftr/manifest.json @@ -1,12 +1,8 @@ { "domain": "shiftr", - "name": "Shiftr", + "name": "shiftr.io", "documentation": "https://www.home-assistant.io/integrations/shiftr", - "requirements": [ - "paho-mqtt==1.5.0" - ], + "requirements": ["paho-mqtt==1.5.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 36af63da9f8..007f6ef1d99 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,11 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": [ - "shodan==1.20.0" - ], + "requirements": ["shodan==1.21.1"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] -} \ No newline at end of file + "codeowners": ["@fabaff"] +} diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 850b06332f8..856ea0784ba 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -5,13 +5,12 @@ import uuid import voluptuous as vol -from homeassistant.const import HTTP_NOT_FOUND, HTTP_BAD_REQUEST -from homeassistant.core import callback -from homeassistant.components import http +from homeassistant.components import http, websocket_api from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.const import HTTP_BAD_REQUEST, HTTP_NOT_FOUND +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json, save_json -from homeassistant.components import websocket_api ATTR_NAME = "name" diff --git a/homeassistant/components/shopping_list/manifest.json b/homeassistant/components/shopping_list/manifest.json index 41fe28defaa..0c8b66b9a03 100644 --- a/homeassistant/components/shopping_list/manifest.json +++ b/homeassistant/components/shopping_list/manifest.json @@ -1,10 +1,9 @@ { "domain": "shopping_list", - "name": "Shopping list", + "name": "Shopping List", "documentation": "https://www.home-assistant.io/integrations/shopping_list", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/sht31/manifest.json b/homeassistant/components/sht31/manifest.json index 6ed947e5c82..3d36e7f5797 100644 --- a/homeassistant/components/sht31/manifest.json +++ b/homeassistant/components/sht31/manifest.json @@ -1,11 +1,8 @@ { "domain": "sht31", - "name": "Sht31", + "name": "Sensirion SHT31", "documentation": "https://www.home-assistant.io/integrations/sht31", - "requirements": [ - "Adafruit-GPIO==1.0.3", - "Adafruit-SHT31==1.0.2" - ], + "requirements": ["Adafruit-GPIO==1.0.3", "Adafruit-SHT31==1.0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/sigfox/sensor.py b/homeassistant/components/sigfox/sensor.py index b890880389c..27e2fe9b563 100644 --- a/homeassistant/components/sigfox/sensor.py +++ b/homeassistant/components/sigfox/sensor.py @@ -1,15 +1,15 @@ """Sensor for SigFox devices.""" -import logging import datetime import json +import logging from urllib.parse import urljoin import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/signal_messenger/__init__.py b/homeassistant/components/signal_messenger/__init__.py new file mode 100644 index 00000000000..045eb95f1b3 --- /dev/null +++ b/homeassistant/components/signal_messenger/__init__.py @@ -0,0 +1 @@ +"""The signalmessenger component.""" diff --git a/homeassistant/components/signal_messenger/manifest.json b/homeassistant/components/signal_messenger/manifest.json new file mode 100644 index 00000000000..98a7b4e59a6 --- /dev/null +++ b/homeassistant/components/signal_messenger/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "signal_messenger", + "name": "Signal Messenger", + "documentation": "https://www.home-assistant.io/integrations/signal_messenger", + "dependencies": [], + "codeowners": ["@bbernhard"], + "requirements": ["pysignalclirestapi==0.1.4"] +} diff --git a/homeassistant/components/signal_messenger/notify.py b/homeassistant/components/signal_messenger/notify.py new file mode 100644 index 00000000000..8fbf9c70873 --- /dev/null +++ b/homeassistant/components/signal_messenger/notify.py @@ -0,0 +1,71 @@ +"""Signal Messenger for notify component.""" +import logging + +from pysignalclirestapi import SignalCliRestApi, SignalCliRestApiError +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + PLATFORM_SCHEMA, + BaseNotificationService, +) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +CONF_SENDER_NR = "number" +CONF_RECP_NR = "recipients" +CONF_SIGNAL_CLI_REST_API = "url" +ATTR_FILENAME = "attachment" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_SENDER_NR): cv.string, + vol.Required(CONF_SIGNAL_CLI_REST_API): cv.string, + vol.Required(CONF_RECP_NR): vol.All(cv.ensure_list, [cv.string]), + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the SignalMessenger notification service.""" + + sender_nr = config[CONF_SENDER_NR] + recp_nrs = config[CONF_RECP_NR] + signal_cli_rest_api_url = config[CONF_SIGNAL_CLI_REST_API] + + signal_cli_rest_api = SignalCliRestApi( + signal_cli_rest_api_url, sender_nr, api_version=1 + ) + + return SignalNotificationService(recp_nrs, signal_cli_rest_api) + + +class SignalNotificationService(BaseNotificationService): + """Implement the notification service for SignalMessenger.""" + + def __init__(self, recp_nrs, signal_cli_rest_api): + """Initialize the service.""" + + self._recp_nrs = recp_nrs + self._signal_cli_rest_api = signal_cli_rest_api + + def send_message(self, message="", **kwargs): + """Send a message to a one or more recipients. + + Additionally a file can be attached. + """ + + _LOGGER.debug("Sending signal message") + + data = kwargs.get(ATTR_DATA) + + filename = None + if data is not None and ATTR_FILENAME in data: + filename = data[ATTR_FILENAME] + + try: + self._signal_cli_rest_api.send_message(message, self._recp_nrs, filename) + except SignalCliRestApiError as ex: + _LOGGER.error("%s", ex) + raise ex diff --git a/homeassistant/components/simplepush/manifest.json b/homeassistant/components/simplepush/manifest.json index d303ac221dd..b6d8005431b 100644 --- a/homeassistant/components/simplepush/manifest.json +++ b/homeassistant/components/simplepush/manifest.json @@ -2,9 +2,7 @@ "domain": "simplepush", "name": "Simplepush", "documentation": "https://www.home-assistant.io/integrations/simplepush", - "requirements": [ - "simplepush==1.1.4" - ], + "requirements": ["simplepush==1.1.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/simplisafe/.translations/da.json b/homeassistant/components/simplisafe/.translations/da.json index 3ec3d7b456c..0d3970eeba5 100644 --- a/homeassistant/components/simplisafe/.translations/da.json +++ b/homeassistant/components/simplisafe/.translations/da.json @@ -9,7 +9,7 @@ "data": { "code": "Kode (til Home Assistant)", "password": "Adgangskode", - "username": "Email adresse" + "username": "Emailadresse" }, "title": "Udfyld dine oplysninger" } diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 9671d56c873..05dad43955c 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -177,11 +177,23 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): self._state = None last_event = self._simplisafe.last_event_data[self._system.system_id] + + try: + last_event_sensor_type = EntityTypes(last_event["sensorType"]).name + except ValueError: + _LOGGER.warning( + 'Encountered unknown entity type: %s ("%s"). Please report it at' + "https://github.com/home-assistant/home-assistant/issues.", + last_event["sensorType"], + last_event["sensorName"], + ) + last_event_sensor_type = None + self._attrs.update( { ATTR_LAST_EVENT_INFO: last_event["info"], ATTR_LAST_EVENT_SENSOR_NAME: last_event["sensorName"], - ATTR_LAST_EVENT_SENSOR_TYPE: EntityTypes(last_event["sensorType"]).name, + ATTR_LAST_EVENT_SENSOR_TYPE: last_event_sensor_type, ATTR_LAST_EVENT_TIMESTAMP: utc_from_timestamp( last_event["eventTimestamp"] ), diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 2df49bb5209..ccb822e7f45 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -1,13 +1,9 @@ { "domain": "simplisafe", - "name": "Simplisafe", + "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": [ - "simplisafe-python==5.3.6" - ], + "requirements": ["simplisafe-python==5.3.6"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/simulated/manifest.json b/homeassistant/components/simulated/manifest.json index 4dcd0715704..6a30f6a00cc 100644 --- a/homeassistant/components/simulated/manifest.json +++ b/homeassistant/components/simulated/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/simulated", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/simulated/sensor.py b/homeassistant/components/simulated/sensor.py index f6ed54e5191..d05448a82c7 100644 --- a/homeassistant/components/simulated/sensor.py +++ b/homeassistant/components/simulated/sensor.py @@ -1,8 +1,8 @@ """Adds a simulated sensor.""" +from datetime import datetime import logging import math from random import Random -from datetime import datetime import voluptuous as vol diff --git a/homeassistant/components/sinch/manifest.json b/homeassistant/components/sinch/manifest.json index a1864428fee..5253655844b 100644 --- a/homeassistant/components/sinch/manifest.json +++ b/homeassistant/components/sinch/manifest.json @@ -1,12 +1,8 @@ { "domain": "sinch", - "name": "Sinch", + "name": "Sinch SMS", "documentation": "https://www.home-assistant.io/components/sinch", "dependencies": [], - "codeowners": [ - "@bendikrb" - ], - "requirements": [ - "clx-sdk-xms==1.0.0" - ] -} \ No newline at end of file + "codeowners": ["@bendikrb"], + "requirements": ["clx-sdk-xms==1.0.0"] +} diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index d7d1f242c67..c0092f013c4 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -1,25 +1,25 @@ """Support for Sinch notifications.""" import logging -import voluptuous as vol from clx.xms.api import MtBatchTextSmsResult from clx.xms.client import Client from clx.xms.exceptions import ( ErrorResponseException, - UnexpectedResponseException, - UnauthorizedException, NotFoundException, + UnauthorizedException, + UnexpectedResponseException, ) +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.notify import ( - ATTR_MESSAGE, ATTR_DATA, + ATTR_MESSAGE, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_SENDER +import homeassistant.helpers.config_validation as cv DOMAIN = "sinch" diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index d5cfc488151..c101100fbe8 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -2,9 +2,7 @@ "domain": "sisyphus", "name": "Sisyphus", "documentation": "https://www.home-assistant.io/integrations/sisyphus", - "requirements": [ - "sisyphus-control==2.2.1" - ], + "requirements": ["sisyphus-control==2.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/sky_hub/device_tracker.py b/homeassistant/components/sky_hub/device_tracker.py index 109c410c16d..c7dc1092b73 100644 --- a/homeassistant/components/sky_hub/device_tracker.py +++ b/homeassistant/components/sky_hub/device_tracker.py @@ -5,13 +5,13 @@ import re import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) _MAC_REGEX = re.compile(r"(([0-9A-Fa-f]{1,2}\:){5}[0-9A-Fa-f]{1,2})") @@ -95,8 +95,7 @@ def _parse_skyhub_response(data_str): pattmatch = re.search("attach_dev = '(.*)'", data_str) if pattmatch is None: raise OSError( - "Error: Impossible to fetch data from" - + " Sky Hub. Try to reboot the router." + "Error: Impossible to fetch data from Sky Hub. Try to reboot the router." ) patt = pattmatch.group(1) @@ -107,8 +106,6 @@ def _parse_skyhub_response(data_str): if _MAC_REGEX.match(dvc[1]): devices[dvc[1]] = dvc[0] else: - raise RuntimeError( - "Error: MAC address " + dvc[1] + " not in correct format." - ) + raise RuntimeError(f"Error: MAC address {dvc[1]} not in correct format.") return devices diff --git a/homeassistant/components/sky_hub/manifest.json b/homeassistant/components/sky_hub/manifest.json index 6be45690629..4d2b3733a0c 100644 --- a/homeassistant/components/sky_hub/manifest.json +++ b/homeassistant/components/sky_hub/manifest.json @@ -1,6 +1,6 @@ { "domain": "sky_hub", - "name": "Sky hub", + "name": "Sky Hub", "documentation": "https://www.home-assistant.io/integrations/sky_hub", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index 7ab42c5da87..03ff593bb4a 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -2,9 +2,7 @@ "domain": "skybeacon", "name": "Skybeacon", "documentation": "https://www.home-assistant.io/integrations/skybeacon", - "requirements": [ - "pygatt[GATTTOOL]==4.0.5" - ], + "requirements": ["pygatt[GATTTOOL]==4.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/skybell/light.py b/homeassistant/components/skybell/light.py index 52f144f640c..c9aa622ad0b 100644 --- a/homeassistant/components/skybell/light.py +++ b/homeassistant/components/skybell/light.py @@ -27,12 +27,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def _to_skybell_level(level): - """Convert the given HASS light level (0-255) to Skybell (0-100).""" + """Convert the given Home Assistant light level (0-255) to Skybell (0-100).""" return int((level * 100) / 255) def _to_hass_level(level): - """Convert the given Skybell (0-100) light level to HASS (0-255).""" + """Convert the given Skybell (0-100) light level to Home Assistant (0-255).""" return int((level * 255) / 100) diff --git a/homeassistant/components/skybell/manifest.json b/homeassistant/components/skybell/manifest.json index 8a005be52e2..8e3ec66356c 100644 --- a/homeassistant/components/skybell/manifest.json +++ b/homeassistant/components/skybell/manifest.json @@ -1,10 +1,8 @@ { "domain": "skybell", - "name": "Skybell", + "name": "SkyBell", "documentation": "https://www.home-assistant.io/integrations/skybell", - "requirements": [ - "skybellpy==0.4.0" - ], + "requirements": ["skybellpy==0.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index 10dc4bffccf..2d78409e21a 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -2,9 +2,7 @@ "domain": "slack", "name": "Slack", "documentation": "https://www.home-assistant.io/integrations/slack", - "requirements": [ - "slacker==0.13.0" - ], + "requirements": ["slacker==0.13.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index 7035299709d..2b4d9d010a3 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -1,13 +1,14 @@ """Support for SleepIQ from SleepNumber.""" -import logging from datetime import timedelta +import logging +from sleepyq import Sleepyq import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD from homeassistant.util import Throttle DOMAIN = "sleepiq" @@ -47,8 +48,6 @@ def setup(hass, config): """ global DATA - from sleepyq import Sleepyq - username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] client = Sleepyq(username, password) diff --git a/homeassistant/components/sleepiq/manifest.json b/homeassistant/components/sleepiq/manifest.json index ac605f42b0c..e6b0fe5c34a 100644 --- a/homeassistant/components/sleepiq/manifest.json +++ b/homeassistant/components/sleepiq/manifest.json @@ -1,10 +1,8 @@ { "domain": "sleepiq", - "name": "Sleepiq", + "name": "SleepIQ", "documentation": "https://www.home-assistant.io/integrations/sleepiq", - "requirements": [ - "sleepyq==0.7" - ], + "requirements": ["sleepyq==0.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/slide/__init__.py b/homeassistant/components/slide/__init__.py index 54154ae863e..ccf4465577b 100644 --- a/homeassistant/components/slide/__init__.py +++ b/homeassistant/components/slide/__init__.py @@ -1,24 +1,25 @@ """Component for the Go Slide API.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol from goslideapi import GoSlideCloud, goslideapi +import voluptuous as vol from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, - STATE_OPEN, + CONF_USERNAME, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, ) 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 +from homeassistant.helpers.event import async_call_later, async_track_time_interval + +from .const import API, COMPONENT, DEFAULT_RETRY, DOMAIN, SLIDES _LOGGER = logging.getLogger(__name__) @@ -59,8 +60,7 @@ async def async_setup(hass, config): for slide in result: if "device_id" not in slide: _LOGGER.error( - "Found invalid Slide entry, device_id is " "missing. Entry=%s", - slide, + "Found invalid Slide entry, device_id is missing. Entry=%s", slide, ) continue @@ -103,7 +103,7 @@ async def async_setup(hass, config): ) elif "code" in slide["device_info"]: _LOGGER.warning( - "Slide %s (%s) is offline with " "code=%s", + "Slide %s (%s) is offline with code=%s", slide["id"], slidenew["mac"], slide["device_info"]["code"], diff --git a/homeassistant/components/slide/cover.py b/homeassistant/components/slide/cover.py index 1c4e6da5aac..a567a9bf61b 100644 --- a/homeassistant/components/slide/cover.py +++ b/homeassistant/components/slide/cover.py @@ -2,15 +2,16 @@ import logging -from homeassistant.const import ATTR_ID from homeassistant.components.cover import ( ATTR_POSITION, - STATE_CLOSED, - STATE_OPENING, - STATE_CLOSING, DEVICE_CLASS_CURTAIN, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPENING, CoverDevice, ) +from homeassistant.const import ATTR_ID + from .const import API, DOMAIN, SLIDES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/slide/manifest.json b/homeassistant/components/slide/manifest.json index 3d2836e1809..74dc562203f 100644 --- a/homeassistant/components/slide/manifest.json +++ b/homeassistant/components/slide/manifest.json @@ -2,11 +2,7 @@ "domain": "slide", "name": "Slide", "documentation": "https://www.home-assistant.io/integrations/slide", - "requirements": [ - "goslide-api==0.5.1" - ], + "requirements": ["goslide-api==0.5.1"], "dependencies": [], - "codeowners": [ - "@ualex73" - ] + "codeowners": ["@ualex73"] } diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 8f0caa7c548..1c4b98c2911 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -1,12 +1,8 @@ { "domain": "sma", - "name": "Sma", + "name": "SMA Solar", "documentation": "https://www.home-assistant.io/integrations/sma", - "requirements": [ - "pysma==0.3.4" - ], + "requirements": ["pysma==0.3.4"], "dependencies": [], - "codeowners": [ - "@kellerza" - ] + "codeowners": ["@kellerza"] } diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index ff1c48a141d..8caebb4f871 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -110,7 +110,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if not config_sensors: # Use all sensors by default config_sensors = {s.name: [] for s in sensor_def} - # Prepare all HASS sensor entities + # Prepare all Home Assistant sensor entities for name, attr in config_sensors.items(): sub_sensors = [sensor_def[s] for s in attr] hass_sensors.append(SMAsensor(sensor_def[name], sub_sensors)) diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index ecab09f6ff9..d34653e60e7 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -24,7 +24,7 @@ CONF_HOST_PASSWORD = "host_password" DOMAIN = "smappee" DATA_SMAPPEE = "SMAPPEE" -_SENSOR_REGEX = re.compile(r"(?P([A-Za-z]+))\=" + r"(?P([0-9\.]+))") +_SENSOR_REGEX = re.compile(r"(?P([A-Za-z]+))\=(?P([0-9\.]+))") CONFIG_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index 6f5a4b7b64b..f3ed9c6e620 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -2,9 +2,7 @@ "domain": "smappee", "name": "Smappee", "documentation": "https://www.home-assistant.io/integrations/smappee", - "requirements": [ - "smappy==0.2.16" - ], + "requirements": ["smappy==0.2.16"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/smappee/sensor.py b/homeassistant/components/smappee/sensor.py index 28abf759d09..c61d28bbaac 100644 --- a/homeassistant/components/smappee/sensor.py +++ b/homeassistant/components/smappee/sensor.py @@ -97,19 +97,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) if smappee.is_local_active: - for location_id in smappee.locations.keys(): + if smappee.is_remote_active: + location_keys = smappee.locations.keys() + else: + location_keys = [None] + for location_id in location_keys: for sensor in SENSOR_TYPES: if "local" in SENSOR_TYPES[sensor]: - if smappee.is_remote_active: - dev.append( - SmappeeSensor( - smappee, location_id, sensor, SENSOR_TYPES[sensor] - ) - ) - else: - dev.append( - SmappeeSensor(smappee, None, sensor, SENSOR_TYPES[sensor]) + dev.append( + SmappeeSensor( + smappee, location_id, sensor, SENSOR_TYPES[sensor] ) + ) add_entities(dev, True) diff --git a/homeassistant/components/smarthab/manifest.json b/homeassistant/components/smarthab/manifest.json index 515f3dcbf65..dc3a2857659 100644 --- a/homeassistant/components/smarthab/manifest.json +++ b/homeassistant/components/smarthab/manifest.json @@ -2,9 +2,7 @@ "domain": "smarthab", "name": "SmartHab", "documentation": "https://www.home-assistant.io/integrations/smarthab", - "requirements": [ - "smarthab==0.20" - ], + "requirements": ["smarthab==0.20"], "dependencies": [], "codeowners": ["@outadoc"] -} \ No newline at end of file +} diff --git a/homeassistant/components/smartthings/.translations/da.json b/homeassistant/components/smartthings/.translations/da.json index 18412069394..04fe2171f39 100644 --- a/homeassistant/components/smartthings/.translations/da.json +++ b/homeassistant/components/smartthings/.translations/da.json @@ -1,9 +1,9 @@ { "config": { "error": { - "app_not_installed": "S\u00f8rg for at du har installeret og autoriseret Home Assistant SmartApp og pr\u00f8v igen.", + "app_not_installed": "S\u00f8rg for, at du har installeret og godkendt Home Assistant SmartApp, og pr\u00f8v igen.", "app_setup_error": "SmartApp kunne ikke konfigureres. Pr\u00f8v igen.", - "base_url_not_https": "`base_url` til` http` komponenten skal konfigureres og starte med `https://`.", + "base_url_not_https": "`base_url` til `http`-komponenten skal konfigureres og starte med `https://`.", "token_already_setup": "Token er allerede konfigureret.", "token_forbidden": "Adgangstoken er ikke indenfor OAuth", "token_invalid_format": "Adgangstoken skal v\u00e6re i UID/GUID format", diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index 9787fb53917..33f9558023d 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -279,7 +279,7 @@ class DeviceBroker: capabilities = device.capabilities.copy() slots = {} for platform_name in SUPPORTED_PLATFORMS: - platform = importlib.import_module("." + platform_name, self.__module__) + platform = importlib.import_module(f".{platform_name}", self.__module__) if not hasattr(platform, "get_capabilities"): continue assigned = platform.get_capabilities(capabilities) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index 9b67df21491..f2bfef960cd 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -2,6 +2,7 @@ import asyncio import functools import logging +import secrets from urllib.parse import urlparse from uuid import uuid4 @@ -87,7 +88,7 @@ async def validate_installed_app(api, installed_app_id: str): def validate_webhook_requirements(hass: HomeAssistantType) -> bool: - """Ensure HASS is setup properly to receive webhooks.""" + """Ensure Home Assistant is setup properly to receive webhooks.""" if hass.components.cloud.async_active_subscription(): return True if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: @@ -108,7 +109,7 @@ def get_webhook_url(hass: HomeAssistantType) -> str: def _get_app_template(hass: HomeAssistantType): - endpoint = "at " + hass.config.api.base_url + endpoint = f"at {hass.config.api.base_url}" cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] if cloudhook_url is not None: endpoint = "via Nabu Casa" @@ -208,7 +209,7 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): # Create config config = { CONF_INSTANCE_ID: str(uuid4()), - CONF_WEBHOOK_ID: webhook.generate_secret(), + CONF_WEBHOOK_ID: secrets.token_hex(), CONF_CLOUDHOOK_URL: None, } await store.async_save(config) @@ -338,7 +339,7 @@ async def smartapp_sync_subscriptions( ) except Exception as error: # pylint:disable=broad-except _LOGGER.error( - "Failed to remove subscription for '%s' under app " "'%s': %s", + "Failed to remove subscription for '%s' under app '%s': %s", sub.capability, installed_app_id, error, diff --git a/homeassistant/components/smarty/manifest.json b/homeassistant/components/smarty/manifest.json index 003ee804b55..1e56cf84e47 100644 --- a/homeassistant/components/smarty/manifest.json +++ b/homeassistant/components/smarty/manifest.json @@ -1,13 +1,8 @@ { "domain": "smarty", - "name": "smarty", + "name": "Salda Smarty", "documentation": "https://www.home-assistant.io/integrations/smarty", - "requirements": [ - "pysmarty==0.8" - ], + "requirements": ["pysmarty==0.8"], "dependencies": [], - "codeowners": [ - "@z0mbieprocess" - ] + "codeowners": ["@z0mbieprocess"] } - diff --git a/homeassistant/components/smhi/.translations/da.json b/homeassistant/components/smhi/.translations/da.json index b43fef7ec45..52c4f54ebd7 100644 --- a/homeassistant/components/smhi/.translations/da.json +++ b/homeassistant/components/smhi/.translations/da.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "Navnet findes allerede", - "wrong_location": "Placering kun i Sverige" + "wrong_location": "Lokalitet kun i Sverige" }, "step": { "user": { @@ -11,7 +11,7 @@ "longitude": "L\u00e6ngdegrad", "name": "Navn" }, - "title": "Placering i Sverige" + "title": "Lokalitet i Sverige" } }, "title": "Svensk vejr service (SMHI)" diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index 3b60cb66165..2c04896497a 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -1,4 +1,5 @@ """Config flow to configure SMHI component.""" +from smhi.smhi_lib import Smhi, SmhiForecastException import voluptuous as vol from homeassistant import config_entries @@ -96,7 +97,6 @@ class SmhiFlowHandler(config_entries.ConfigFlow): async def _check_location(self, longitude: str, latitude: str) -> bool: """Return true if location is ok.""" - from smhi.smhi_lib import Smhi, SmhiForecastException try: session = aiohttp_client.async_get_clientsession(self.hass) diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index b3fdc2825ae..e6bb655dc59 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -1,11 +1,9 @@ { "domain": "smhi", - "name": "Smhi", + "name": "SMHI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smhi", - "requirements": [ - "smhi-pkg==1.0.10" - ], + "requirements": ["smhi-pkg==1.0.10"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/smhi/weather.py b/homeassistant/components/smhi/weather.py index 5f6722b72a6..574b8d85767 100644 --- a/homeassistant/components/smhi/weather.py +++ b/homeassistant/components/smhi/weather.py @@ -6,6 +6,8 @@ from typing import Dict, List import aiohttp import async_timeout +from smhi import Smhi +from smhi.smhi_lib import SmhiForecastException from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -90,7 +92,6 @@ class SmhiWeather(WeatherEntity): session: aiohttp.ClientSession = None, ) -> None: """Initialize the SMHI weather entity.""" - from smhi import Smhi self._name = name self._latitude = latitude @@ -107,7 +108,6 @@ class SmhiWeather(WeatherEntity): @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self) -> None: """Refresh the forecast data from SMHI weather API.""" - from smhi.smhi_lib import SmhiForecastException def fail(): """Postpone updates.""" diff --git a/homeassistant/components/smtp/manifest.json b/homeassistant/components/smtp/manifest.json index b6ec6b4ca0d..974eb684765 100644 --- a/homeassistant/components/smtp/manifest.json +++ b/homeassistant/components/smtp/manifest.json @@ -1,10 +1,8 @@ { "domain": "smtp", - "name": "Smtp", + "name": "SMTP", "documentation": "https://www.home-assistant.io/integrations/smtp", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index d592f25a61d..1b7ae4ebdb7 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -10,6 +10,13 @@ import smtplib import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, @@ -21,14 +28,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) ATTR_IMAGES = "images" # optional embedded image file attachments @@ -184,7 +183,7 @@ class MailNotificationService(BaseNotificationService): msg["From"] = f"{self._sender_name} <{self._sender}>" else: msg["From"] = self._sender - msg["X-Mailer"] = "HomeAssistant" + msg["X-Mailer"] = "Home Assistant" msg["Date"] = email.utils.format_datetime(dt_util.now()) msg["Message-Id"] = email.utils.make_msgid() diff --git a/homeassistant/components/snapcast/manifest.json b/homeassistant/components/snapcast/manifest.json index 1314b91f982..e64d062b320 100644 --- a/homeassistant/components/snapcast/manifest.json +++ b/homeassistant/components/snapcast/manifest.json @@ -2,9 +2,7 @@ "domain": "snapcast", "name": "Snapcast", "documentation": "https://www.home-assistant.io/integrations/snapcast", - "requirements": [ - "snapcast==2.0.10" - ], + "requirements": ["snapcast==2.0.10"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/snips/__init__.py b/homeassistant/components/snips/__init__.py index 93e445e8ced..65015bd723c 100644 --- a/homeassistant/components/snips/__init__.py +++ b/homeassistant/components/snips/__init__.py @@ -1,13 +1,13 @@ """Support for Snips on-device ASR and NLU.""" +from datetime import timedelta import json import logging -from datetime import timedelta import voluptuous as vol -from homeassistant.core import callback -from homeassistant.helpers import intent, config_validation as cv from homeassistant.components import mqtt +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv, intent DOMAIN = "snips" CONF_INTENTS = "intents" diff --git a/homeassistant/components/snips/manifest.json b/homeassistant/components/snips/manifest.json index e15d86e811f..8aeb5bbb186 100644 --- a/homeassistant/components/snips/manifest.json +++ b/homeassistant/components/snips/manifest.json @@ -3,8 +3,6 @@ "name": "Snips", "documentation": "https://www.home-assistant.io/integrations/snips", "requirements": [], - "dependencies": [ - "mqtt" - ], + "dependencies": ["mqtt"], "codeowners": [] } diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index aad7f49c962..a01bee29b33 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -1,10 +1,8 @@ { "domain": "snmp", - "name": "Snmp", + "name": "SNMP", "documentation": "https://www.home-assistant.io/integrations/snmp", - "requirements": [ - "pysnmp==4.4.12" - ], + "requirements": ["pysnmp==4.4.12"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 8d5be1221c4..578b97c801e 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -2,7 +2,6 @@ import logging from pyasn1.type.univ import Integer - import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, diff --git a/homeassistant/components/sochain/manifest.json b/homeassistant/components/sochain/manifest.json index 93361d41e2b..1cc3a76c3be 100644 --- a/homeassistant/components/sochain/manifest.json +++ b/homeassistant/components/sochain/manifest.json @@ -1,10 +1,8 @@ { "domain": "sochain", - "name": "Sochain", + "name": "SoChain", "documentation": "https://www.home-assistant.io/integrations/sochain", - "requirements": [ - "python-sochain-api==0.0.2" - ], + "requirements": ["python-sochain-api==0.0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/socialblade/manifest.json b/homeassistant/components/socialblade/manifest.json index c90342018f8..2ce7fbabf0f 100644 --- a/homeassistant/components/socialblade/manifest.json +++ b/homeassistant/components/socialblade/manifest.json @@ -1,10 +1,8 @@ { "domain": "socialblade", - "name": "Socialblade", + "name": "Social Blade", "documentation": "https://www.home-assistant.io/integrations/socialblade", - "requirements": [ - "socialbladeclient==0.2" - ], + "requirements": ["socialbladeclient==0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index f2635936cb5..f3c10e98dcf 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -1,11 +1,8 @@ { "domain": "solaredge", - "name": "Solaredge", + "name": "SolarEdge", "documentation": "https://www.home-assistant.io/integrations/solaredge", - "requirements": [ - "solaredge==0.0.2", - "stringcase==1.2.0" - ], + "requirements": ["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 f0f1660a821..60cabaf38f0 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -41,7 +41,7 @@ async def async_setup_entry(hass, entry, async_add_entities): return _LOGGER.debug("Credentials correct and site is active") except KeyError: - _LOGGER.error("Missing details data in solaredge response") + _LOGGER.error("Missing details data in SolarEdge response") return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") @@ -350,7 +350,9 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService): power_to = [] if "connections" not in power_flow: - _LOGGER.error("Missing connections in power flow data") + _LOGGER.debug( + "Missing connections in power flow data. Assuming site does not have any" + ) return for connection in power_flow["connections"]: diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json index 91ed6f55ee1..02fe4dad398 100644 --- a/homeassistant/components/solaredge_local/manifest.json +++ b/homeassistant/components/solaredge_local/manifest.json @@ -1,13 +1,8 @@ { "domain": "solaredge_local", - "name": "Solar Edge Local", + "name": "SolarEdge Local", "documentation": "https://www.home-assistant.io/integrations/solaredge_local", - "requirements": [ - "solaredge-local==0.2.0" - ], + "requirements": ["solaredge-local==0.2.0"], "dependencies": [], - "codeowners": [ - "@drobtravels", - "@scheric" - ] + "codeowners": ["@drobtravels", "@scheric"] } diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 917fb86ddcb..ecf9dfde8b1 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -1,10 +1,10 @@ """Support for SolarEdge-local Monitoring API.""" -import logging -from datetime import timedelta -import statistics from copy import deepcopy +from datetime import timedelta +import logging +import statistics -from requests.exceptions import HTTPError, ConnectTimeout +from requests.exceptions import ConnectTimeout, HTTPError from solaredge_local import SolarEdge import voluptuous as vol @@ -12,8 +12,8 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_IP_ADDRESS, CONF_NAME, - POWER_WATT, ENERGY_WATT_HOUR, + POWER_WATT, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) diff --git a/homeassistant/components/solarlog/.translations/de.json b/homeassistant/components/solarlog/.translations/de.json new file mode 100644 index 00000000000..5a1b384ed27 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen. \u00dcberpr\u00fcfen Sie die Host-Adresse" + }, + "step": { + "user": { + "data": { + "host": "Der Hostname oder die IP-Adresse Ihres Solar-Log-Ger\u00e4ts", + "name": "Das Pr\u00e4fix, das f\u00fcr Ihre Solar-Log-Sensoren verwendet werden soll" + }, + "title": "Definieren Sie Ihre Solar-Log-Verbindung" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json index 7f40935e5a5..b64496c4591 100644 --- a/homeassistant/components/solarlog/.translations/ru.json +++ b/homeassistant/components/solarlog/.translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430." }, "step": { diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index 67eb8006cec..933f8014090 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -1,7 +1,7 @@ """Constants for the Solar-Log integration.""" from datetime import timedelta -from homeassistant.const import POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT DOMAIN = "solarlog" diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index 583529ffe87..85ab9eb913e 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -6,14 +6,14 @@ from requests.exceptions import HTTPError, Timeout from sunwatcher.solarlog.solarlog import SolarLog 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.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import DOMAIN, DEFAULT_HOST, DEFAULT_NAME, SCAN_INTERVAL, SENSOR_TYPES +from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN, SCAN_INTERVAL, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/solax/manifest.json b/homeassistant/components/solax/manifest.json index 14ac59d4621..6c0e9c3b01d 100644 --- a/homeassistant/components/solax/manifest.json +++ b/homeassistant/components/solax/manifest.json @@ -1,10 +1,8 @@ { - "domain": "solax", - "name": "Solax Inverter", - "documentation": "https://www.home-assistant.io/integrations/solax", - "requirements": [ - "solax==0.2.2" - ], - "dependencies": [], - "codeowners": ["@squishykid"] - } + "domain": "solax", + "name": "SolaX Power", + "documentation": "https://www.home-assistant.io/integrations/solax", + "requirements": ["solax==0.2.2"], + "dependencies": [], + "codeowners": ["@squishykid"] +} diff --git a/homeassistant/components/solax/sensor.py b/homeassistant/components/solax/sensor.py index a5b4547b344..8eb61560e63 100644 --- a/homeassistant/components/solax/sensor.py +++ b/homeassistant/components/solax/sensor.py @@ -1,6 +1,5 @@ """Support for Solax inverter via local API.""" import asyncio - from datetime import timedelta import logging @@ -8,11 +7,11 @@ from solax import real_time_api from solax.inverter import InverterError import voluptuous as vol -from homeassistant.const import TEMP_CELSIUS, CONF_IP_ADDRESS, CONF_PORT -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_IP_ADDRESS, CONF_PORT, TEMP_CELSIUS from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json index 18b33d1bc9b..a1a5b9489fa 100644 --- a/homeassistant/components/soma/.translations/ca.json +++ b/homeassistant/components/soma/.translations/ca.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Nom\u00e9s pots configurar un compte de Soma.", "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3." + "connection_error": "No s'ha pogut connectar amb SOMA Connect.", + "missing_configuration": "El component Soma no est\u00e0 configurat. Mira'n la documentaci\u00f3.", + "result_error": "SOMA Connect ha respost amb un estat d'error." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Soma." diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json index 557eeab55b1..49bf83148a2 100644 --- a/homeassistant/components/soma/.translations/da.json +++ b/homeassistant/components/soma/.translations/da.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Du kan kun konfigurere en Soma-konto.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", - "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen." + "connection_error": "Kunne ikke oprette forbindelse til SOMA Connect.", + "missing_configuration": "Soma-komponenten er ikke konfigureret. F\u00f8lg venligst dokumentationen.", + "result_error": "SOMA Connect svarede med fejlstatus." }, "create_entry": { "default": "Godkendt med Soma." diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json index cb08613c07b..384e7ec4207 100644 --- a/homeassistant/components/soma/.translations/de.json +++ b/homeassistant/components/soma/.translations/de.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "connection_error": "Verbindung zu SOMA Connect fehlgeschlagen.", + "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", + "result_error": "SOMA Connect hat mit einem Fehlerstatus geantwortet." }, "create_entry": { "default": "Erfolgreich bei Soma authentifiziert." diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index 42e09a8762c..46bfd441fc4 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "You can only configure one Soma account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Soma component is not configured. Please follow the documentation." + "connection_error": "Failed to connect to SOMA Connect.", + "missing_configuration": "The Soma component is not configured. Please follow the documentation.", + "result_error": "SOMA Connect responded with error status." }, "create_entry": { "default": "Successfully authenticated with Soma." diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json index 86922622704..6df113b82c9 100644 --- a/homeassistant/components/soma/.translations/es.json +++ b/homeassistant/components/soma/.translations/es.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", - "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n." + "connection_error": "No se ha podido conectar a SOMA Connect.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n.", + "result_error": "SOMA Connect respondi\u00f3 con un error." }, "create_entry": { "default": "Autenticado con \u00e9xito con Soma." diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json index a758ab0f615..0889cdea2ec 100644 --- a/homeassistant/components/soma/.translations/fr.json +++ b/homeassistant/components/soma/.translations/fr.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", - "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation." + "connection_error": "Impossible de se connecter \u00e0 SOMA Connect.", + "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation.", + "result_error": "SOMA Connect a r\u00e9pondu avec l'\u00e9tat d'erreur." }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json index 1398b2a66be..6c7d0129708 100644 --- a/homeassistant/components/soma/.translations/it.json +++ b/homeassistant/components/soma/.translations/it.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u00c8 possibile configurare un solo account Soma.", "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", - "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione." + "connection_error": "Impossibile connettersi a SOMA Connect.", + "missing_configuration": "Il componente Soma non \u00e8 configurato. Si prega di seguire la documentazione.", + "result_error": "SOMA Connect ha risposto con stato di errore." }, "create_entry": { "default": "Autenticato con successo con Soma." diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json index 90995ebc9f2..ae4d84671a3 100644 --- a/homeassistant/components/soma/.translations/ko.json +++ b/homeassistant/components/soma/.translations/ko.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\ud558\ub098\uc758 Soma \uacc4\uc815\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." + "connection_error": "SOMA Connect \uc5d0 \uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", + "missing_configuration": "Soma \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", + "result_error": "SOMA Connect \uac00 \uc624\ub958 \uc0c1\ud0dc\ub85c \uc751\ub2f5\ud588\uc2b5\ub2c8\ub2e4." }, "create_entry": { "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json index 93e9a1e66c4..fdf180a1a61 100644 --- a/homeassistant/components/soma/.translations/lb.json +++ b/homeassistant/components/soma/.translations/lb.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Soma Kont konfigur\u00e9ieren.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + "connection_error": "Feeler beim verbannen mat SOMA Connect.", + "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", + "result_error": "SOMA Connect \u00e4ntwert mat engem Feeler Code." }, "create_entry": { "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json index c1188b0ac63..058f7222666 100644 --- a/homeassistant/components/soma/.translations/nl.json +++ b/homeassistant/components/soma/.translations/nl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen." + "connection_error": "Kan geen verbinding maken met SOMA Connect.", + "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen.", + "result_error": "SOMA Connect reageerde met een foutstatus." }, "create_entry": { "default": "Succesvol geverifieerd met Soma." diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index b2d80208b83..518cbc6a37e 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." + "connection_error": "Kunne ikke koble til SOMA Connect.", + "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", + "result_error": "SOMA Connect svarte med feilstatus." }, "create_entry": { "default": "Vellykket autentisering med Somfy." diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index c71e160142e..102413bf446 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji.", - "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + "connection_error": "Nie uda\u0142o si\u0119 po\u0142\u0105czy\u0107 z SOMA Connect.", + "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", + "result_error": "SOMA Connect odpowiedzia\u0142 statusem b\u0142\u0119du." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Soma" diff --git a/homeassistant/components/soma/.translations/pt-BR.json b/homeassistant/components/soma/.translations/pt-BR.json index da05e3b43ae..3485b5304b4 100644 --- a/homeassistant/components/soma/.translations/pt-BR.json +++ b/homeassistant/components/soma/.translations/pt-BR.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "connection_error": "Falha na liga\u00e7\u00e3o \u00e0 SOMA Connect.", + "result_error": "SOMA Connect respondeu com status de erro." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json index f28d672d3f2..fa581eb0821 100644 --- a/homeassistant/components/soma/.translations/ru.json +++ b/homeassistant/components/soma/.translations/ru.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a SOMA Connect.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "result_error": "SOMA Connect \u043e\u0442\u0432\u0435\u0442\u0438\u043b \u0441\u043e \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u043e\u0448\u0438\u0431\u043a\u0438." }, "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." @@ -15,7 +17,7 @@ "port": "\u041f\u043e\u0440\u0442" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SOMA Connect.", - "title": "Soma" + "title": "SOMA Connect" } }, "title": "Soma" diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json index b3075208d2c..01f7e50eb96 100644 --- a/homeassistant/components/soma/.translations/sl.json +++ b/homeassistant/components/soma/.translations/sl.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "Nastavite lahko samo en ra\u010dun Soma.", "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", - "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo." + "connection_error": "Povezava s SOMA Connect ni uspela.", + "missing_configuration": "Komponenta Soma ni konfigurirana. Upo\u0161tevajte dokumentacijo.", + "result_error": "SOMA Connect se je odzvala s statusom napake." }, "create_entry": { "default": "Uspe\u0161no overjen s Soma." diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json index 893abe82ee1..73b26cb91f1 100644 --- a/homeassistant/components/soma/.translations/zh-Hant.json +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", - "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + "connection_error": "SOMA \u9023\u7dda\u5931\u6557\u3002", + "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", + "result_error": "SOMA \u9023\u7dda\u56de\u61c9\u72c0\u614b\u932f\u8aa4\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index b4daa28b5b2..93ee4fc9b8f 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -1,20 +1,18 @@ """Support for Soma Smartshades.""" import logging -import voluptuous as vol from api.soma_api import SomaApi from requests import RequestException +import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.const import CONF_HOST, CONF_PORT - -from .const import DOMAIN, HOST, PORT, API - +from .const import API, DOMAIN, HOST, PORT DEVICES = "devices" diff --git a/homeassistant/components/soma/config_flow.py b/homeassistant/components/soma/config_flow.py index e2f89273520..afb5d05b77e 100644 --- a/homeassistant/components/soma/config_flow.py +++ b/homeassistant/components/soma/config_flow.py @@ -1,12 +1,13 @@ """Config flow for Soma.""" import logging -import voluptuous as vol from api.soma_api import SomaApi from requests import RequestException +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST, CONF_PORT + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -39,14 +40,22 @@ class SomaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Finish config flow.""" api = SomaApi(user_input["host"], user_input["port"]) try: - await self.hass.async_add_executor_job(api.list_devices) + result = await self.hass.async_add_executor_job(api.list_devices) _LOGGER.info("Successfully set up Soma Connect") - return self.async_create_entry( - title="Soma Connect", - data={"host": user_input["host"], "port": user_input["port"]}, + if result["result"] == "success": + return self.async_create_entry( + title="Soma Connect", + data={"host": user_input["host"], "port": user_input["port"]}, + ) + _LOGGER.error( + "Connection to SOMA Connect failed (result:%s)", result["result"] ) + return self.async_abort(reason="result_error") except RequestException: - _LOGGER.error("Connection to SOMA Connect failed") + _LOGGER.error("Connection to SOMA Connect failed with RequestException") + return self.async_abort(reason="connection_error") + except KeyError: + _LOGGER.error("Connection to SOMA Connect failed with KeyError") return self.async_abort(reason="connection_error") async def async_step_import(self, user_input=None): diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index 1577b7f2911..d23cc9ec5d0 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -2,9 +2,8 @@ import logging -from homeassistant.components.cover import CoverDevice, ATTR_POSITION -from homeassistant.components.soma import DOMAIN, SomaEntity, DEVICES, API - +from homeassistant.components.cover import ATTR_POSITION, CoverDevice +from homeassistant.components.soma import API, DEVICES, DOMAIN, SomaEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/soma/manifest.json b/homeassistant/components/soma/manifest.json index 35a77c063b8..397531562b1 100644 --- a/homeassistant/components/soma/manifest.json +++ b/homeassistant/components/soma/manifest.json @@ -1,13 +1,9 @@ { "domain": "soma", - "name": "Soma Open API", + "name": "Soma Connect", "config_flow": true, "documentation": "", "dependencies": [], - "codeowners": [ - "@ratsept" - ], - "requirements": [ - "pysoma==0.0.10" - ] + "codeowners": ["@ratsept"], + "requirements": ["pysoma==0.0.10"] } diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index aa2f92f0be6..67f1f6b7d46 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -3,7 +3,9 @@ "abort": { "already_setup": "You can only configure one Soma account.", "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Soma component is not configured. Please follow the documentation." + "missing_configuration": "The Soma component is not configured. Please follow the documentation.", + "result_error": "SOMA Connect responded with error status.", + "connection_error": "Failed to connect to SOMA Connect." }, "create_entry": { "default": "Successfully authenticated with Soma." diff --git a/homeassistant/components/somfy/.translations/da.json b/homeassistant/components/somfy/.translations/da.json index 9d05fd65a06..b50c030c636 100644 --- a/homeassistant/components/somfy/.translations/da.json +++ b/homeassistant/components/somfy/.translations/da.json @@ -8,6 +8,11 @@ "create_entry": { "default": "Godkendt med Somfy." }, + "step": { + "pick_implementation": { + "title": "V\u00e6lg godkendelsesmetode" + } + }, "title": "Somfy" } } \ No newline at end of file diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 1368725777b..365c6839300 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -5,15 +5,15 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/somfy/ """ import asyncio -import logging from datetime import timedelta +import logging -import voluptuous as vol from requests import HTTPError +import voluptuous as vol -from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py index 1cfea8ff7d8..761ee19f8cb 100644 --- a/homeassistant/components/somfy/api.py +++ b/homeassistant/components/somfy/api.py @@ -1,10 +1,10 @@ -"""API for Somfy bound to HASS OAuth.""" +"""API for Somfy bound to Home Assistant OAuth.""" from asyncio import run_coroutine_threadsafe from typing import Dict, Union from pymfy.api import somfy_api -from homeassistant import core, config_entries +from homeassistant import config_entries, core from homeassistant.helpers import config_entry_oauth2_flow diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index cb180d4e247..2d143fbd196 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -3,6 +3,7 @@ import logging from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/somfy/cover.py b/homeassistant/components/somfy/cover.py index 12f12676f92..b48e326162d 100644 --- a/homeassistant/components/somfy/cover.py +++ b/homeassistant/components/somfy/cover.py @@ -1,13 +1,14 @@ """Support for Somfy Covers.""" -from pymfy.api.devices.category import Category from pymfy.api.devices.blind import Blind +from pymfy.api.devices.category import Category from homeassistant.components.cover import ( - CoverDevice, ATTR_POSITION, ATTR_TILT_POSITION, + CoverDevice, ) -from . import DOMAIN, SomfyEntity, DEVICES, API + +from . import API, DEVICES, DOMAIN, SomfyEntity async def async_setup_entry(hass, config_entry, async_add_entities): @@ -90,15 +91,15 @@ class SomfyCover(SomfyEntity, CoverDevice): def set_cover_tilt_position(self, **kwargs): """Move the cover tilt to a specific position.""" - self.cover.orientation = kwargs[ATTR_TILT_POSITION] + self.cover.orientation = 100 - kwargs[ATTR_TILT_POSITION] def open_cover_tilt(self, **kwargs): """Open the cover tilt.""" - self.cover.orientation = 100 + self.cover.orientation = 0 def close_cover_tilt(self, **kwargs): """Close the cover tilt.""" - self.cover.orientation = 0 + self.cover.orientation = 100 def stop_cover_tilt(self, **kwargs): """Stop the cover.""" diff --git a/homeassistant/components/somfy/switch.py b/homeassistant/components/somfy/switch.py index 58ad2e5905d..bc31d68ec1d 100644 --- a/homeassistant/components/somfy/switch.py +++ b/homeassistant/components/somfy/switch.py @@ -3,7 +3,8 @@ from pymfy.api.devices.camera_protect import CameraProtect from pymfy.api.devices.category import Category from homeassistant.components.switch import SwitchDevice -from . import DOMAIN, SomfyEntity, DEVICES, API + +from . import API, DEVICES, DOMAIN, SomfyEntity async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index 35422cf09a1..03b69f070d0 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -1,10 +1,8 @@ { - "domain": "somfy_mylink", - "name": "Somfy MyLink", - "documentation": "https://www.home-assistant.io/integrations/somfy_mylink", - "requirements": [ - "somfy-mylink-synergy==1.0.6" - ], - "dependencies": [], - "codeowners": [] - } \ No newline at end of file + "domain": "somfy_mylink", + "name": "Somfy MyLink", + "documentation": "https://www.home-assistant.io/integrations/somfy_mylink", + "requirements": ["somfy-mylink-synergy==1.0.6"], + "dependencies": [], + "codeowners": [] +} diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index b090a90d719..a82ffc41e29 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -1,12 +1,8 @@ { "domain": "songpal", - "name": "Songpal", + "name": "Sony Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", - "requirements": [ - "python-songpal==0.11.2" - ], + "requirements": ["python-songpal==0.11.2"], "dependencies": [], - "codeowners": [ - "@rytilahti" - ] + "codeowners": ["@rytilahti"] } diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 681c97a7710..27a81b2a667 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -1,19 +1,19 @@ """Support for Songpal-enabled (Sony) media devices.""" import asyncio -import logging from collections import OrderedDict +import logging -import voluptuous as vol from songpal import ( + ConnectChange, + ContentChange, Device, + PowerChange, SongpalException, VolumeChange, - ContentChange, - PowerChange, - ConnectChange, ) +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -25,9 +25,9 @@ from homeassistant.components.media_player.const import ( from homeassistant.const import ( ATTR_ENTITY_ID, CONF_NAME, + EVENT_HOMEASSISTANT_STOP, STATE_OFF, STATE_ON, - EVENT_HOMEASSISTANT_STOP, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/sonos/.translations/da.json b/homeassistant/components/sonos/.translations/da.json index c303bca0aa8..c4b1a555245 100644 --- a/homeassistant/components/sonos/.translations/da.json +++ b/homeassistant/components/sonos/.translations/da.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "no_devices_found": "Ingen Sonos-enheder kunne findes p\u00e5 netv\u00e6rket.", + "no_devices_found": "Der blev ikke fundet nogen Sonos-enheder p\u00e5 netv\u00e6rket.", "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Sonos" }, "step": { diff --git a/homeassistant/components/sonos/.translations/ko.json b/homeassistant/components/sonos/.translations/ko.json index 4ca3d621599..931a0beadfc 100644 --- a/homeassistant/components/sonos/.translations/ko.json +++ b/homeassistant/components/sonos/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Sonos \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Sonos \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Sonos" } }, diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 46723bdcf5f..74a8dea06d4 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,9 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": [ - "pysonos==0.0.24" - ], + "requirements": ["pysonos==0.0.24"], "dependencies": [], "ssdp": [ { diff --git a/homeassistant/components/sony_projector/manifest.json b/homeassistant/components/sony_projector/manifest.json index 497347671a9..b92d7bd204e 100644 --- a/homeassistant/components/sony_projector/manifest.json +++ b/homeassistant/components/sony_projector/manifest.json @@ -1,10 +1,8 @@ { "domain": "sony_projector", - "name": "Sony projector", + "name": "Sony Projector", "documentation": "https://www.home-assistant.io/integrations/sony_projector", - "requirements": [ - "pysdcp==1" - ], + "requirements": ["pysdcp==1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/soundtouch/manifest.json b/homeassistant/components/soundtouch/manifest.json index e3ca9ed72e8..25c4f7d1d1c 100644 --- a/homeassistant/components/soundtouch/manifest.json +++ b/homeassistant/components/soundtouch/manifest.json @@ -1,10 +1,8 @@ { "domain": "soundtouch", - "name": "Soundtouch", + "name": "Bose Soundtouch", "documentation": "https://www.home-assistant.io/integrations/soundtouch", - "requirements": [ - "libsoundtouch==0.7.2" - ], + "requirements": ["libsoundtouch==0.7.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index a9f6e05011f..72677995a9d 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -2,6 +2,7 @@ import logging import re +from libsoundtouch import soundtouch_device import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice @@ -100,9 +101,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return remote_config = {"id": "ha.component.soundtouch", "host": host, "port": port} - soundtouch_device = SoundTouchDevice(None, remote_config) - hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) - add_entities([soundtouch_device]) + bose_soundtouch_entity = SoundTouchDevice(None, remote_config) + hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) + add_entities([bose_soundtouch_entity]) else: name = config.get(CONF_NAME) remote_config = { @@ -110,9 +111,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "port": config.get(CONF_PORT), "host": config.get(CONF_HOST), } - soundtouch_device = SoundTouchDevice(name, remote_config) - hass.data[DATA_SOUNDTOUCH].append(soundtouch_device) - add_entities([soundtouch_device]) + bose_soundtouch_entity = SoundTouchDevice(name, remote_config) + hass.data[DATA_SOUNDTOUCH].append(bose_soundtouch_entity) + add_entities([bose_soundtouch_entity]) def service_handle(service): """Handle the applying of a service.""" @@ -184,7 +185,6 @@ class SoundTouchDevice(MediaPlayerDevice): def __init__(self, name, config): """Create Soundtouch Entity.""" - from libsoundtouch import soundtouch_device self._device = soundtouch_device(config["host"], config["port"]) if name is None: @@ -304,7 +304,7 @@ class SoundTouchDevice(MediaPlayerDevice): if self._status.station_name is not None: return self._status.station_name if self._status.artist is not None: - return self._status.artist + " - " + self._status.track + return f"{self._status.artist} - {self._status.track}" return None diff --git a/homeassistant/components/spaceapi/manifest.json b/homeassistant/components/spaceapi/manifest.json index 4ab05f170ca..10580321c29 100644 --- a/homeassistant/components/spaceapi/manifest.json +++ b/homeassistant/components/spaceapi/manifest.json @@ -1,12 +1,8 @@ { "domain": "spaceapi", - "name": "Spaceapi", + "name": "Space API", "documentation": "https://www.home-assistant.io/integrations/spaceapi", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@fabaff" - ] + "dependencies": ["http"], + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/spc/__init__.py b/homeassistant/components/spc/__init__.py index b5db4b685ae..1601090463f 100644 --- a/homeassistant/components/spc/__init__.py +++ b/homeassistant/components/spc/__init__.py @@ -1,11 +1,14 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw import SpcWebGateway +from pyspcwebgw.area import Area +from pyspcwebgw.zone import Zone import voluptuous as vol -from homeassistant.helpers import discovery, aiohttp_client -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers import aiohttp_client, discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send _LOGGER = logging.getLogger(__name__) @@ -33,11 +36,8 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the SPC component.""" - from pyspcwebgw import SpcWebGateway async def async_upate_callback(spc_object): - from pyspcwebgw.area import Area - from pyspcwebgw.zone import Zone if isinstance(spc_object, Area): async_dispatcher_send(hass, SIGNAL_UPDATE_ALARM.format(spc_object.id)) diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index fa9a9681fff..ca5d77b2a82 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw.const import AreaMode + import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel.const import ( SUPPORT_ALARM_ARM_AWAY, @@ -24,7 +26,6 @@ _LOGGER = logging.getLogger(__name__) def _get_alarm_state(area): """Get the alarm state.""" - from pyspcwebgw.const import AreaMode if area.verified_alarm: return STATE_ALARM_TRIGGERED @@ -92,24 +93,20 @@ class SpcAlarm(alarm.AlarmControlPanel): async def async_alarm_disarm(self, code=None): """Send disarm command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.UNSET) async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_A) async def async_alarm_arm_night(self, code=None): """Send arm home command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.PART_SET_B) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - from pyspcwebgw.const import AreaMode await self._api.change_mode(area=self._area, new_mode=AreaMode.FULL_SET) diff --git a/homeassistant/components/spc/binary_sensor.py b/homeassistant/components/spc/binary_sensor.py index 1ce02af390f..2104f931c0a 100644 --- a/homeassistant/components/spc/binary_sensor.py +++ b/homeassistant/components/spc/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Vanderbilt (formerly Siemens) SPC alarm systems.""" import logging +from pyspcwebgw.const import ZoneInput + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -60,7 +62,6 @@ class SpcBinarySensor(BinarySensorDevice): @property def is_on(self): """Whether the device is switched on.""" - from pyspcwebgw.const import ZoneInput return self._zone.input == ZoneInput.OPEN diff --git a/homeassistant/components/spc/manifest.json b/homeassistant/components/spc/manifest.json index 5b59d3bfe29..99e6bc48012 100644 --- a/homeassistant/components/spc/manifest.json +++ b/homeassistant/components/spc/manifest.json @@ -1,10 +1,8 @@ { "domain": "spc", - "name": "Spc", + "name": "Vanderbilt SPC", "documentation": "https://www.home-assistant.io/integrations/spc", - "requirements": [ - "pyspcwebgw==0.4.0" - ], + "requirements": ["pyspcwebgw==0.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/speedtestdotnet/manifest.json b/homeassistant/components/speedtestdotnet/manifest.json index 821d4158f57..c3c76101ce7 100644 --- a/homeassistant/components/speedtestdotnet/manifest.json +++ b/homeassistant/components/speedtestdotnet/manifest.json @@ -1,12 +1,8 @@ { "domain": "speedtestdotnet", - "name": "Speedtestdotnet", + "name": "Speedtest.net", "documentation": "https://www.home-assistant.io/integrations/speedtestdotnet", - "requirements": [ - "speedtest-cli==2.1.2" - ], + "requirements": ["speedtest-cli==2.1.2"], "dependencies": [], - "codeowners": [ - "@rohankapoorcom" - ] + "codeowners": ["@rohankapoorcom"] } diff --git a/homeassistant/components/spider/manifest.json b/homeassistant/components/spider/manifest.json index 567f6f0e513..c61b4186847 100644 --- a/homeassistant/components/spider/manifest.json +++ b/homeassistant/components/spider/manifest.json @@ -1,12 +1,8 @@ { "domain": "spider", - "name": "Spider", + "name": "Itho Daalderop Spider", "documentation": "https://www.home-assistant.io/integrations/spider", - "requirements": [ - "spiderpy==1.3.1" - ], + "requirements": ["spiderpy==1.3.1"], "dependencies": [], - "codeowners": [ - "@peternijssen" - ] + "codeowners": ["@peternijssen"] } diff --git a/homeassistant/components/splunk/__init__.py b/homeassistant/components/splunk/__init__.py index c483d7fae87..1d5d39416a3 100644 --- a/homeassistant/components/splunk/__init__.py +++ b/homeassistant/components/splunk/__init__.py @@ -7,12 +7,12 @@ import requests import voluptuous as vol from homeassistant.const import ( - CONF_SSL, - CONF_VERIFY_SSL, CONF_HOST, CONF_NAME, CONF_PORT, + CONF_SSL, CONF_TOKEN, + CONF_VERIFY_SSL, EVENT_STATE_CHANGED, ) from homeassistant.helpers import state as state_helper diff --git a/homeassistant/components/spotcrime/manifest.json b/homeassistant/components/spotcrime/manifest.json index 50d9ba3163d..6fa2b10544d 100644 --- a/homeassistant/components/spotcrime/manifest.json +++ b/homeassistant/components/spotcrime/manifest.json @@ -1,10 +1,8 @@ { "domain": "spotcrime", - "name": "Spotcrime", + "name": "Spot Crime", "documentation": "https://www.home-assistant.io/integrations/spotcrime", - "requirements": [ - "spotcrime==1.0.4" - ], + "requirements": ["spotcrime==1.0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index f514a95c042..ab41becea65 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -2,12 +2,7 @@ "domain": "spotify", "name": "Spotify", "documentation": "https://www.home-assistant.io/integrations/spotify", - "requirements": [ - "spotipy-homeassistant==2.4.4.dev1" - ], - "dependencies": [ - "configurator", - "http" - ], + "requirements": ["spotipy-homeassistant==2.4.4.dev1"], + "dependencies": ["configurator", "http"], "codeowners": [] } diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index ec21a5d7822..ba0c725eb7f 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -37,7 +37,7 @@ CONF_CLIENT_ID = "client_id" CONF_CLIENT_SECRET = "client_secret" CONFIGURATOR_DESCRIPTION = ( - "To link your Spotify account, " "click the link, login, and authorize:" + "To link your Spotify account, click the link, login, and authorize:" ) CONFIGURATOR_LINK_NAME = "Link Spotify account" CONFIGURATOR_SUBMIT_CAPTION = "I authorized successfully" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 39435524c20..c3edbef6944 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -1,12 +1,8 @@ { "domain": "sql", - "name": "Sql", + "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": [ - "sqlalchemy==1.3.11" - ], + "requirements": ["sqlalchemy==1.3.12"], "dependencies": [], - "codeowners": [ - "@dgomes" - ] -} \ No newline at end of file + "codeowners": ["@dgomes"] +} diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index 1f651f746e6..b5297db96ce 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -1,6 +1,6 @@ { "domain": "squeezebox", - "name": "Squeezebox", + "name": "Logitech Squeezebox", "documentation": "https://www.home-assistant.io/integrations/squeezebox", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index b3fb82591c9..94c497e4db6 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -38,9 +38,9 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.exceptions import PlatformNotReady from homeassistant.util.dt import utcnow from .const import DOMAIN, SERVICE_CALL_METHOD @@ -538,11 +538,11 @@ class SqueezeBoxDevice(MediaPlayerDevice): """ Call Squeezebox JSON/RPC method. - Escaped optional parameters are added to the command to form the list - of positional parameters (p0, p1..., pN) passed to JSON/RPC server. + Additional parameters are added to the command to form the list of + positional parameters (p0, p1..., pN) passed to JSON/RPC server. """ all_params = [command] if parameters: for parameter in parameters: - all_params.append(urllib.parse.quote(parameter, safe="+:=/?")) + all_params.append(parameter) return self.async_query(*all_params) diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b9a9d4b46c9..94e256f0523 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -2,31 +2,30 @@ import asyncio from datetime import timedelta import logging -from urllib.parse import urlparse import aiohttp from defusedxml import ElementTree from netdisco import ssdp, util -from homeassistant.helpers.event import async_track_time_interval from homeassistant.generated.ssdp import SSDP +from homeassistant.helpers.event import async_track_time_interval DOMAIN = "ssdp" SCAN_INTERVAL = timedelta(seconds=60) -ATTR_HOST = "host" -ATTR_PORT = "port" -ATTR_SSDP_DESCRIPTION = "ssdp_description" -ATTR_ST = "ssdp_st" -ATTR_NAME = "name" -ATTR_MODEL_NAME = "model_name" -ATTR_MODEL_NUMBER = "model_number" -ATTR_SERIAL = "serial_number" -ATTR_MANUFACTURER = "manufacturer" -ATTR_MANUFACTURERURL = "manufacturerURL" -ATTR_UDN = "udn" -ATTR_UPNP_DEVICE_TYPE = "upnp_device_type" -ATTR_PRESENTATIONURL = "presentation_url" +# Attributes for accessing info from SSDP response +ATTR_SSDP_LOCATION = "ssdp_location" +ATTR_SSDP_ST = "ssdp_st" +# Attributes for accessing info from retrieved UPnP device description +ATTR_UPNP_DEVICE_TYPE = "deviceType" +ATTR_UPNP_FRIENDLY_NAME = "friendlyName" +ATTR_UPNP_MANUFACTURER = "manufacturer" +ATTR_UPNP_MANUFACTURER_URL = "manufacturerURL" +ATTR_UPNP_MODEL_NAME = "modelName" +ATTR_UPNP_MODEL_NUMBER = "modelNumber" +ATTR_UPNP_PRESENTATION_URL = "presentationURL" +ATTR_UPNP_SERIAL = "serialNumber" +ATTR_UPNP_UDN = "UDN" _LOGGER = logging.getLogger(__name__) @@ -157,24 +156,12 @@ class Scanner: def info_from_entry(entry, device_info): - """Get most important info from an entry.""" - url = urlparse(entry.location) + """Get info from an entry.""" info = { - ATTR_HOST: url.hostname, - ATTR_PORT: url.port, - ATTR_SSDP_DESCRIPTION: entry.location, - ATTR_ST: entry.st, + ATTR_SSDP_LOCATION: entry.location, + ATTR_SSDP_ST: entry.st, } - if device_info: - info[ATTR_NAME] = device_info.get("friendlyName") - info[ATTR_MODEL_NAME] = device_info.get("modelName") - info[ATTR_MODEL_NUMBER] = device_info.get("modelNumber") - info[ATTR_SERIAL] = device_info.get("serialNumber") - info[ATTR_MANUFACTURER] = device_info.get("manufacturer") - info[ATTR_MANUFACTURERURL] = device_info.get("manufacturerURL") - info[ATTR_UDN] = device_info.get("UDN") - info[ATTR_UPNP_DEVICE_TYPE] = device_info.get("deviceType") - info[ATTR_PRESENTATIONURL] = device_info.get("presentationURL") + info.update(device_info) return info diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index 1a6bfa36233..77e212a0833 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -1,13 +1,8 @@ { "domain": "ssdp", - "name": "SSDP", + "name": "Simple Service Discovery Protocol (SSDP)", "documentation": "https://www.home-assistant.io/integrations/ssdp", - "requirements": [ - "defusedxml==0.6.0", - "netdisco==2.6.0" - ], - "dependencies": [ - ], - "codeowners": [ - ] + "requirements": ["defusedxml==0.6.0", "netdisco==2.6.0"], + "dependencies": [], + "codeowners": [] } diff --git a/homeassistant/components/starline/.translations/ca.json b/homeassistant/components/starline/.translations/ca.json index 04426c2acfa..72cf1a66760 100644 --- a/homeassistant/components/starline/.translations/ca.json +++ b/homeassistant/components/starline/.translations/ca.json @@ -11,6 +11,7 @@ "app_id": "ID d'aplicaci\u00f3", "app_secret": "Secret" }, + "description": "ID d'aplicaci\u00f3 i codi secret de compte de desenvolupador de StarLine", "title": "Credencials d'aplicaci\u00f3" }, "auth_captcha": { diff --git a/homeassistant/components/starline/.translations/da.json b/homeassistant/components/starline/.translations/da.json new file mode 100644 index 00000000000..2a8cbcf1270 --- /dev/null +++ b/homeassistant/components/starline/.translations/da.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Forkert applikations-id eller hemmelighed", + "error_auth_mfa": "Forkert kode", + "error_auth_user": "Forkert brugernavn eller adgangskode" + }, + "step": { + "auth_app": { + "data": { + "app_id": "App-id", + "app_secret": "Hemmelighed" + }, + "description": "Applikations-id og hemmelig kode fra StarLine-udviklerkonto ", + "title": "Applikations-legitimationsoplysninger" + }, + "auth_captcha": { + "data": { + "captcha_code": "Kode fra billede" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS-kode" + }, + "description": "Indtast koden, der er sendt til telefon {phone_number}", + "title": "Tofaktor-godkendelse" + }, + "auth_user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "StarLine-konto email og adgangskode", + "title": "Brugeroplysninger" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/de.json b/homeassistant/components/starline/.translations/de.json index 657e6c08b1a..138969cc8b1 100644 --- a/homeassistant/components/starline/.translations/de.json +++ b/homeassistant/components/starline/.translations/de.json @@ -1,16 +1,31 @@ { "config": { "error": { - "error_auth_mfa": "Ung\u00fcltiger Code" + "error_auth_app": "Falsche Anwendungs-ID oder Secret", + "error_auth_mfa": "Ung\u00fcltiger Code", + "error_auth_user": "Falscher Benutzername oder falsches Passwort" }, "step": { + "auth_app": { + "data": { + "app_id": "App-ID", + "app_secret": "Geheimnis" + }, + "description": "Anwendungs-ID und Geheimcode aus dem StarLine-Entwicklerkonto", + "title": "Anmeldeinformationen der Anwendung" + }, "auth_captcha": { + "data": { + "captcha_code": "Code aus dem Bild" + }, + "description": "{captcha_img}", "title": "Captcha" }, "auth_mfa": { "data": { "mfa_code": "SMS Code" }, + "description": "Geben Sie den an das Telefon gesendeten Code ein {Telefon_Nummer}", "title": "2-Faktor-Authentifizierung" }, "auth_user": { @@ -18,8 +33,10 @@ "password": "Passwort", "username": "Benutzername" }, + "description": "StarLine Konto E-Mail und Passwort", "title": "Anmeldeinformationen" } - } + }, + "title": "StarLine" } } \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/fr.json b/homeassistant/components/starline/.translations/fr.json index 67a7dae4afc..d15f5c37edf 100644 --- a/homeassistant/components/starline/.translations/fr.json +++ b/homeassistant/components/starline/.translations/fr.json @@ -1,6 +1,7 @@ { "config": { "error": { + "error_auth_app": "ID applicatif ou code secret incorrect", "error_auth_mfa": "code incorrect", "error_auth_user": "identifiant ou mot de passe incorrect" }, @@ -10,6 +11,7 @@ "app_id": "ID de l'application", "app_secret": "Secret" }, + "description": "ID applicatif et code secret du compte d\u00e9veloppeur StarLine", "title": "Informations d'identification de l'application" }, "auth_captcha": { @@ -23,6 +25,7 @@ "data": { "mfa_code": "Code SMS" }, + "description": "Entrez le code envoy\u00e9 au t\u00e9l\u00e9phone {phone_number}", "title": "Autorisation \u00e0 deux facteurs" }, "auth_user": { diff --git a/homeassistant/components/starline/.translations/ko.json b/homeassistant/components/starline/.translations/ko.json new file mode 100644 index 00000000000..4d7ecf427f8 --- /dev/null +++ b/homeassistant/components/starline/.translations/ko.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID \ud639\uc740 \ubcf4\uc548\ud0a4\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "error_auth_mfa": "\ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "error_auth_user": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + }, + "step": { + "auth_app": { + "data": { + "app_id": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID", + "app_secret": "\ubcf4\uc548\ud0a4" + }, + "description": "StarLine \uac1c\ubc1c\uc790 \uacc4\uc815\uc758 \uc560\ud50c\ub9ac\ucf00\uc774\uc158 ID \ubc0f \ube44\ubc00\ubc88\ud638", + "title": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \uc790\uaca9 \uc99d\uba85" + }, + "auth_captcha": { + "data": { + "captcha_code": "\uc774\ubbf8\uc9c0\uc758 \ucf54\ub4dc" + }, + "description": "{captcha_img}", + "title": "\ubcf4\uc548 \ubb38\uc790" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS \ucf54\ub4dc" + }, + "description": "{phone_number} \uc804\ud654\ub85c \uc804\uc1a1\ub41c \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "2\ub2e8\uacc4 \uc778\uc99d" + }, + "auth_user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "StarLine \uacc4\uc815 \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638", + "title": "\uc0ac\uc6a9\uc790 \uc790\uaca9 \uc99d\uba85" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/nl.json b/homeassistant/components/starline/.translations/nl.json new file mode 100644 index 00000000000..df6a51136b6 --- /dev/null +++ b/homeassistant/components/starline/.translations/nl.json @@ -0,0 +1,42 @@ +{ + "config": { + "error": { + "error_auth_app": "Onjuiste applicatie-ID of geheim", + "error_auth_mfa": "Ongeldige code", + "error_auth_user": "Ongeldige gebruikersnaam of wachtwoord" + }, + "step": { + "auth_app": { + "data": { + "app_id": "Toepassings-ID", + "app_secret": "Geheime code" + }, + "description": "Toepassings-ID en de geheime code van StarLine developer account", + "title": "Inloggegevens van de applicatie" + }, + "auth_captcha": { + "data": { + "captcha_code": "Code van afbeelding" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS code" + }, + "description": "Voer de code in die wordt verzonden naar telefoon {phone_number}", + "title": "Tweestapsverificatie" + }, + "auth_user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam" + }, + "description": "StarLine-account e-mailadres en wachtwoord", + "title": "Gebruikersgegevens" + } + }, + "title": "StarLine" + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/pt-BR.json b/homeassistant/components/starline/.translations/pt-BR.json new file mode 100644 index 00000000000..158c2b01cf9 --- /dev/null +++ b/homeassistant/components/starline/.translations/pt-BR.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "error_auth_mfa": "C\u00f3digo incorreto", + "error_auth_user": "Usu\u00e1rio ou senha incorretos" + }, + "step": { + "auth_app": { + "title": "Credenciais do aplicativo" + }, + "auth_captcha": { + "data": { + "captcha_code": "C\u00f3digo da imagem" + }, + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "C\u00f3digo SMS" + }, + "description": "Digite o c\u00f3digo enviado para o telefone {phone_number}", + "title": "Autoriza\u00e7\u00e3o de dois fatores" + }, + "auth_user": { + "data": { + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/.translations/sv.json b/homeassistant/components/starline/.translations/sv.json new file mode 100644 index 00000000000..42d01b56753 --- /dev/null +++ b/homeassistant/components/starline/.translations/sv.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "auth_app": { + "data": { + "app_secret": "Hemlighet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 22772282a7c..303507b1491 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -1,17 +1,18 @@ """The StarLine component.""" import voluptuous as vol + from homeassistant.config_entries import ConfigEntry from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from .account import StarlineAccount from .const import ( - DOMAIN, - PLATFORMS, - SERVICE_UPDATE_STATE, - SERVICE_SET_SCAN_INTERVAL, CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL, + DOMAIN, + PLATFORMS, + SERVICE_SET_SCAN_INTERVAL, + SERVICE_UPDATE_STATE, ) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 2e7653eb380..8d0214d1b5c 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -1,6 +1,7 @@ """StarLine Account.""" -from datetime import timedelta, datetime -from typing import Callable, Optional, Dict, Any +from datetime import datetime, timedelta +from typing import Any, Callable, Dict, Optional + from starline import StarlineApi, StarlineDevice from homeassistant.config_entries import ConfigEntry @@ -8,13 +9,13 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.event import async_track_time_interval from .const import ( + DATA_EXPIRES, + DATA_SLID_TOKEN, + DATA_SLNET_TOKEN, + DATA_USER_ID, + DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER, - DEFAULT_SCAN_INTERVAL, - DATA_USER_ID, - DATA_SLNET_TOKEN, - DATA_SLID_TOKEN, - DATA_EXPIRES, ) @@ -22,7 +23,7 @@ class StarlineAccount: """StarLine Account class.""" def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry): - """Constructor.""" + """Initialize StarLine account.""" self._hass: HomeAssistant = hass self._config_entry: ConfigEntry = config_entry self._update_interval: int = DEFAULT_SCAN_INTERVAL diff --git a/homeassistant/components/starline/binary_sensor.py b/homeassistant/components/starline/binary_sensor.py index fd28ff74cf4..f2288e9363b 100644 --- a/homeassistant/components/starline/binary_sensor.py +++ b/homeassistant/components/starline/binary_sensor.py @@ -1,11 +1,12 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_DOOR, DEVICE_CLASS_LOCK, - DEVICE_CLASS_PROBLEM, DEVICE_CLASS_POWER, + DEVICE_CLASS_PROBLEM, + BinarySensorDevice, ) + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity @@ -43,7 +44,7 @@ class StarlineSensor(StarlineEntity, BinarySensorDevice): name: str, device_class: str, ): - """Constructor.""" + """Initialize sensor.""" super().__init__(account, device, key, name) self._device_class = device_class diff --git a/homeassistant/components/starline/config_flow.py b/homeassistant/components/starline/config_flow.py index 2253cc3cd22..fa559f62913 100644 --- a/homeassistant/components/starline/config_flow.py +++ b/homeassistant/components/starline/config_flow.py @@ -1,25 +1,26 @@ """Config flow to configure StarLine component.""" from typing import Optional + from starline import StarlineAuth import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from .const import ( # pylint: disable=unused-import - DOMAIN, CONF_APP_ID, CONF_APP_SECRET, - CONF_MFA_CODE, CONF_CAPTCHA_CODE, - LOGGER, - ERROR_AUTH_APP, - ERROR_AUTH_USER, - ERROR_AUTH_MFA, - DATA_USER_ID, - DATA_SLNET_TOKEN, - DATA_SLID_TOKEN, + CONF_MFA_CODE, DATA_EXPIRES, + DATA_SLID_TOKEN, + DATA_SLNET_TOKEN, + DATA_USER_ID, + DOMAIN, + ERROR_AUTH_APP, + ERROR_AUTH_MFA, + ERROR_AUTH_USER, + LOGGER, ) diff --git a/homeassistant/components/starline/device_tracker.py b/homeassistant/components/starline/device_tracker.py index b5254c761d8..6f202bbae52 100644 --- a/homeassistant/components/starline/device_tracker.py +++ b/homeassistant/components/starline/device_tracker.py @@ -2,6 +2,7 @@ from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.helpers.restore_state import RestoreEntity + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity diff --git a/homeassistant/components/starline/entity.py b/homeassistant/components/starline/entity.py index b0d948ae2c8..5db4d369f5e 100644 --- a/homeassistant/components/starline/entity.py +++ b/homeassistant/components/starline/entity.py @@ -1,6 +1,8 @@ """StarLine base entity.""" from typing import Callable, Optional + from homeassistant.helpers.entity import Entity + from .account import StarlineAccount, StarlineDevice @@ -10,7 +12,7 @@ class StarlineEntity(Entity): def __init__( self, account: StarlineAccount, device: StarlineDevice, key: str, name: str ): - """Constructor.""" + """Initialize StarLine entity.""" self._account = account self._device = device self._key = key diff --git a/homeassistant/components/starline/lock.py b/homeassistant/components/starline/lock.py index 0a20a36ae8b..804e8c8df2d 100644 --- a/homeassistant/components/starline/lock.py +++ b/homeassistant/components/starline/lock.py @@ -1,5 +1,6 @@ """Support for StarLine lock.""" from homeassistant.components.lock import LockDevice + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json index ef343aae4ce..aaffa20a698 100644 --- a/homeassistant/components/starline/manifest.json +++ b/homeassistant/components/starline/manifest.json @@ -3,11 +3,7 @@ "name": "StarLine", "config_flow": true, "documentation": "https://www.home-assistant.io/components/starline", - "requirements": [ - "starline==0.1.3" - ], + "requirements": ["starline==0.1.3"], "dependencies": [], - "codeowners": [ - "@anonym-tsk" - ] + "codeowners": ["@anonym-tsk"] } diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index ba0cef0255d..0c6cd8de683 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -3,6 +3,7 @@ from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity @@ -41,7 +42,7 @@ class StarlineSensor(StarlineEntity, Entity): unit: str, icon: str, ): - """Constructor.""" + """Initialize StarLine sensor.""" super().__init__(account, device, key, name) self._device_class = device_class self._unit = unit diff --git a/homeassistant/components/starline/switch.py b/homeassistant/components/starline/switch.py index 92dec10b9d3..920fe686d9a 100644 --- a/homeassistant/components/starline/switch.py +++ b/homeassistant/components/starline/switch.py @@ -1,5 +1,6 @@ """Support for StarLine switch.""" from homeassistant.components.switch import SwitchDevice + from .account import StarlineAccount, StarlineDevice from .const import DOMAIN from .entity import StarlineEntity diff --git a/homeassistant/components/starlingbank/manifest.json b/homeassistant/components/starlingbank/manifest.json index d68b6ea125c..82ac665031e 100644 --- a/homeassistant/components/starlingbank/manifest.json +++ b/homeassistant/components/starlingbank/manifest.json @@ -1,6 +1,6 @@ { "domain": "starlingbank", - "name": "Starlingbank", + "name": "Starling Bank", "documentation": "https://www.home-assistant.io/integrations/starlingbank", "requirements": ["starlingbank==3.2"], "dependencies": [], diff --git a/homeassistant/components/startca/manifest.json b/homeassistant/components/startca/manifest.json index 79637bfb124..a8aafee91ac 100644 --- a/homeassistant/components/startca/manifest.json +++ b/homeassistant/components/startca/manifest.json @@ -1,10 +1,8 @@ { "domain": "startca", - "name": "Startca", + "name": "Start.ca", "documentation": "https://www.home-assistant.io/integrations/startca", - "requirements": [ - "xmltodict==0.12.0" - ], + "requirements": ["xmltodict==0.12.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 55ae15cede7..e07f21e5d60 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -140,7 +140,7 @@ class StartcaData: async def async_update(self): """Get the Start.ca bandwidth data from the web service.""" _LOGGER.debug("Updating Start.ca usage data") - url = "https://www.start.ca/support/usage/api?key=" + self.api_key + url = f"https://www.start.ca/support/usage/api?key={self.api_key}" with async_timeout.timeout(REQUEST_TIMEOUT): req = await self.websession.get(url) if req.status != 200: diff --git a/homeassistant/components/statistics/manifest.json b/homeassistant/components/statistics/manifest.json index 3dab05942b9..8df384dd0bd 100644 --- a/homeassistant/components/statistics/manifest.json +++ b/homeassistant/components/statistics/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/statistics", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "after_dependencies": ["recorder"], + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 51868c6d0a8..6e042b1536f 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -1,25 +1,26 @@ """Support for statistics for sensor values.""" +from collections import deque import logging import statistics -from collections import deque import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.recorder.models import States +from homeassistant.components.recorder.util import execute, session_scope from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, - CONF_ENTITY_ID, - EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, - STATE_UNAVAILABLE, ATTR_UNIT_OF_MEASUREMENT, + CONF_ENTITY_ID, + CONF_NAME, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change from homeassistant.util import dt as dt_util -from homeassistant.components.recorder.util import session_scope, execute _LOGGER = logging.getLogger(__name__) @@ -275,7 +276,6 @@ class StatisticsSensor(Entity): If MaxAge is provided then query will restrict to entries younger then current datetime - MaxAge. """ - from homeassistant.components.recorder.models import States _LOGGER.debug("%s: initializing values from the database", self.entity_id) diff --git a/homeassistant/components/statsd/manifest.json b/homeassistant/components/statsd/manifest.json index 387576f8845..22478ee0fc7 100644 --- a/homeassistant/components/statsd/manifest.json +++ b/homeassistant/components/statsd/manifest.json @@ -1,10 +1,8 @@ { "domain": "statsd", - "name": "Statsd", + "name": "StatsD", "documentation": "https://www.home-assistant.io/integrations/statsd", - "requirements": [ - "statsd==3.2.1" - ], + "requirements": ["statsd==3.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/steam_online/manifest.json b/homeassistant/components/steam_online/manifest.json index ecc6198cb88..d45aea51388 100644 --- a/homeassistant/components/steam_online/manifest.json +++ b/homeassistant/components/steam_online/manifest.json @@ -1,10 +1,8 @@ { "domain": "steam_online", - "name": "Steam online", + "name": "Steam", "documentation": "https://www.home-assistant.io/integrations/steam_online", - "requirements": [ - "steamodd==4.21" - ], + "requirements": ["steamodd==4.21"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 85e5c49fb2c..2ec78c52645 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -1,6 +1,7 @@ """Sensor for Steam account status.""" from datetime import timedelta import logging +from time import mktime import steam import voluptuous as vol @@ -11,6 +12,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import utc_from_timestamp _LOGGER = logging.getLogger(__name__) @@ -70,7 +72,12 @@ class SteamSensor(Entity): self._steamod = steamod self._account = account self._profile = None - self._game = self._state = self._name = self._avatar = None + self._game = None + self._state = None + self._name = None + self._avatar = None + self._last_online = None + self._level = None @property def name(self): @@ -107,11 +114,19 @@ class SteamSensor(Entity): }.get(self._profile.status, STATE_OFFLINE) self._name = self._profile.persona self._avatar = self._profile.avatar_medium + self._last_online = self._get_last_online() + self._level = self._profile.level except self._steamod.api.HTTPTimeoutError as error: _LOGGER.warning(error) - self._game = self._state = self._name = self._avatar = None + self._game = None + self._state = None + self._name = None + self._avatar = None + self._last_online = None + self._level = None def _get_current_game(self): + """Gather current game name from APP ID.""" game_id = self._profile.current_game[0] game_extra_info = self._profile.current_game[2] @@ -140,10 +155,26 @@ class SteamSensor(Entity): _LOGGER.error("Unable to find name of app with ID=%s", game_id) return repr(game_id) + def _get_last_online(self): + """Convert last_online from the steam module into timestamp UTC.""" + last_online = utc_from_timestamp(mktime(self._profile.last_online)) + + if last_online: + return last_online + + return None + @property def device_state_attributes(self): """Return the state attributes.""" - return {"game": self._game} if self._game else None + attr = {} + if self._game is not None: + attr["game"] = self._game + if self._last_online is not None: + attr["last_online"] = self._last_online + if self._level is not None: + attr["level"] = self._level + return attr @property def entity_picture(self): diff --git a/homeassistant/components/stiebel_eltron/manifest.json b/homeassistant/components/stiebel_eltron/manifest.json index 385df6a5063..769d63328a7 100644 --- a/homeassistant/components/stiebel_eltron/manifest.json +++ b/homeassistant/components/stiebel_eltron/manifest.json @@ -2,13 +2,7 @@ "domain": "stiebel_eltron", "name": "STIEBEL ELTRON", "documentation": "https://www.home-assistant.io/integrations/stiebel_eltron", - "requirements": [ - "pystiebeleltron==0.0.1.dev2" - ], - "dependencies": [ - "modbus" - ], - "codeowners": [ - "@fucm" - ] -} \ No newline at end of file + "requirements": ["pystiebeleltron==0.0.1.dev2"], + "dependencies": ["modbus"], + "codeowners": ["@fucm"] +} diff --git a/homeassistant/components/stookalert/__init__.py b/homeassistant/components/stookalert/__init__.py new file mode 100644 index 00000000000..c9f86228515 --- /dev/null +++ b/homeassistant/components/stookalert/__init__.py @@ -0,0 +1 @@ +"""The Stookalert component.""" diff --git a/homeassistant/components/stookalert/binary_sensor.py b/homeassistant/components/stookalert/binary_sensor.py new file mode 100644 index 00000000000..c8515f401da --- /dev/null +++ b/homeassistant/components/stookalert/binary_sensor.py @@ -0,0 +1,79 @@ +"""This component provides support for Stookalert Binary Sensor.""" +from datetime import timedelta +import logging + +import stookalert +import voluptuous as vol + +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +from homeassistant.helpers import config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=60) +CONF_PROVINCE = "province" +DEFAULT_NAME = "Stookalert" +ATTRIBUTION = "Data provided by rivm.nl" +PROVINCES = [ + "Drenthe", + "Flevoland", + "Friesland", + "Gelderland", + "Groningen", + "Limburg", + "Noord-Brabant", + "Noord-Holland", + "Overijssel", + "Utrecht", + "Zeeland", + "Zuid-Holland", +] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_PROVINCE): vol.In(PROVINCES), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Stookalert binary sensor platform.""" + province = config[CONF_PROVINCE] + name = config[CONF_NAME] + api_handler = stookalert.stookalert(province) + add_entities([StookalertBinarySensor(name, api_handler)], update_before_add=True) + + +class StookalertBinarySensor(BinarySensorDevice): + """An implementation of RIVM Stookalert.""" + + def __init__(self, name, api_handler): + """Initialize a Stookalert device.""" + self._name = name + self._api_handler = api_handler + + @property + def device_state_attributes(self): + """Return the attribute(s) of the sensor.""" + state_attr = {ATTR_ATTRIBUTION: ATTRIBUTION} + + if self._api_handler.last_updated is not None: + state_attr["last_updated"] = self._api_handler.last_updated.isoformat() + + return state_attr + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def is_on(self): + """Return True if the Alert is active.""" + return self._api_handler.state == 1 + + def update(self): + """Update the data from the Stookalert handler.""" + self._api_handler.get_alerts() diff --git a/homeassistant/components/stookalert/manifest.json b/homeassistant/components/stookalert/manifest.json new file mode 100644 index 00000000000..73e59c2eddb --- /dev/null +++ b/homeassistant/components/stookalert/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "stookalert", + "name": "RIVM Stookalert", + "documentation": "https://www.home-assistant.io/integrations/stookalert", + "dependencies": [], + "codeowners": ["@fwestenberg"], + "requirements": ["stookalert==0.1.4"] +} diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 9304257f853..d88f90a83f8 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -1,10 +1,10 @@ """Provide functionality to stream video source.""" import logging +import secrets import threading import voluptuous as vol -from homeassistant.auth.util import generate_secret from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -23,7 +23,6 @@ from .const import ( from .core import PROVIDERS from .hls import async_setup_hls - _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) @@ -73,7 +72,7 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N stream.add_provider(fmt) if not stream.access_token: - stream.access_token = generate_secret() + stream.access_token = secrets.token_hex() stream.start() return hass.data[DOMAIN][ATTR_ENDPOINTS][fmt].format(stream.access_token) except Exception: @@ -83,6 +82,7 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N async def async_setup(hass, config): """Set up stream.""" # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel from .recorder import async_setup_recorder hass.data[DOMAIN] = {} @@ -163,6 +163,7 @@ class Stream: def start(self): """Start a stream.""" # Keep import here so that we can import stream integration without installing reqs + # pylint: disable=import-outside-toplevel from .worker import stream_worker if self._thread is None or not self._thread.isAlive(): diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index a00315e1064..873d76cf189 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -2,11 +2,8 @@ "domain": "stream", "name": "Stream", "documentation": "https://www.home-assistant.io/integrations/stream", - "requirements": [ - "av==6.1.2" - ], - "dependencies": [ - "http" - ], - "codeowners": ["@hunterjm"] + "requirements": ["av==6.1.2"], + "dependencies": ["http"], + "codeowners": ["@hunterjm"], + "quality_scale": "internal" } diff --git a/homeassistant/components/streamlabswater/manifest.json b/homeassistant/components/streamlabswater/manifest.json index 83b6314c4ab..52d6fb724f8 100644 --- a/homeassistant/components/streamlabswater/manifest.json +++ b/homeassistant/components/streamlabswater/manifest.json @@ -1,10 +1,8 @@ { "domain": "streamlabswater", - "name": "Streamlabs Water", + "name": "StreamLabs", "documentation": "https://www.home-assistant.io/integrations/streamlabswater", - "requirements": [ - "streamlabswater==1.0.1" - ], + "requirements": ["streamlabswater==1.0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/stt/manifest.json b/homeassistant/components/stt/manifest.json index 03ea914dec1..c25221f5baa 100644 --- a/homeassistant/components/stt/manifest.json +++ b/homeassistant/components/stt/manifest.json @@ -1,6 +1,6 @@ { "domain": "stt", - "name": "Stt", + "name": "Speech-to-Text (STT)", "documentation": "https://www.home-assistant.io/integrations/stt", "requirements": [], "dependencies": ["http"], diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 654f99f7ea4..90b6f2ebc73 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -1,6 +1,6 @@ { "domain": "suez_water", - "name": "Suez Water Consumption Sensor", + "name": "Suez Water", "documentation": "https://www.home-assistant.io/integrations/suez_water", "dependencies": [], "codeowners": ["@ooii"], diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 05f82183e46..bfa529adb34 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -2,10 +2,9 @@ from datetime import timedelta import logging -import voluptuous as vol - -from pysuez.client import PySuezError from pysuez import SuezClient +from pysuez.client import PySuezError +import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, VOLUME_LITERS diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index e848449e61e..213952bead3 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -1,12 +1,12 @@ """Support for functionality to keep track of the sun.""" -import logging from datetime import timedelta +import logging from homeassistant.const import ( CONF_ELEVATION, + EVENT_CORE_CONFIG_UPDATE, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, - EVENT_CORE_CONFIG_UPDATE, ) from homeassistant.core import callback from homeassistant.helpers.entity import Entity @@ -17,7 +17,6 @@ from homeassistant.helpers.sun import ( ) from homeassistant.util import dt as dt_util - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -77,7 +76,7 @@ async def async_setup(hass, config): """Track the state of the sun.""" if config.get(CONF_ELEVATION) is not None: _LOGGER.warning( - "Elevation is now configured in home assistant core. " + "Elevation is now configured in Home Assistant core. " "See https://home-assistant.io/docs/configuration/basic/" ) Sun(hass) diff --git a/homeassistant/components/sun/manifest.json b/homeassistant/components/sun/manifest.json index 31fcb1d7c2e..f0fb80923b1 100644 --- a/homeassistant/components/sun/manifest.json +++ b/homeassistant/components/sun/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sun", "requirements": [], "dependencies": [], - "codeowners": [ - "@Swamp-Ig" - ] + "codeowners": ["@Swamp-Ig"], + "quality_scale": "internal" } diff --git a/homeassistant/components/supervisord/sensor.py b/homeassistant/components/supervisord/sensor.py index e1a816f91ae..c79c09248b4 100644 --- a/homeassistant/components/supervisord/sensor.py +++ b/homeassistant/components/supervisord/sensor.py @@ -6,8 +6,8 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_URL -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/supla/manifest.json b/homeassistant/components/supla/manifest.json index 72635ca641a..742e6a07c4a 100644 --- a/homeassistant/components/supla/manifest.json +++ b/homeassistant/components/supla/manifest.json @@ -2,11 +2,7 @@ "domain": "supla", "name": "Supla", "documentation": "https://www.home-assistant.io/integrations/supla", - "requirements": [ - "pysupla==0.0.3" - ], + "requirements": ["pysupla==0.0.3"], "dependencies": [], - "codeowners": [ - "@mwegrzynek" - ] + "codeowners": ["@mwegrzynek"] } diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py new file mode 100644 index 00000000000..450d7eb9a15 --- /dev/null +++ b/homeassistant/components/surepetcare/__init__.py @@ -0,0 +1,163 @@ +"""Support for Sure Petcare cat/pet flaps.""" +import logging + +from surepy import ( + SurePetcare, + SurePetcareAuthenticationError, + SurePetcareError, + SureThingID, +) +import voluptuous as vol + +from homeassistant.const import ( + CONF_ID, + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_TYPE, + CONF_USERNAME, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + CONF_FLAPS, + CONF_HOUSEHOLD_ID, + CONF_PETS, + DATA_SURE_PETCARE, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + SPC, + TOPIC_UPDATE, +) + +_LOGGER = logging.getLogger(__name__) + + +FLAP_SCHEMA = vol.Schema( + {vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string} +) + +PET_SCHEMA = vol.Schema( + {vol.Required(CONF_ID): cv.positive_int, vol.Required(CONF_NAME): cv.string} +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_HOUSEHOLD_ID): cv.positive_int, + vol.Required(CONF_FLAPS): vol.All(cv.ensure_list, [FLAP_SCHEMA]), + vol.Required(CONF_PETS): vol.All(cv.ensure_list, [PET_SCHEMA]), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Initialize the Sure Petcare component.""" + conf = config[DOMAIN] + + # update interval + scan_interval = conf[CONF_SCAN_INTERVAL] + + # shared data + hass.data[DOMAIN] = hass.data[DATA_SURE_PETCARE] = {} + + # sure petcare api connection + try: + surepy = SurePetcare( + conf[CONF_USERNAME], + conf[CONF_PASSWORD], + conf[CONF_HOUSEHOLD_ID], + hass.loop, + async_get_clientsession(hass), + ) + await surepy.refresh_token() + except SurePetcareAuthenticationError: + _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") + return False + except SurePetcareError as error: + _LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error) + return False + + # add flaps + things = [ + { + CONF_NAME: flap[CONF_NAME], + CONF_ID: flap[CONF_ID], + CONF_TYPE: SureThingID.FLAP.name, + } + for flap in conf[CONF_FLAPS] + ] + + # add pets + things.extend( + [ + { + CONF_NAME: pet[CONF_NAME], + CONF_ID: pet[CONF_ID], + CONF_TYPE: SureThingID.PET.name, + } + for pet in conf[CONF_PETS] + ] + ) + + spc = hass.data[DATA_SURE_PETCARE][SPC] = SurePetcareAPI( + hass, surepy, things, conf[CONF_HOUSEHOLD_ID] + ) + + # initial update + await spc.async_update() + + async_track_time_interval(hass, spc.async_update, scan_interval) + + # load platforms + hass.async_create_task( + hass.helpers.discovery.async_load_platform("binary_sensor", DOMAIN, {}, config) + ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config) + ) + + return True + + +class SurePetcareAPI: + """Define a generic Sure Petcare object.""" + + def __init__(self, hass, surepy, ids, household_id): + """Initialize the Sure Petcare object.""" + self.hass = hass + self.surepy = surepy + self.household_id = household_id + self.ids = ids + self.states = {} + + async def async_update(self, args=None): + """Refresh Sure Petcare data.""" + for thing in self.ids: + sure_id = thing[CONF_ID] + sure_type = thing[CONF_TYPE] + + try: + type_state = self.states.setdefault(sure_type, {}) + + if sure_type == SureThingID.FLAP.name: + type_state[sure_id] = await self.surepy.get_flap_data(sure_id) + elif sure_type == SureThingID.PET.name: + type_state[sure_id] = await self.surepy.get_pet_data(sure_id) + + except SurePetcareError as error: + _LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error) + + async_dispatcher_send(self.hass, TOPIC_UPDATE) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py new file mode 100644 index 00000000000..100da5cb790 --- /dev/null +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -0,0 +1,176 @@ +"""Support for Sure PetCare Flaps/Pets binary sensors.""" +import logging + +from surepy import SureLocationID, SureLockStateID, SureThingID + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_LOCK, + DEVICE_CLASS_PRESENCE, + BinarySensorDevice, +) +from homeassistant.const import CONF_ID, CONF_NAME, CONF_TYPE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_SURE_PETCARE, DEFAULT_DEVICE_CLASS, SPC, TOPIC_UPDATE + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up Sure PetCare Flaps sensors based on a config entry.""" + if discovery_info is None: + return + + entities = [] + + spc = hass.data[DATA_SURE_PETCARE][SPC] + + for thing in spc.ids: + sure_id = thing[CONF_ID] + sure_type = thing[CONF_TYPE] + + if sure_type == SureThingID.FLAP.name: + entity = Flap(sure_id, thing[CONF_NAME], spc) + elif sure_type == SureThingID.PET.name: + entity = Pet(sure_id, thing[CONF_NAME], spc) + + entities.append(entity) + + async_add_entities(entities, True) + + +class SurePetcareBinarySensor(BinarySensorDevice): + """A binary sensor implementation for Sure Petcare Entities.""" + + def __init__( + self, _id: int, name: str, spc, device_class: str, sure_type: SureThingID + ): + """Initialize a Sure Petcare binary sensor.""" + self._id = _id + self._name = name + self._spc = spc + self._device_class = device_class + self._sure_type = sure_type + self._state = {} + + self._async_unsub_dispatcher_connect = None + + @property + def is_on(self): + """Return true if entity is on/unlocked.""" + return bool(self._state) + + @property + def should_poll(self): + """Return true.""" + return False + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + return self._state + + @property + def device_class(self): + """Return the device class.""" + return DEFAULT_DEVICE_CLASS if not self._device_class else self._device_class + + @property + def unique_id(self): + """Return an unique ID.""" + return f"{self._spc.household_id}-{self._id}" + + async def async_update(self): + """Get the latest data and update the state.""" + self._state = self._spc.states[self._sure_type][self._id].get("data") + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update + ) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() + + +class Flap(SurePetcareBinarySensor): + """Sure Petcare Flap.""" + + def __init__(self, _id: int, name: str, spc): + """Initialize a Sure Petcare Flap.""" + super().__init__( + _id, + f"Flap {name.capitalize()}", + spc, + DEVICE_CLASS_LOCK, + SureThingID.FLAP.name, + ) + + @property + def is_on(self): + """Return true if entity is on/unlocked.""" + try: + return bool(self._state["locking"]["mode"] == SureLockStateID.UNLOCKED) + except (KeyError, TypeError): + return None + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attributes = None + if self._state: + try: + attributes = { + "battery_voltage": self._state["battery"] / 4, + "locking_mode": self._state["locking"]["mode"], + "device_rssi": self._state["signal"]["device_rssi"], + "hub_rssi": self._state["signal"]["hub_rssi"], + } + + except (KeyError, TypeError) as error: + _LOGGER.error( + "Error getting device state attributes from %s: %s\n\n%s", + self._name, + error, + self._state, + ) + attributes = self._state + + return attributes + + +class Pet(SurePetcareBinarySensor): + """Sure Petcare Pet.""" + + def __init__(self, _id: int, name: str, spc): + """Initialize a Sure Petcare Pet.""" + super().__init__( + _id, + f"Pet {name.capitalize()}", + spc, + DEVICE_CLASS_PRESENCE, + SureThingID.PET.name, + ) + + @property + def is_on(self): + """Return true if entity is at home.""" + try: + return bool(self._state["where"] == SureLocationID.INSIDE) + except (KeyError, TypeError): + return False diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py new file mode 100644 index 00000000000..731bfba07e6 --- /dev/null +++ b/homeassistant/components/surepetcare/const.py @@ -0,0 +1,27 @@ +"""Constants for the Sure Petcare component.""" +from datetime import timedelta + +DOMAIN = "surepetcare" +DEFAULT_DEVICE_CLASS = "lock" +DEFAULT_ICON = "mdi:cat" +DEFAULT_SCAN_INTERVAL = timedelta(minutes=3) + +DATA_SURE_PETCARE = f"data_{DOMAIN}" +SPC = "spc" +SUREPY = "surepy" + +CONF_HOUSEHOLD_ID = "household_id" +CONF_FLAPS = "flaps" +CONF_PETS = "pets" +CONF_DATA = "data" + +SURE_IDS = "sure_ids" + +# platforms +TOPIC_UPDATE = f"{DOMAIN}_data_update" + +# flap +BATTERY_ICON = "mdi:battery" +SURE_BATT_VOLTAGE_FULL = 1.6 # voltage +SURE_BATT_VOLTAGE_LOW = 1.25 # voltage +SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json new file mode 100644 index 00000000000..b4879932714 --- /dev/null +++ b/homeassistant/components/surepetcare/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "surepetcare", + "name": "Sure Petcare", + "documentation": "https://www.home-assistant.io/integrations/surepetcare", + "dependencies": [], + "codeowners": ["@benleb"], + "requirements": ["surepy==0.1.10"] +} diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py new file mode 100644 index 00000000000..dd7fdcb0316 --- /dev/null +++ b/homeassistant/components/surepetcare/sensor.py @@ -0,0 +1,134 @@ +"""Support for Sure PetCare Flaps/Pets sensors.""" +import logging + +from surepy import SureThingID + +from homeassistant.const import ( + ATTR_VOLTAGE, + CONF_ID, + CONF_NAME, + CONF_TYPE, + DEVICE_CLASS_BATTERY, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import ( + DATA_SURE_PETCARE, + SPC, + SURE_BATT_VOLTAGE_DIFF, + SURE_BATT_VOLTAGE_LOW, + TOPIC_UPDATE, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up Sure PetCare Flaps sensors.""" + if discovery_info is None: + return + + spc = hass.data[DATA_SURE_PETCARE][SPC] + async_add_entities( + [ + FlapBattery(entity[CONF_ID], entity[CONF_NAME], spc) + for entity in spc.ids + if entity[CONF_TYPE] == SureThingID.FLAP.name + ], + True, + ) + + +class FlapBattery(Entity): + """Sure Petcare Flap.""" + + def __init__(self, _id: int, name: str, spc): + """Initialize a Sure Petcare Flap battery sensor.""" + self._id = _id + self._name = f"Flap {name.capitalize()} Battery Level" + self._spc = spc + self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data") + + self._async_unsub_dispatcher_connect = None + + @property + def should_poll(self): + """Return true.""" + return False + + @property + def name(self): + """Return the name of the device if any.""" + return self._name + + @property + def state(self): + """Return battery level in percent.""" + try: + per_battery_voltage = self._state["battery"] / 4 + voltage_diff = per_battery_voltage - SURE_BATT_VOLTAGE_LOW + battery_percent = int(voltage_diff / SURE_BATT_VOLTAGE_DIFF * 100) + except (KeyError, TypeError): + battery_percent = None + + return battery_percent + + @property + def unique_id(self): + """Return an unique ID.""" + return f"{self._spc.household_id}-{self._id}" + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_BATTERY + + @property + def device_state_attributes(self): + """Return state attributes.""" + attributes = None + if self._state: + try: + voltage_per_battery = float(self._state["battery"]) / 4 + attributes = { + ATTR_VOLTAGE: f"{float(self._state['battery']):.2f}", + f"{ATTR_VOLTAGE}_per_battery": f"{voltage_per_battery:.2f}", + } + except (KeyError, TypeError) as error: + attributes = self._state + _LOGGER.error( + "Error getting device state attributes from %s: %s\n\n%s", + self._name, + error, + self._state, + ) + + return attributes + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return "%" + + async def async_update(self): + """Get the latest data and update the state.""" + self._state = self._spc.states[SureThingID.FLAP.name][self._id].get("data") + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update + ) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() diff --git a/homeassistant/components/swiss_hydrological_data/manifest.json b/homeassistant/components/swiss_hydrological_data/manifest.json index f54ef55ce17..88d7bfe5104 100644 --- a/homeassistant/components/swiss_hydrological_data/manifest.json +++ b/homeassistant/components/swiss_hydrological_data/manifest.json @@ -1,12 +1,8 @@ { "domain": "swiss_hydrological_data", - "name": "Swiss hydrological data", + "name": "Swiss Hydrological Data", "documentation": "https://www.home-assistant.io/integrations/swiss_hydrological_data", - "requirements": [ - "swisshydrodata==0.0.3" - ], + "requirements": ["swisshydrodata==0.0.3"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/swiss_hydrological_data/sensor.py b/homeassistant/components/swiss_hydrological_data/sensor.py index c8e7b9d6fc2..d4624e82bb7 100644 --- a/homeassistant/components/swiss_hydrological_data/sensor.py +++ b/homeassistant/components/swiss_hydrological_data/sensor.py @@ -13,7 +13,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by the Swiss Federal Office for the " "Environment FOEN" +ATTRIBUTION = "Data provided by the Swiss Federal Office for the Environment FOEN" ATTR_DELTA_24H = "delta-24h" ATTR_MAX_1H = "max-1h" diff --git a/homeassistant/components/swiss_public_transport/manifest.json b/homeassistant/components/swiss_public_transport/manifest.json index c91d85fecd7..2ef1e8fa69d 100644 --- a/homeassistant/components/swiss_public_transport/manifest.json +++ b/homeassistant/components/swiss_public_transport/manifest.json @@ -2,11 +2,7 @@ "domain": "swiss_public_transport", "name": "Swiss public transport", "documentation": "https://www.home-assistant.io/integrations/swiss_public_transport", - "requirements": [ - "python_opendata_transport==0.1.4" - ], + "requirements": ["python_opendata_transport==0.2.1"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index be967247dc7..1562a498080 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -24,6 +24,7 @@ ATTR_START = "start" ATTR_TARGET = "destination" ATTR_TRAIN_NUMBER = "train_number" ATTR_TRANSFERS = "transfers" +ATTR_DELAY = "delay" ATTRIBUTION = "Data provided by transport.opendata.ch" @@ -113,6 +114,7 @@ class SwissPublicTransportSensor(Entity): ATTR_TARGET: self._opendata.to_name, ATTR_REMAINING_TIME: f"{self._remaining_time}", ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_DELAY: self._opendata.connections[0]["delay"], } return attr diff --git a/homeassistant/components/swisscom/manifest.json b/homeassistant/components/swisscom/manifest.json index 27e33c81607..caf18c8da3e 100644 --- a/homeassistant/components/swisscom/manifest.json +++ b/homeassistant/components/swisscom/manifest.json @@ -1,6 +1,6 @@ { "domain": "swisscom", - "name": "Swisscom", + "name": "Swisscom Internet-Box", "documentation": "https://www.home-assistant.io/integrations/swisscom", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/switch/.translations/da.json b/homeassistant/components/switch/.translations/da.json new file mode 100644 index 00000000000..2514a56a010 --- /dev/null +++ b/homeassistant/components/switch/.translations/da.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "Skift {entity_name}", + "turn_off": "Sluk {entity_name}", + "turn_on": "T\u00e6nd for {entity_name}" + }, + "condition_type": { + "is_off": "{entity_name} er fra", + "is_on": "{entity_name} er til", + "turn_off": "{entity_name} slukket", + "turn_on": "{entity_name} t\u00e6ndt" + }, + "trigger_type": { + "turned_off": "{entity_name} slukkede", + "turned_on": "{entity_name} t\u00e6ndte" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json index 02c303f9329..d3b9b1dd169 100644 --- a/homeassistant/components/switch/.translations/ko.json +++ b/homeassistant/components/switch/.translations/ko.json @@ -6,14 +6,14 @@ "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", - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", + "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", + "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { - "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", - "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4" + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9c8 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index 26d5658d668..fb5c2969d7e 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -4,30 +4,25 @@ import logging import voluptuous as vol -from homeassistant.loader import bind_hass -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity +from homeassistant.const import ( + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.const import ( - STATE_ON, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - SERVICE_TOGGLE, -) -from homeassistant.components import group - +from homeassistant.helpers.entity import ToggleEntity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs DOMAIN = "switch" SCAN_INTERVAL = timedelta(seconds=30) -GROUP_NAME_ALL_SWITCHES = "all switches" -ENTITY_ID_ALL_SWITCHES = group.ENTITY_ID_FORMAT.format("all_switches") - ENTITY_ID_FORMAT = DOMAIN + ".{}" ATTR_TODAY_ENERGY_KWH = "today_energy_kwh" @@ -51,19 +46,18 @@ _LOGGER = logging.getLogger(__name__) @bind_hass -def is_on(hass, entity_id=None): +def is_on(hass, entity_id): """Return if the switch is on based on the statemachine. Async friendly. """ - entity_id = entity_id or ENTITY_ID_ALL_SWITCHES return hass.states.is_state(entity_id, STATE_ON) async def async_setup(hass, config): """Track states and offer events for switches.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_SWITCHES + _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) diff --git a/homeassistant/components/switch/device_action.py b/homeassistant/components/switch/device_action.py index a65c1acc512..a50131f094c 100644 --- a/homeassistant/components/switch/device_action.py +++ b/homeassistant/components/switch/device_action.py @@ -1,13 +1,14 @@ """Provides device actions for switches.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, Context from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import TemplateVarsType, ConfigType -from . import DOMAIN +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from . import DOMAIN ACTION_SCHEMA = toggle_entity.ACTION_SCHEMA.extend({vol.Required(CONF_DOMAIN): DOMAIN}) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 56f8f6c196e..87aefdb616d 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -1,14 +1,15 @@ """Provides device conditions for switches.""" from typing import Dict, List + import voluptuous as vol -from homeassistant.core import HomeAssistant from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN -from homeassistant.helpers.typing import ConfigType +from homeassistant.core import HomeAssistant from homeassistant.helpers.condition import ConditionCheckerType -from . import DOMAIN +from homeassistant.helpers.typing import ConfigType +from . import DOMAIN CONDITION_SCHEMA = toggle_entity.CONDITION_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 7f0458b3e9f..cb5d5f7aa0e 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -1,14 +1,15 @@ """Provides device triggers for switches.""" from typing import List + import voluptuous as vol -from homeassistant.core import HomeAssistant, CALLBACK_TYPE from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import toggle_entity from homeassistant.const import CONF_DOMAIN +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers.typing import ConfigType -from . import DOMAIN +from . import DOMAIN TRIGGER_SCHEMA = toggle_entity.TRIGGER_SCHEMA.extend( {vol.Required(CONF_DOMAIN): DOMAIN} diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 1bdc1d39083..5486b8d880c 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,10 +1,11 @@ """Light support for switch entities.""" import logging -from typing import cast, Callable, Dict, Optional, Sequence +from typing import Callable, Dict, Optional, Sequence, cast import voluptuous as vol from homeassistant.components import switch +from homeassistant.components.light import PLATFORM_SCHEMA, Light from homeassistant.const import ( ATTR_ENTITY_ID, CONF_ENTITY_ID, @@ -18,9 +19,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.components.light import PLATFORM_SCHEMA, Light - - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index 73daca18c6a..b14c8ca48d5 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -3,8 +3,7 @@ "name": "Switch", "documentation": "https://www.home-assistant.io/integrations/switch", "requirements": [], - "dependencies": [ - "group" - ], - "codeowners": [] + "dependencies": ["group"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py index 7ed1f70cb97..d2bfc569956 100644 --- a/homeassistant/components/switch/reproduce_state.py +++ b/homeassistant/components/switch/reproduce_state.py @@ -5,10 +5,10 @@ from typing import Iterable, Optional from homeassistant.const import ( ATTR_ENTITY_ID, - STATE_ON, - STATE_OFF, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 204a3605bb8..4193a88e3ed 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -1,12 +1,8 @@ { "domain": "switchbot", - "name": "Switchbot", + "name": "SwitchBot", "documentation": "https://www.home-assistant.io/integrations/switchbot", - "requirements": [ - "PySwitchbot==0.6.2" - ], + "requirements": ["PySwitchbot==0.6.2"], "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 55e2a8b9641..ed7fba570a8 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -78,7 +78,7 @@ class SwitchBot(SwitchDevice, RestoreEntity): @property def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return self._mac.replace(":", "") @property diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 9f4347d61d2..63f2aa47a3a 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -5,6 +5,8 @@ from datetime import datetime, timedelta from logging import getLogger from typing import Dict, Optional +from aioswitcher.api import SwitcherV2Api +from aioswitcher.bridge import SwitcherV2Bridge import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_EDIT @@ -88,7 +90,6 @@ async def _validate_edit_permission( async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: """Set up the switcher component.""" - from aioswitcher.bridge import SwitcherV2Bridge phone_id = config[DOMAIN][CONF_PHONE_ID] device_id = config[DOMAIN][CONF_DEVICE_ID] @@ -99,7 +100,7 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: await v2bridge.start() async def async_stop_bridge(event: EventType) -> None: - """On homeassistant stop, gracefully stop the bridge if running.""" + """On Home Assistant stop, gracefully stop the bridge if running.""" await v2bridge.stop() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_bridge) @@ -122,7 +123,6 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: async def async_set_auto_off_service(service: ServiceCallType) -> None: """Use for handling setting device auto-off service calls.""" - from aioswitcher.api import SwitcherV2Api await _validate_edit_permission( hass, service.context, service.data[CONF_ENTITY_ID] diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 453ae542b2c..81f5d2085c6 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -2,11 +2,7 @@ "domain": "switcher_kis", "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", - "codeowners": [ - "@tomerfi" - ], - "requirements": [ - "aioswitcher==2019.4.26" - ], + "codeowners": ["@tomerfi"], + "requirements": ["aioswitcher==2019.4.26"], "dependencies": [] } diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index 454baca4eef..c8eaddcb5bd 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,7 +1,16 @@ """Home Assistant Switcher Component Switch platform.""" from logging import getLogger -from typing import Callable, Dict, TYPE_CHECKING +from typing import TYPE_CHECKING, Callable, Dict + +from aioswitcher.api import SwitcherV2Api +from aioswitcher.consts import ( + COMMAND_OFF, + COMMAND_ON, + STATE_OFF as SWITCHER_STATE_OFF, + STATE_ON as SWITCHER_STATE_ON, + WAITING_TEXT, +) from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -16,6 +25,7 @@ from . import ( SIGNAL_SWITCHER_DEVICE_UPDATE, ) +# pylint: disable=ungrouped-imports if TYPE_CHECKING: from aioswitcher.devices import SwitcherV2Device from aioswitcher.api.messages import SwitcherV2ControlResponseMSG @@ -70,7 +80,6 @@ class SwitcherControl(SwitchDevice): @property def is_on(self) -> bool: """Return True if entity is on.""" - from aioswitcher.consts import STATE_ON as SWITCHER_STATE_ON return self._state == SWITCHER_STATE_ON @@ -82,7 +91,6 @@ class SwitcherControl(SwitchDevice): @property def device_state_attributes(self) -> Dict: """Return the optional state attributes.""" - from aioswitcher.consts import WAITING_TEXT attribs = {} @@ -96,10 +104,6 @@ class SwitcherControl(SwitchDevice): @property def available(self) -> bool: """Return True if entity is available.""" - from aioswitcher.consts import ( - STATE_OFF as SWITCHER_STATE_OFF, - STATE_ON as SWITCHER_STATE_ON, - ) return self._state in [SWITCHER_STATE_ON, SWITCHER_STATE_OFF] @@ -135,13 +139,6 @@ class SwitcherControl(SwitchDevice): async def _control_device(self, send_on: bool) -> None: """Turn the entity on or off.""" - from aioswitcher.api import SwitcherV2Api - from aioswitcher.consts import ( - COMMAND_OFF, - COMMAND_ON, - STATE_OFF as SWITCHER_STATE_OFF, - STATE_ON as SWITCHER_STATE_ON, - ) response: "SwitcherV2ControlResponseMSG" = None async with SwitcherV2Api( diff --git a/homeassistant/components/switchmate/manifest.json b/homeassistant/components/switchmate/manifest.json index b15024b545c..1035b86d6ce 100644 --- a/homeassistant/components/switchmate/manifest.json +++ b/homeassistant/components/switchmate/manifest.json @@ -1,12 +1,8 @@ { "domain": "switchmate", - "name": "Switchmate", + "name": "Switchmate SimplySmart Home", "documentation": "https://www.home-assistant.io/integrations/switchmate", - "requirements": [ - "pySwitchmate==0.4.6" - ], + "requirements": ["pySwitchmate==0.4.6"], "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index ddb0db3feee..268115434cf 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -46,7 +46,7 @@ class SwitchmateEntity(SwitchDevice): @property def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return self._mac.replace(":", "") @property diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 41ac1024a85..ac9f6f8b2cc 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -1,10 +1,8 @@ { "domain": "syncthru", - "name": "Syncthru", + "name": "Samsung SyncThru Printer", "documentation": "https://www.home-assistant.io/integrations/syncthru", - "requirements": [ - "pysyncthru==0.5.0" - ], + "requirements": ["pysyncthru==0.5.0"], "dependencies": [], "codeowners": ["@nielstron"] } diff --git a/homeassistant/components/synology/manifest.json b/homeassistant/components/synology/manifest.json index ea743836c74..c541a4903c1 100644 --- a/homeassistant/components/synology/manifest.json +++ b/homeassistant/components/synology/manifest.json @@ -2,9 +2,7 @@ "domain": "synology", "name": "Synology", "documentation": "https://www.home-assistant.io/integrations/synology", - "requirements": [ - "py-synology==0.2.0" - ], + "requirements": ["py-synology==0.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/synology_chat/manifest.json b/homeassistant/components/synology_chat/manifest.json index 3522ba0405c..bfc888b99d9 100644 --- a/homeassistant/components/synology_chat/manifest.json +++ b/homeassistant/components/synology_chat/manifest.json @@ -1,6 +1,6 @@ { "domain": "synology_chat", - "name": "Synology chat", + "name": "Synology Chat", "documentation": "https://www.home-assistant.io/integrations/synology_chat", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/synology_chat/notify.py b/homeassistant/components/synology_chat/notify.py index c67ef79f5d5..3e1aeb4ce13 100644 --- a/homeassistant/components/synology_chat/notify.py +++ b/homeassistant/components/synology_chat/notify.py @@ -5,14 +5,13 @@ import logging import requests import voluptuous as vol -from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_RESOURCE, CONF_VERIFY_SSL +import homeassistant.helpers.config_validation as cv ATTR_FILE_URL = "file_url" diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json index c507e07c717..47c8c46fee1 100644 --- a/homeassistant/components/synology_srm/manifest.json +++ b/homeassistant/components/synology_srm/manifest.json @@ -2,11 +2,7 @@ "domain": "synology_srm", "name": "Synology SRM", "documentation": "https://www.home-assistant.io/integrations/synology_srm", - "requirements": [ - "synology-srm==0.0.7" - ], + "requirements": ["synology-srm==0.0.7"], "dependencies": [], - "codeowners": [ - "@aerialls" - ] + "codeowners": ["@aerialls"] } diff --git a/homeassistant/components/synologydsm/manifest.json b/homeassistant/components/synologydsm/manifest.json index d7dc76b6ac9..d9405b3ee68 100644 --- a/homeassistant/components/synologydsm/manifest.json +++ b/homeassistant/components/synologydsm/manifest.json @@ -1,10 +1,8 @@ { "domain": "synologydsm", - "name": "Synologydsm", + "name": "SynologyDSM", "documentation": "https://www.home-assistant.io/integrations/synologydsm", - "requirements": [ - "python-synology==0.2.0" - ], + "requirements": ["python-synology==0.3.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/syslog/manifest.json b/homeassistant/components/syslog/manifest.json index 00dda3ebc02..d3964b747a2 100644 --- a/homeassistant/components/syslog/manifest.json +++ b/homeassistant/components/syslog/manifest.json @@ -4,7 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/syslog", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/system_health/manifest.json b/homeassistant/components/system_health/manifest.json index 4de7af3f862..c75054232bc 100644 --- a/homeassistant/components/system_health/manifest.json +++ b/homeassistant/components/system_health/manifest.json @@ -1,10 +1,9 @@ { "domain": "system_health", - "name": "System health", + "name": "System Health", "documentation": "https://www.home-assistant.io/integrations/system_health", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 68561d45f8f..44ff9c49a01 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -8,8 +8,8 @@ import voluptuous as vol from homeassistant import __path__ as HOMEASSISTANT_PATH from homeassistant.components.http import HomeAssistantView -import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv CONF_MAX_ENTRIES = "max_entries" CONF_FIRE_EVENT = "fire_event" @@ -57,6 +57,7 @@ def _figure_out_source(record, call_stack, hass): paths = [HOMEASSISTANT_PATH[0], hass.config.config_dir] try: # If netdisco is installed check its path too. + # pylint: disable=import-outside-toplevel from netdisco import __path__ as netdisco_path paths.append(netdisco_path[0]) diff --git a/homeassistant/components/system_log/manifest.json b/homeassistant/components/system_log/manifest.json index 0bc14a56e71..77cfbd62059 100644 --- a/homeassistant/components/system_log/manifest.json +++ b/homeassistant/components/system_log/manifest.json @@ -1,10 +1,9 @@ { "domain": "system_log", - "name": "System log", + "name": "System Log", "documentation": "https://www.home-assistant.io/integrations/system_log", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [] + "dependencies": ["http"], + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 0614ef4b91c..81712edd404 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -1,10 +1,8 @@ { "domain": "systemmonitor", - "name": "Systemmonitor", + "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": [ - "psutil==5.6.7" - ], + "requirements": ["psutil==5.6.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 1739cbb9254..ebf605bdc75 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -9,23 +9,29 @@ import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.util import Throttle +from .const import CONF_FALLBACK + _LOGGER = logging.getLogger(__name__) -DATA_TADO = "tado_data" DOMAIN = "tado" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SIGNAL_TADO_UPDATE_RECEIVED = "tado_update_received_{}_{}" TADO_COMPONENTS = ["sensor", "climate"] +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=15) + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_FALLBACK, default=True): cv.boolean, } ) }, @@ -38,91 +44,106 @@ def setup(hass, config): username = config[DOMAIN][CONF_USERNAME] password = config[DOMAIN][CONF_PASSWORD] - try: - tado = Tado(username, password) - tado.setDebugging(True) - except (RuntimeError, urllib.error.HTTPError): - _LOGGER.error("Unable to connect to mytado with username and password") + tadoconnector = TadoConnector(hass, username, password) + if not tadoconnector.setup(): return False - hass.data[DATA_TADO] = TadoDataStore(tado) + hass.data[DOMAIN] = tadoconnector + # Do first update + tadoconnector.update() + + # Load components for component in TADO_COMPONENTS: - load_platform(hass, component, DOMAIN, {}, config) + load_platform( + hass, + component, + DOMAIN, + {CONF_FALLBACK: config[DOMAIN][CONF_FALLBACK]}, + config, + ) + + # Poll for updates in the background + hass.helpers.event.track_time_interval( + lambda now: tadoconnector.update(), SCAN_INTERVAL + ) return True -class TadoDataStore: +class TadoConnector: """An object to store the Tado data.""" - def __init__(self, tado): - """Initialize Tado data store.""" - self.tado = tado + def __init__(self, hass, username, password): + """Initialize Tado Connector.""" + self.hass = hass + self._username = username + self._password = password - self.sensors = {} - self.data = {} + self.tado = None + self.zones = None + self.devices = None + self.data = { + "zone": {}, + "device": {}, + } + + def setup(self): + """Connect to Tado and fetch the zones.""" + try: + self.tado = Tado(self._username, self._password) + except (RuntimeError, urllib.error.HTTPError) as exc: + _LOGGER.error("Unable to connect: %s", exc) + return False + + self.tado.setDebugging(True) + + # Load zones and devices + self.zones = self.tado.getZones() + self.devices = self.tado.getMe()["homes"] + + return True @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """Update the internal data from mytado.com.""" - for data_id, sensor in list(self.sensors.items()): - data = None + """Update the registered zones.""" + for zone in self.zones: + self.update_sensor("zone", zone["id"]) + for device in self.devices: + self.update_sensor("device", device["id"]) - try: - if "zone" in sensor: - _LOGGER.debug( - "Querying mytado.com for zone %s %s", - sensor["id"], - sensor["name"], - ) - data = self.tado.getState(sensor["id"]) + def update_sensor(self, sensor_type, sensor): + """Update the internal data from Tado.""" + _LOGGER.debug("Updating %s %s", sensor_type, sensor) + try: + if sensor_type == "zone": + data = self.tado.getState(sensor) + elif sensor_type == "device": + data = self.tado.getDevices()[0] + else: + _LOGGER.debug("Unknown sensor: %s", sensor_type) + return + except RuntimeError: + _LOGGER.error( + "Unable to connect to Tado while updating %s %s", sensor_type, sensor, + ) + return - if "device" in sensor: - _LOGGER.debug( - "Querying mytado.com for device %s %s", - sensor["id"], - sensor["name"], - ) - data = self.tado.getDevices()[0] + self.data[sensor_type][sensor] = data - except RuntimeError: - _LOGGER.error( - "Unable to connect to myTado. %s %s", sensor["id"], sensor["id"] - ) + _LOGGER.debug("Dispatching update to %s %s: %s", sensor_type, sensor, data) + dispatcher_send( + self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format(sensor_type, sensor) + ) - self.data[data_id] = data - - def add_sensor(self, data_id, sensor): - """Add a sensor to update in _update().""" - self.sensors[data_id] = sensor - self.data[data_id] = None - - def get_data(self, data_id): - """Get the cached data.""" - data = {"error": "no data"} - - if data_id in self.data: - data = self.data[data_id] - - return data - - def get_zones(self): - """Wrap for getZones().""" - return self.tado.getZones() - - def get_capabilities(self, tado_id): - """Wrap for getCapabilities(..).""" - return self.tado.getCapabilities(tado_id) - - def get_me(self): - """Wrap for getMe().""" - return self.tado.getMe() + def get_capabilities(self, zone_id): + """Return the capabilities of the devices.""" + return self.tado.getCapabilities(zone_id) def reset_zone_overlay(self, zone_id): - """Wrap for resetZoneOverlay(..).""" + """Reset the zone back to the default operation.""" self.tado.resetZoneOverlay(zone_id) - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + self.update_sensor("zone", zone_id) def set_zone_overlay( self, @@ -133,13 +154,32 @@ class TadoDataStore: device_type="HEATING", mode=None, ): - """Wrap for setZoneOverlay(..).""" - self.tado.setZoneOverlay( - zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + """Set a zone overlay.""" + _LOGGER.debug( + "Set overlay for zone %s: mode=%s, temp=%s, duration=%s, type=%s, mode=%s", + zone_id, + overlay_mode, + temperature, + duration, + device_type, + mode, ) - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + try: + self.tado.setZoneOverlay( + zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + ) + except urllib.error.HTTPError as exc: + _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + self.update_sensor("zone", zone_id) def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" - self.tado.setZoneOverlay(zone_id, overlay_mode, None, None, device_type, "OFF") - self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg + try: + self.tado.setZoneOverlay( + zone_id, overlay_mode, None, None, device_type, "OFF" + ) + except urllib.error.HTTPError as exc: + _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + self.update_sensor("zone", zone_id) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 2baf1f380b5..88433db0991 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,6 +1,5 @@ -"""Support for Tado to create a climate device for each zone.""" +"""Support for Tado thermostats.""" import logging -from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -23,27 +22,20 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS -from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_TADO +from . import CONF_FALLBACK, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import ( + CONST_MODE_OFF, + CONST_MODE_SMART_SCHEDULE, + CONST_OVERLAY_MANUAL, + CONST_OVERLAY_TADO_MODE, + TYPE_AIR_CONDITIONING, +) _LOGGER = logging.getLogger(__name__) -CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Default mytado mode -CONST_MODE_OFF = "OFF" # Switch off heating in a zone - -# When we change the temperature setting, we need an overlay mode -# wait until tado changes the mode automatic -CONST_OVERLAY_TADO_MODE = "TADO_MODE" -# the user has change the temperature or mode manually -CONST_OVERLAY_MANUAL = "MANUAL" -# the temperature will be reset after a timespan -CONST_OVERLAY_TIMER = "TIMER" - -CONST_MODE_FAN_HIGH = "HIGH" -CONST_MODE_FAN_MIDDLE = "MIDDLE" -CONST_MODE_FAN_LOW = "LOW" - FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} HVAC_MAP_TADO_HEAT = { @@ -78,35 +70,29 @@ SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tado climate platform.""" - tado = hass.data[DATA_TADO] + tado = hass.data[DOMAIN] - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return + entities = [] + for zone in tado.zones: + entity = create_climate_entity( + tado, zone["name"], zone["id"], discovery_info[CONF_FALLBACK] + ) + if entity: + entities.append(entity) - climate_devices = [] - for zone in zones: - device = create_climate_device(tado, hass, zone, zone["name"], zone["id"]) - if not device: - continue - climate_devices.append(device) - - if climate_devices: - add_entities(climate_devices, True) + if entities: + add_entities(entities, True) -def create_climate_device(tado, hass, zone, name, zone_id): - """Create a Tado climate device.""" +def create_climate_entity(tado, name: str, zone_id: int, fallback: bool): + """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) + _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) + + zone_type = capabilities["type"] - unit = TEMP_CELSIUS - ac_device = capabilities["type"] == "AIR_CONDITIONING" - hot_water_device = capabilities["type"] == "HOT_WATER" ac_support_heat = False - - if ac_device: + if zone_type == TYPE_AIR_CONDITIONING: # Only use heat if available # (you don't have to setup a heat mode, but cool is required) # Heat is preferred as it generally has a lower minimum temperature @@ -118,67 +104,56 @@ def create_climate_device(tado, hass, zone, name, zone_id): elif "temperatures" in capabilities: temperatures = capabilities["temperatures"] else: - _LOGGER.debug("Received zone %s has no temperature; not adding", name) - return + _LOGGER.debug("Not adding zone %s since it has no temperature", name) + return None min_temp = float(temperatures["celsius"]["min"]) max_temp = float(temperatures["celsius"]["max"]) step = temperatures["celsius"].get("step", PRECISION_TENTHS) - data_id = f"zone {name} {zone_id}" - device = TadoClimate( + entity = TadoClimate( tado, name, zone_id, - data_id, - hass.config.units.temperature(min_temp, unit), - hass.config.units.temperature(max_temp, unit), - step, - ac_device, - hot_water_device, - ac_support_heat, - ) - - tado.add_sensor( - data_id, {"id": zone_id, "zone": zone, "name": name, "climate": device} - ) - - return device - - -class TadoClimate(ClimateDevice): - """Representation of a Tado climate device.""" - - def __init__( - self, - store, - zone_name, - zone_id, - data_id, + zone_type, min_temp, max_temp, step, - ac_device, - hot_water_device, ac_support_heat, - tolerance=0.3, + fallback, + ) + return entity + + +class TadoClimate(ClimateDevice): + """Representation of a Tado climate entity.""" + + def __init__( + self, + tado, + zone_name, + zone_id, + zone_type, + min_temp, + max_temp, + step, + ac_support_heat, + fallback, ): - """Initialize of Tado climate device.""" - self._store = store - self._data_id = data_id + """Initialize of Tado climate entity.""" + self._tado = tado self.zone_name = zone_name self.zone_id = zone_id + self.zone_type = zone_type - self._ac_device = ac_device - self._hot_water_device = hot_water_device + self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._ac_support_heat = ac_support_heat self._cooling = False self._active = False self._device_is_active = False - self._unit = TEMP_CELSIUS self._cur_temp = None self._cur_humidity = None self._is_away = False @@ -186,12 +161,34 @@ class TadoClimate(ClimateDevice): self._max_temp = max_temp self._step = step self._target_temp = None - self._tolerance = tolerance + + if fallback: + _LOGGER.debug("Default overlay is set to TADO MODE") + # Fallback to Smart Schedule at next Schedule switch + self._default_overlay = CONST_OVERLAY_TADO_MODE + else: + _LOGGER.debug("Default overlay is set to MANUAL MODE") + # Don't fallback to Smart Schedule, but keep in manual mode + self._default_overlay = CONST_OVERLAY_MANUAL self._current_fan = CONST_MODE_OFF self._current_operation = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), + async_update_callback, + ) + @property def supported_features(self): """Return the list of supported features.""" @@ -199,18 +196,19 @@ class TadoClimate(ClimateDevice): @property def name(self): - """Return the name of the device.""" + """Return the name of the entity.""" return self.zone_name + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + @property def current_humidity(self): """Return the current humidity.""" return self._cur_humidity - def set_humidity(self, humidity: int) -> None: - """Set new target humidity.""" - pass - @property def current_temperature(self): """Return the sensor temperature.""" @@ -234,9 +232,9 @@ class TadoClimate(ClimateDevice): Need to be a subset of HVAC_MODES. """ - if self._ac_device and self._ac_support_heat: - return SUPPORT_HVAC_HEAT_COOL - if self._ac_device and not self._ac_support_heat: + if self._ac_device: + if self._ac_support_heat: + return SUPPORT_HVAC_HEAT_COOL return SUPPORT_HVAC_COOL return SUPPORT_HVAC_HEAT @@ -248,16 +246,10 @@ class TadoClimate(ClimateDevice): """ if not self._device_is_active: return CURRENT_HVAC_OFF - if self._ac_device and self._ac_support_heat and self._cooling: - if self._active: - return CURRENT_HVAC_COOL - return CURRENT_HVAC_IDLE - if self._ac_device and self._ac_support_heat and not self._cooling: - if self._active: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE - if self._ac_device and not self._ac_support_heat: + if self._ac_device: if self._active: + if self._ac_support_heat and not self._cooling: + return CURRENT_HVAC_HEAT return CURRENT_HVAC_COOL return CURRENT_HVAC_IDLE if self._active: @@ -284,7 +276,7 @@ class TadoClimate(ClimateDevice): @property def preset_mode(self): - """Return the current preset mode, e.g., home, away, temp.""" + """Return the current preset mode (home, away).""" if self._is_away: return PRESET_AWAY return PRESET_HOME @@ -301,7 +293,7 @@ class TadoClimate(ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" - return self._unit + return TEMP_CELSIUS @property def target_temperature_step(self): @@ -313,23 +305,13 @@ class TadoClimate(ClimateDevice): """Return the temperature we try to reach.""" return self._target_temp - @property - def target_temperature_high(self): - """Return the upper bound temperature we try to reach.""" - return None - - @property - def target_temperature_low(self): - """Return the lower bound temperature we try to reach.""" - return None - def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is None: return - self._current_operation = CONST_OVERLAY_TADO_MODE + self._current_operation = self._default_overlay self._overlay_mode = None self._target_temp = temperature self._control_heating() @@ -343,50 +325,51 @@ class TadoClimate(ClimateDevice): elif hvac_mode == HVAC_MODE_AUTO: mode = CONST_MODE_SMART_SCHEDULE elif hvac_mode == HVAC_MODE_HEAT: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay elif hvac_mode == HVAC_MODE_COOL: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay elif hvac_mode == HVAC_MODE_HEAT_COOL: - mode = CONST_OVERLAY_TADO_MODE + mode = self._default_overlay self._current_operation = mode self._overlay_mode = None - if self._target_temp is None and self._ac_device: - self._target_temp = 27 + + # Set a target temperature if we don't have any + # This can happen when we switch from Off to On + if self._target_temp is None: + if self._ac_device: + self._target_temp = self.max_temp + else: + self._target_temp = self.min_temp + self.schedule_update_ha_state() + self._control_heating() @property def min_temp(self): """Return the minimum temperature.""" - return convert_temperature( - self._min_temp, self._unit, self.hass.config.units.temperature_unit - ) + return self._min_temp @property def max_temp(self): """Return the maximum temperature.""" - return convert_temperature( - self._max_temp, self._unit, self.hass.config.units.temperature_unit - ) + return self._max_temp def update(self): - """Update the state of this climate device.""" - self._store.update() - - data = self._store.get_data(self._data_id) - - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) + """Handle update callbacks.""" + _LOGGER.debug("Updating climate platform for zone %d", self.zone_id) + try: + data = self._tado.data["zone"][self.zone_id] + except KeyError: + _LOGGER.debug("No data") return if "sensorDataPoints" in data: sensor_data = data["sensorDataPoints"] - unit = TEMP_CELSIUS - if "insideTemperature" in sensor_data: temperature = float(sensor_data["insideTemperature"]["celsius"]) - self._cur_temp = self.hass.config.units.temperature(temperature, unit) + self._cur_temp = temperature if "humidity" in sensor_data: humidity = float(sensor_data["humidity"]["percentage"]) @@ -398,7 +381,7 @@ class TadoClimate(ClimateDevice): and data["setting"]["temperature"] is not None ): setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = self.hass.config.units.temperature(setting, unit) + self._target_temp = setting if "tadoMode" in data: mode = data["tadoMode"] @@ -468,135 +451,38 @@ class TadoClimate(ClimateDevice): self._current_fan = fan_speed def _control_heating(self): - """Send new target temperature to mytado.""" - if None not in (self._cur_temp, self._target_temp): - _LOGGER.info( - "Obtained current (%d) and target temperature (%d). " - "Tado thermostat active", - self._cur_temp, - self._target_temp, - ) - + """Send new target temperature to Tado.""" if self._current_operation == CONST_MODE_SMART_SCHEDULE: - _LOGGER.info( - "Switching mytado.com to SCHEDULE (default) for zone %s (%d)", + _LOGGER.debug( + "Switching to SMART_SCHEDULE for zone %s (%d)", self.zone_name, self.zone_id, ) - self._store.reset_zone_overlay(self.zone_id) + self._tado.reset_zone_overlay(self.zone_id) self._overlay_mode = self._current_operation return if self._current_operation == CONST_MODE_OFF: - if self._ac_device: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - AIR_CONDITIONING", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off( - self.zone_id, CONST_OVERLAY_MANUAL, "AIR_CONDITIONING" - ) - elif self._hot_water_device: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - HOT_WATER", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off( - self.zone_id, CONST_OVERLAY_MANUAL, "HOT_WATER" - ) - else: - _LOGGER.info( - "Switching mytado.com to OFF for zone %s (%d) - HEATING", - self.zone_name, - self.zone_id, - ) - self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, "HEATING") + _LOGGER.debug( + "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id + ) + self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type) self._overlay_mode = self._current_operation return - if self._ac_device: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - AIR_CONDITIONING", - self._current_operation, - self.zone_name, - self.zone_id, - self._target_temp, - ) - self._store.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - "AIR_CONDITIONING", - "COOL", - ) - elif self._hot_water_device: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HOT_WATER", - self._current_operation, - self.zone_name, - self.zone_id, - self._target_temp, - ) - self._store.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - "HOT_WATER", - ) - else: - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s (%d). Temp (%s) - HEATING", - self._current_operation, - self.zone_name, - self.zone_id, - self._target_temp, - ) - self._store.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - "HEATING", - ) - + _LOGGER.debug( + "Switching to %s for zone %s (%d) with temperature %s °C", + self._current_operation, + self.zone_name, + self.zone_id, + self._target_temp, + ) + self._tado.set_zone_overlay( + self.zone_id, + self._current_operation, + self._target_temp, + None, + self.zone_type, + "COOL" if self._ac_device else None, + ) self._overlay_mode = self._current_operation - - @property - def is_aux_heat(self) -> Optional[bool]: - """Return true if aux heater. - - Requires SUPPORT_AUX_HEAT. - """ - return None - - def turn_aux_heat_on(self) -> None: - """Turn auxiliary heater on.""" - pass - - def turn_aux_heat_off(self) -> None: - """Turn auxiliary heater off.""" - pass - - @property - def swing_mode(self) -> Optional[str]: - """Return the swing setting. - - Requires SUPPORT_SWING_MODE. - """ - return None - - @property - def swing_modes(self) -> Optional[List[str]]: - """Return the list of available swing modes. - - Requires SUPPORT_SWING_MODE. - """ - return None - - def set_swing_mode(self, swing_mode: str) -> None: - """Set new target swing operation.""" - pass diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py new file mode 100644 index 00000000000..3c0232c8ba2 --- /dev/null +++ b/homeassistant/components/tado/const.py @@ -0,0 +1,18 @@ +"""Constant values for the Tado component.""" + +# Configuration +CONF_FALLBACK = "fallback" + +# Types +TYPE_AIR_CONDITIONING = "AIR_CONDITIONING" +TYPE_HEATING = "HEATING" +TYPE_HOT_WATER = "HOT_WATER" + +# Base modes +CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule +CONST_MODE_OFF = "OFF" # Switch off heating in a zone + +# When we change the temperature setting, we need an overlay mode +CONST_OVERLAY_TADO_MODE = "TADO_MODE" # wait until tado changes the mode automatic +CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually +CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py index c63f5061dfa..ea797754da8 100644 --- a/homeassistant/components/tado/device_tracker.py +++ b/homeassistant/components/tado/device_tracker.py @@ -60,9 +60,7 @@ class TadoDeviceScanner(DeviceScanner): if self.home_id is None: self.tadoapiurl = "https://my.tado.com/api/v2/me" else: - self.tadoapiurl = ( - "https://my.tado.com/api/v2" "/homes/{home_id}/mobileDevices" - ) + self.tadoapiurl = "https://my.tado.com/api/v2/homes/{home_id}/mobileDevices" # The API URL always needs a username and password self.tadoapiurl += "?username={username}&password={password}" diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index 4728f1622ed..7539988d42e 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -2,11 +2,7 @@ "domain": "tado", "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", - "requirements": [ - "python-tado==0.2.9" - ], + "requirements": ["python-tado==0.2.9"], "dependencies": [], - "codeowners": [ - "@michaelarnauts" - ] + "codeowners": ["@michaelarnauts"] } diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 346b27bec26..a928b61a508 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -1,130 +1,110 @@ """Support for Tado sensors for each zone.""" import logging -from homeassistant.const import ATTR_ID, ATTR_NAME, TEMP_CELSIUS +from homeassistant.const import TEMP_CELSIUS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from . import DATA_TADO +from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED +from .const import TYPE_AIR_CONDITIONING, TYPE_HEATING, TYPE_HOT_WATER _LOGGER = logging.getLogger(__name__) -ATTR_DATA_ID = "data_id" -ATTR_DEVICE = "device" -ATTR_ZONE = "zone" +ZONE_SENSORS = { + TYPE_HEATING: [ + "temperature", + "humidity", + "power", + "link", + "heating", + "tado mode", + "overlay", + "early start", + "open window", + ], + TYPE_AIR_CONDITIONING: [ + "temperature", + "humidity", + "power", + "link", + "ac", + "tado mode", + "overlay", + ], + TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], +} -CLIMATE_HEAT_SENSOR_TYPES = [ - "temperature", - "humidity", - "power", - "link", - "heating", - "tado mode", - "overlay", - "early start", -] - -CLIMATE_COOL_SENSOR_TYPES = [ - "temperature", - "humidity", - "power", - "link", - "ac", - "tado mode", - "overlay", -] - -HOT_WATER_SENSOR_TYPES = ["power", "link", "tado mode", "overlay"] +DEVICE_SENSORS = ["tado bridge status"] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the sensor platform.""" - tado = hass.data[DATA_TADO] + tado = hass.data[DOMAIN] - try: - zones = tado.get_zones() - except RuntimeError: - _LOGGER.error("Unable to get zone info from mytado") - return - - sensor_items = [] - for zone in zones: - if zone["type"] == "HEATING": - for variable in CLIMATE_HEAT_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - elif zone["type"] == "HOT_WATER": - for variable in HOT_WATER_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - elif zone["type"] == "AIR_CONDITIONING": - for variable in CLIMATE_COOL_SENSOR_TYPES: - sensor_items.append( - create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) - ) - - me_data = tado.get_me() - sensor_items.append( - create_device_sensor( - tado, - me_data, - me_data["homes"][0]["name"], - me_data["homes"][0]["id"], - "tado bridge status", + # Create zone sensors + entities = [] + for zone in tado.zones: + entities.extend( + [ + create_zone_sensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS.get(zone["type"]) + ] ) - ) - if sensor_items: - add_entities(sensor_items, True) + # Create device sensors + for home in tado.devices: + entities.extend( + [ + create_device_sensor(tado, home["name"], home["id"], variable) + for variable in DEVICE_SENSORS + ] + ) + + add_entities(entities, True) -def create_zone_sensor(tado, zone, name, zone_id, variable): +def create_zone_sensor(tado, name, zone_id, variable): """Create a zone sensor.""" - data_id = f"zone {name} {zone_id}" - - tado.add_sensor( - data_id, - {ATTR_ZONE: zone, ATTR_NAME: name, ATTR_ID: zone_id, ATTR_DATA_ID: data_id}, - ) - - return TadoSensor(tado, name, zone_id, variable, data_id) + return TadoSensor(tado, name, "zone", zone_id, variable) -def create_device_sensor(tado, device, name, device_id, variable): +def create_device_sensor(tado, name, device_id, variable): """Create a device sensor.""" - data_id = f"device {name} {device_id}" - - tado.add_sensor( - data_id, - { - ATTR_DEVICE: device, - ATTR_NAME: name, - ATTR_ID: device_id, - ATTR_DATA_ID: data_id, - }, - ) - - return TadoSensor(tado, name, device_id, variable, data_id) + return TadoSensor(tado, name, "device", device_id, variable) class TadoSensor(Entity): """Representation of a tado Sensor.""" - def __init__(self, store, zone_name, zone_id, zone_variable, data_id): + def __init__(self, tado, zone_name, sensor_type, zone_id, zone_variable): """Initialize of the Tado Sensor.""" - self._store = store + self._tado = tado self.zone_name = zone_name self.zone_id = zone_id self.zone_variable = zone_variable + self.sensor_type = sensor_type self._unique_id = f"{zone_variable} {zone_id}" - self._data_id = data_id self._state = None self._state_attributes = None + async def async_added_to_hass(self): + """Register for sensor updates.""" + + @callback + def async_update_callback(): + """Schedule an entity update.""" + self.async_schedule_update_ha_state(True) + + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format(self.sensor_type, self.zone_id), + async_update_callback, + ) + @property def unique_id(self): """Return the unique id.""" @@ -165,14 +145,16 @@ class TadoSensor(Entity): if self.zone_variable == "humidity": return "mdi:water-percent" + @property + def should_poll(self) -> bool: + """Do not poll.""" + return False + def update(self): - """Update method called when should_poll is true.""" - self._store.update() - - data = self._store.get_data(self._data_id) - - if data is None: - _LOGGER.debug("Received no data for zone %s", self.zone_name) + """Handle update callbacks.""" + try: + data = self._tado.data[self.sensor_type][self.zone_id] + except KeyError: return unit = TEMP_CELSIUS @@ -259,3 +241,9 @@ class TadoSensor(Entity): self._state = True else: self._state = False + + elif self.zone_variable == "open window": + if "openWindowDetected" in data: + self._state = data["openWindowDetected"] + else: + self._state = False diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 02cdba5c46a..6bb4fc200af 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -45,7 +45,9 @@ TAHOMA_TYPES = { "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", "io:SomfyBasicContactIOSystemSensor": "sensor", "io:SomfyContactIOSystemSensor": "sensor", + "io:TemperatureIOSystemSensor": "sensor", "io:VerticalExteriorAwningIOComponent": "cover", + "io:VerticalInteriorBlindVeluxIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", "io:GarageOpenerIOComponent": "cover", "io:DiscreteGarageOpenerIOComponent": "cover", @@ -58,6 +60,7 @@ TAHOMA_TYPES = { "rts:ExteriorVenetianBlindRTSComponent": "cover", "rts:GarageDoor4TRTSComponent": "switch", "rts:RollerShutterRTSComponent": "cover", + "rts:OnOffRTSComponent": "switch", "rts:VenetianBlindRTSComponent": "cover", } diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index 6c5dcbd807c..e11c2f4cdf5 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -35,6 +35,7 @@ TAHOMA_DEVICE_CLASSES = { "io:RollerShutterVeluxIOComponent": DEVICE_CLASS_SHUTTER, "io:RollerShutterWithLowSpeedManagementIOComponent": DEVICE_CLASS_SHUTTER, "io:VerticalExteriorAwningIOComponent": DEVICE_CLASS_AWNING, + "io:VerticalInteriorBlindVeluxIOComponent": DEVICE_CLASS_BLIND, "io:WindowOpenerVeluxIOComponent": DEVICE_CLASS_WINDOW, "io:GarageOpenerIOComponent": DEVICE_CLASS_GARAGE, "io:DiscreteGarageOpenerIOComponent": DEVICE_CLASS_GARAGE, @@ -163,10 +164,15 @@ class TahomaCover(TahomaDevice, CoverDevice): def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - if self.tahoma_device.type == HORIZONTAL_AWNING: - self.apply_action("setPosition", kwargs.get(ATTR_POSITION, 0)) + if self.tahoma_device.type == "io:WindowOpenerVeluxIOComponent": + command = "setClosure" else: - self.apply_action("setPosition", 100 - kwargs.get(ATTR_POSITION, 0)) + command = "setPosition" + + if self.tahoma_device.type == HORIZONTAL_AWNING: + self.apply_action(command, kwargs.get(ATTR_POSITION, 0)) + else: + self.apply_action(command, 100 - kwargs.get(ATTR_POSITION, 0)) @property def is_closed(self): @@ -235,6 +241,8 @@ class TahomaCover(TahomaDevice, CoverDevice): HORIZONTAL_AWNING, "io:RollerShutterGenericIOComponent", "io:VerticalExteriorAwningIOComponent", + "io:VerticalInteriorBlindVeluxIOComponent", + "io:WindowOpenerVeluxIOComponent", ): self.apply_action("stop") else: diff --git a/homeassistant/components/tahoma/manifest.json b/homeassistant/components/tahoma/manifest.json index 1e99d4b288d..f01d6740b56 100644 --- a/homeassistant/components/tahoma/manifest.json +++ b/homeassistant/components/tahoma/manifest.json @@ -2,11 +2,7 @@ "domain": "tahoma", "name": "Tahoma", "documentation": "https://www.home-assistant.io/integrations/tahoma", - "requirements": [ - "tahoma-api==0.0.14" - ], + "requirements": ["tahoma-api==0.0.16"], "dependencies": [], - "codeowners": [ - "@philklei" - ] + "codeowners": ["@philklei"] } diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 5279b160d9c..85ccb55761d 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -2,7 +2,7 @@ from datetime import timedelta import logging -from homeassistant.const import ATTR_BATTERY_LEVEL +from homeassistant.const import ATTR_BATTERY_LEVEL, TEMP_CELSIUS from homeassistant.helpers.entity import Entity from . import DOMAIN as TAHOMA_DOMAIN, TahomaDevice @@ -40,8 +40,8 @@ class TahomaSensor(TahomaDevice, Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - if self.tahoma_device.type == "Temperature Sensor": - return None + if self.tahoma_device.type == "io:TemperatureIOSystemSensor": + return TEMP_CELSIUS if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": return None if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": @@ -79,6 +79,11 @@ class TahomaSensor(TahomaDevice, Entity): if self.tahoma_device.type == "rtds:RTDSMotionSensor": self.current_value = self.tahoma_device.active_states["core:OccupancyState"] self._available = True + if self.tahoma_device.type == "io:TemperatureIOSystemSensor": + self.current_value = round( + float(self.tahoma_device.active_states["core:TemperatureState"]), 1 + ) + self._available = True _LOGGER.debug("Update %s, value: %d", self._name, self.current_value) diff --git a/homeassistant/components/tahoma/switch.py b/homeassistant/components/tahoma/switch.py index a0a95ab47ce..1612120f313 100644 --- a/homeassistant/components/tahoma/switch.py +++ b/homeassistant/components/tahoma/switch.py @@ -45,9 +45,14 @@ class TahomaSwitch(TahomaDevice, SwitchDevice): else: self._state = STATE_OFF - self._available = bool( - self.tahoma_device.active_states.get("core:StatusState") == "available" - ) + # A RTS power socket doesn't have a feedback channel, + # so we must assume the socket is available. + if self.tahoma_device.type == "rts:OnOffRTSComponent": + self._available = True + else: + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) _LOGGER.debug("Update %s, state: %s", self._name, self._state) diff --git a/homeassistant/components/tank_utility/manifest.json b/homeassistant/components/tank_utility/manifest.json index 328285ab67b..68d487ce5c5 100644 --- a/homeassistant/components/tank_utility/manifest.json +++ b/homeassistant/components/tank_utility/manifest.json @@ -1,10 +1,8 @@ { "domain": "tank_utility", - "name": "Tank utility", + "name": "Tank Utility", "documentation": "https://www.home-assistant.io/integrations/tank_utility", - "requirements": [ - "tank_utility==1.4.0" - ], + "requirements": ["tank_utility==1.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tank_utility/sensor.py b/homeassistant/components/tank_utility/sensor.py index 4dcc880703c..61a3d7367bf 100644 --- a/homeassistant/components/tank_utility/sensor.py +++ b/homeassistant/components/tank_utility/sensor.py @@ -73,7 +73,7 @@ class TankUtilitySensor(Entity): self._token = token self._device = device self._state = None - self._name = "Tank Utility " + self.device + self._name = f"Tank Utility {self.device}" self._unit_of_measurement = SENSOR_UNIT_OF_MEASUREMENT self._attributes = {} @@ -116,6 +116,8 @@ class TankUtilitySensor(Entity): if ( http_error.response.status_code == requests.codes.unauthorized # pylint: disable=no-member + or http_error.response.status_code + == requests.codes.bad_request # pylint: disable=no-member ): _LOGGER.info("Getting new token") self._token = auth.get_token(self._email, self._password, force=True) diff --git a/homeassistant/components/tapsaff/manifest.json b/homeassistant/components/tapsaff/manifest.json index 79fff1b2b4c..bfdb1adda79 100644 --- a/homeassistant/components/tapsaff/manifest.json +++ b/homeassistant/components/tapsaff/manifest.json @@ -1,10 +1,8 @@ { "domain": "tapsaff", - "name": "Tapsaff", + "name": "Taps Aff", "documentation": "https://www.home-assistant.io/integrations/tapsaff", - "requirements": [ - "tapsaff==0.2.1" - ], + "requirements": ["tapsaff==0.2.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tautulli/manifest.json b/homeassistant/components/tautulli/manifest.json index cc635082c35..338943a3e6c 100644 --- a/homeassistant/components/tautulli/manifest.json +++ b/homeassistant/components/tautulli/manifest.json @@ -2,11 +2,7 @@ "domain": "tautulli", "name": "Tautulli", "documentation": "https://www.home-assistant.io/integrations/tautulli", - "requirements": [ - "pytautulli==0.5.0" - ], + "requirements": ["pytautulli==0.5.0"], "dependencies": [], - "codeowners": [ - "@ludeeus" - ] + "codeowners": ["@ludeeus"] } diff --git a/homeassistant/components/tcp/manifest.json b/homeassistant/components/tcp/manifest.json index 53f3f6c6463..fea16a087c8 100644 --- a/homeassistant/components/tcp/manifest.json +++ b/homeassistant/components/tcp/manifest.json @@ -1,6 +1,6 @@ { "domain": "tcp", - "name": "Tcp", + "name": "TCP", "documentation": "https://www.home-assistant.io/integrations/tcp", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/tcp/sensor.py b/homeassistant/components/tcp/sensor.py index a387b3fc0bb..2732f2d6bd1 100644 --- a/homeassistant/components/tcp/sensor.py +++ b/homeassistant/components/tcp/sensor.py @@ -1,23 +1,23 @@ """Support for TCP socket based sensors.""" import logging -import socket import select +import socket import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, + CONF_NAME, CONF_PAYLOAD, + CONF_PORT, CONF_TIMEOUT, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, ) from homeassistant.exceptions import TemplateError -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ted5000/manifest.json b/homeassistant/components/ted5000/manifest.json index b02a9db3fdc..820ee348b3b 100644 --- a/homeassistant/components/ted5000/manifest.json +++ b/homeassistant/components/ted5000/manifest.json @@ -1,10 +1,8 @@ { "domain": "ted5000", - "name": "Ted5000", + "name": "The Energy Detective TED5000", "documentation": "https://www.home-assistant.io/integrations/ted5000", - "requirements": [ - "xmltodict==0.12.0" - ], + "requirements": ["xmltodict==0.12.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/teksavvy/manifest.json b/homeassistant/components/teksavvy/manifest.json index 220a086e0be..9de98dcffb6 100644 --- a/homeassistant/components/teksavvy/manifest.json +++ b/homeassistant/components/teksavvy/manifest.json @@ -1,6 +1,6 @@ { "domain": "teksavvy", - "name": "Teksavvy", + "name": "TekSavvy", "documentation": "https://www.home-assistant.io/integrations/teksavvy", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/teksavvy/sensor.py b/homeassistant/components/teksavvy/sensor.py index dc8b16b8ce1..fe183129eaa 100644 --- a/homeassistant/components/teksavvy/sensor.py +++ b/homeassistant/components/teksavvy/sensor.py @@ -1,8 +1,8 @@ """Support for TekSavvy Bandwidth Monitor.""" from datetime import timedelta import logging -import async_timeout +import async_timeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/telegram/manifest.json b/homeassistant/components/telegram/manifest.json index 392c45ea886..55700521cd5 100644 --- a/homeassistant/components/telegram/manifest.json +++ b/homeassistant/components/telegram/manifest.json @@ -3,8 +3,6 @@ "name": "Telegram", "documentation": "https://www.home-assistant.io/integrations/telegram", "requirements": [], - "dependencies": [ - "telegram_bot" - ], + "dependencies": ["telegram_bot"], "codeowners": [] } diff --git a/homeassistant/components/telegram/notify.py b/homeassistant/components/telegram/notify.py index 23c36e3bafa..ceb660d9e1d 100644 --- a/homeassistant/components/telegram/notify.py +++ b/homeassistant/components/telegram/notify.py @@ -3,8 +3,6 @@ import logging import voluptuous as vol -from homeassistant.const import ATTR_LOCATION - from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, @@ -13,6 +11,7 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import ATTR_LOCATION _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index d365060e204..9b56201f8c7 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -1,8 +1,8 @@ """Support to send and receive Telegram messages.""" -import io -from ipaddress import ip_network from functools import partial import importlib +import io +from ipaddress import ip_network import logging import requests @@ -19,7 +19,6 @@ from telegram.parsemode import ParseMode from telegram.utils.request import Request import voluptuous as vol -from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE from homeassistant.const import ( ATTR_COMMAND, ATTR_LATITUDE, @@ -27,14 +26,18 @@ from homeassistant.const import ( CONF_API_KEY, CONF_PLATFORM, CONF_TIMEOUT, - HTTP_DIGEST_AUTHENTICATION, CONF_URL, + HTTP_DIGEST_AUTHENTICATION, ) -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) +ATTR_DATA = "data" +ATTR_MESSAGE = "message" +ATTR_TITLE = "title" + ATTR_ARGS = "args" ATTR_AUTHENTICATION = "authentication" ATTR_CALLBACK_QUERY = "callback_query" @@ -625,7 +628,7 @@ class TelegramNotificationService: """Answer a callback originated with a press in an inline keyboard.""" params = self._get_msg_kwargs(kwargs) _LOGGER.debug( - "Answer callback query with callback ID %s: %s, " "alert: %s.", + "Answer callback query with callback ID %s: %s, alert: %s.", callback_query_id, message, show_alert, @@ -779,7 +782,13 @@ class BaseTelegramBotEntity: if event_data is None: return message_ok - event_data[ATTR_DATA] = data[ATTR_DATA] + query_data = event_data[ATTR_DATA] = data[ATTR_DATA] + + if query_data[0] == "/": + pieces = query_data.split(" ") + event_data[ATTR_COMMAND] = pieces[0] + event_data[ATTR_ARGS] = pieces[1:] + event_data[ATTR_MSG] = data[ATTR_MSG] event_data[ATTR_CHAT_INSTANCE] = data[ATTR_CHAT_INSTANCE] event_data[ATTR_MSGID] = data[ATTR_MSGID] diff --git a/homeassistant/components/telegram_bot/manifest.json b/homeassistant/components/telegram_bot/manifest.json index 7fb648e5cb5..29f6ade8af4 100644 --- a/homeassistant/components/telegram_bot/manifest.json +++ b/homeassistant/components/telegram_bot/manifest.json @@ -2,10 +2,7 @@ "domain": "telegram_bot", "name": "Telegram bot", "documentation": "https://www.home-assistant.io/integrations/telegram_bot", - "requirements": [ - "python-telegram-bot==11.1.0", - "PySocks==1.7.1" - ], + "requirements": ["python-telegram-bot==11.1.0", "PySocks==1.7.1"], "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index cf3d13d5edc..8bdeef25118 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -2,8 +2,8 @@ import logging from telegram import Update -from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter -from telegram.ext import Updater, Handler +from telegram.error import NetworkError, RetryAfter, TelegramError, TimedOut +from telegram.ext import Handler, Updater from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback diff --git a/homeassistant/components/tellduslive/.translations/ca.json b/homeassistant/components/tellduslive/.translations/ca.json index fafa8798401..a337474c96b 100644 --- a/homeassistant/components/tellduslive/.translations/ca.json +++ b/homeassistant/components/tellduslive/.translations/ca.json @@ -18,6 +18,7 @@ "data": { "host": "Amfitri\u00f3" }, + "description": "buit", "title": "Selecci\u00f3 extrem" } }, diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 41dc39146e8..fa5b7e2d319 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -18,6 +18,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, + "description": "\u043f\u0443\u0441\u0442\u043e", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443." } }, diff --git a/homeassistant/components/tellduslive/__init__.py b/homeassistant/components/tellduslive/__init__.py index 313699e6f1c..917b927691e 100644 --- a/homeassistant/components/tellduslive/__init__.py +++ b/homeassistant/components/tellduslive/__init__.py @@ -1,15 +1,17 @@ """Support for Telldus Live.""" import asyncio -import logging from functools import partial +import logging +from tellduslive import DIM, TURNON, UP, Session import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later + from . import config_flow # noqa: F401 from .const import ( CONF_HOST, @@ -51,7 +53,6 @@ INTERVAL_TRACKER = f"{DOMAIN}_INTERVAL" async def async_setup_entry(hass, entry): """Create a tellduslive session.""" - from tellduslive import Session conf = entry.data[KEY_SESSION] @@ -159,7 +160,6 @@ class TelldusLiveClient: """Find out what type of HA component to create.""" if device.is_sensor: return "sensor" - from tellduslive import DIM, UP, TURNON if device.methods & DIM: return "light" diff --git a/homeassistant/components/tellduslive/config_flow.py b/homeassistant/components/tellduslive/config_flow.py index 19f82dd18f4..893f3b80456 100644 --- a/homeassistant/components/tellduslive/config_flow.py +++ b/homeassistant/components/tellduslive/config_flow.py @@ -4,6 +4,7 @@ import logging import os import async_timeout +from tellduslive import Session, supports_local_api import voluptuous as vol from homeassistant import config_entries @@ -43,7 +44,6 @@ class FlowHandler(config_entries.ConfigFlow): self._scan_interval = SCAN_INTERVAL def _get_auth_url(self): - from tellduslive import Session self._session = Session( public_key=PUBLIC_KEY, @@ -116,7 +116,6 @@ class FlowHandler(config_entries.ConfigFlow): async def async_step_discovery(self, user_input): """Run when a Tellstick is discovered.""" - from tellduslive import supports_local_api _LOGGER.info("Discovered tellstick device: %s", user_input) if supports_local_api(user_input[1]): diff --git a/homeassistant/components/tellduslive/entry.py b/homeassistant/components/tellduslive/entry.py index ecd428d3b15..50a219bf7a1 100644 --- a/homeassistant/components/tellduslive/entry.py +++ b/homeassistant/components/tellduslive/entry.py @@ -2,6 +2,8 @@ from datetime import datetime import logging +from tellduslive import BATTERY_LOW, BATTERY_OK, BATTERY_UNKNOWN + from homeassistant.const import ATTR_BATTERY_LEVEL, DEVICE_DEFAULT_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -91,7 +93,6 @@ class TelldusLiveEntity(Entity): @property def _battery_level(self): """Return the battery level of a device.""" - from tellduslive import BATTERY_LOW, BATTERY_UNKNOWN, BATTERY_OK if self.device.battery == BATTERY_LOW: return 1 diff --git a/homeassistant/components/tellduslive/manifest.json b/homeassistant/components/tellduslive/manifest.json index 777138d44ab..fda47109146 100644 --- a/homeassistant/components/tellduslive/manifest.json +++ b/homeassistant/components/tellduslive/manifest.json @@ -1,13 +1,10 @@ { "domain": "tellduslive", - "name": "Tellduslive", + "name": "Telldus Live", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tellduslive", - "requirements": [ - "tellduslive==0.10.10" - ], + "requirements": ["tellduslive==0.10.10"], "dependencies": [], - "codeowners": [ - "@fredrike" - ] + "codeowners": ["@fredrike"], + "quality_scale": "gold" } diff --git a/homeassistant/components/tellstick/manifest.json b/homeassistant/components/tellstick/manifest.json index 3391533b081..189a4e12c4a 100644 --- a/homeassistant/components/tellstick/manifest.json +++ b/homeassistant/components/tellstick/manifest.json @@ -1,11 +1,8 @@ { "domain": "tellstick", - "name": "Tellstick", + "name": "TellStick", "documentation": "https://www.home-assistant.io/integrations/tellstick", - "requirements": [ - "tellcore-net==0.4", - "tellcore-py==1.1.2" - ], + "requirements": ["tellcore-net==0.4", "tellcore-py==1.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/telnet/switch.py b/homeassistant/components/telnet/switch.py index 87fb70bb888..a99fe044c46 100644 --- a/homeassistant/components/telnet/switch.py +++ b/homeassistant/components/telnet/switch.py @@ -16,9 +16,9 @@ from homeassistant.const import ( CONF_COMMAND_STATE, CONF_NAME, CONF_PORT, - CONF_TIMEOUT, CONF_RESOURCE, CONF_SWITCHES, + CONF_TIMEOUT, CONF_VALUE_TEMPLATE, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/temper/manifest.json b/homeassistant/components/temper/manifest.json index 76656e9936f..4879ecddc16 100644 --- a/homeassistant/components/temper/manifest.json +++ b/homeassistant/components/temper/manifest.json @@ -1,10 +1,8 @@ { "domain": "temper", - "name": "Temper", + "name": "TEMPer", "documentation": "https://www.home-assistant.io/integrations/temper", - "requirements": [ - "temperusb==1.5.3" - ], + "requirements": ["temperusb==1.5.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/temper/sensor.py b/homeassistant/components/temper/sensor.py index 8b782ae4d79..fd26b1702dc 100644 --- a/homeassistant/components/temper/sensor.py +++ b/homeassistant/components/temper/sensor.py @@ -39,7 +39,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for idx, dev in enumerate(temper_devices): if idx != 0: - name = name + "_" + str(idx) + name = f"{name}_{idx!s}" TEMPER_SENSORS.append(TemperSensor(dev, temp_unit, name, scaling)) add_entities(TEMPER_SENSORS) diff --git a/homeassistant/components/template/__init__.py b/homeassistant/components/template/__init__.py index 80421b8e3f8..f100d663d8c 100644 --- a/homeassistant/components/template/__init__.py +++ b/homeassistant/components/template/__init__.py @@ -1,11 +1,10 @@ """The template component.""" +from itertools import chain import logging -from itertools import chain from homeassistant.const import MATCH_ALL - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 116862abc79..7de43ea0702 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -3,28 +3,29 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.binary_sensor import ( - BinarySensorDevice, + DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, - CONF_SENSORS, + ATTR_FRIENDLY_NAME, CONF_DEVICE_CLASS, + CONF_ENTITY_PICTURE_TEMPLATE, + CONF_ICON_TEMPLATE, + CONF_SENSORS, + CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, MATCH_ALL, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id -from homeassistant.helpers.event import async_track_state_change, async_track_same_state +from homeassistant.helpers.event import async_track_same_state, async_track_state_change + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE @@ -222,7 +223,7 @@ class BinarySensorTemplate(BinarySensorDevice): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render template %s, " "the state is unknown", self._name + "Could not render template %s, the state is unknown", self._name ) return _LOGGER.error("Could not render template %s: %s", self._name, ex) @@ -258,7 +259,7 @@ class BinarySensorTemplate(BinarySensorDevice): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index 22035af24ec..f6678067d70 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -3,40 +3,41 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.cover import ( - ENTITY_ID_FORMAT, - CoverDevice, - PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, - SUPPORT_OPEN_TILT, - SUPPORT_CLOSE_TILT, - SUPPORT_STOP_TILT, - SUPPORT_SET_TILT_POSITION, - SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_STOP, - SUPPORT_SET_POSITION, ATTR_POSITION, ATTR_TILT_POSITION, + DEVICE_CLASSES_SCHEMA, + ENTITY_ID_FORMAT, + PLATFORM_SCHEMA, + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverDevice, ) from homeassistant.const import ( - CONF_FRIENDLY_NAME, - CONF_ENTITY_ID, - EVENT_HOMEASSISTANT_START, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, CONF_DEVICE_CLASS, + CONF_ENTITY_ID, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME, + CONF_ICON_TEMPLATE, CONF_OPTIMISTIC, - STATE_OPEN, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, STATE_CLOSED, + STATE_OPEN, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE @@ -436,8 +437,7 @@ class CoverTemplate(CoverDevice): if state < 0 or state > 100: self._tilt_value = None _LOGGER.error( - "Tilt value must be between 0 and 100." " Value was: %.2f", - state, + "Tilt value must be between 0 and 100. Value was: %.2f", state, ) else: self._tilt_value = state @@ -465,7 +465,7 @@ class CoverTemplate(CoverDevice): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index ebb9bcc8b14..89f54444376 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -3,35 +3,36 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( - SPEED_LOW, - SPEED_MEDIUM, - SPEED_HIGH, - SUPPORT_SET_SPEED, - SUPPORT_OSCILLATE, - FanEntity, - ATTR_SPEED, + ATTR_DIRECTION, ATTR_OSCILLATING, - ENTITY_ID_FORMAT, - SUPPORT_DIRECTION, + ATTR_SPEED, DIRECTION_FORWARD, DIRECTION_REVERSE, - ATTR_DIRECTION, + ENTITY_ID_FORMAT, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SUPPORT_DIRECTION, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, + FanEntity, ) from homeassistant.const import ( + CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, - CONF_ENTITY_ID, - STATE_ON, - STATE_OFF, EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/light.py b/homeassistant/components/template/light.py index b71aadd0155..70c097d0b2b 100644 --- a/homeassistant/components/template/light.py +++ b/homeassistant/components/template/light.py @@ -3,30 +3,31 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, ENTITY_ID_FORMAT, - Light, SUPPORT_BRIGHTNESS, + Light, ) from homeassistant.const import ( - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, CONF_ENTITY_ID, + CONF_ENTITY_PICTURE_TEMPLATE, CONF_FRIENDLY_NAME, - STATE_ON, - STATE_OFF, - EVENT_HOMEASSISTANT_START, + CONF_ICON_TEMPLATE, CONF_LIGHTS, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, + STATE_OFF, + STATE_ON, ) -from homeassistant.helpers.config_validation import PLATFORM_SCHEMA +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE @@ -266,38 +267,10 @@ class LightTemplate(Light): self.async_schedule_update_ha_state() async def async_update(self): - """Update the state from the template.""" - if self._template is not None: - try: - state = self._template.async_render().lower() - except TemplateError as ex: - _LOGGER.error(ex) - self._state = None + """Update from templates.""" + self.update_state() - if state in _VALID_STATES: - self._state = state in ("true", STATE_ON) - else: - _LOGGER.error( - "Received invalid light is_on state: %s. Expected: %s", - state, - ", ".join(_VALID_STATES), - ) - self._state = None - - if self._level_template is not None: - try: - brightness = self._level_template.async_render() - except TemplateError as ex: - _LOGGER.error(ex) - self._state = None - - if 0 <= int(brightness) <= 255: - self._brightness = int(brightness) - else: - _LOGGER.error( - "Received invalid brightness : %s. Expected: 0-255", brightness - ) - self._brightness = None + self.update_brightness() for property_name, template in ( ("_icon", self._icon_template), @@ -319,7 +292,7 @@ class LightTemplate(Light): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) @@ -334,3 +307,39 @@ class LightTemplate(Light): self._name, ex, ) + + @callback + def update_brightness(self): + """Update the brightness from the template.""" + if self._level_template is not None: + try: + brightness = self._level_template.async_render() + if 0 <= int(brightness) <= 255: + self._brightness = int(brightness) + else: + _LOGGER.error( + "Received invalid brightness : %s. Expected: 0-255", brightness + ) + self._brightness = None + except TemplateError as ex: + _LOGGER.error(ex) + self._state = None + + @callback + def update_state(self): + """Update the state from the template.""" + if self._template is not None: + try: + state = self._template.async_render().lower() + if state in _VALID_STATES: + self._state = state in ("true", STATE_ON) + else: + _LOGGER.error( + "Received invalid light is_on state: %s. Expected: %s", + state, + ", ".join(_VALID_STATES), + ) + self._state = None + except TemplateError as ex: + _LOGGER.error(ex) + self._state = None diff --git a/homeassistant/components/template/lock.py b/homeassistant/components/template/lock.py index 71e9cc6642d..f4a6b55dd18 100644 --- a/homeassistant/components/template/lock.py +++ b/homeassistant/components/template/lock.py @@ -3,22 +3,22 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.core import callback -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_START, - STATE_ON, - STATE_LOCKED, MATCH_ALL, + STATE_LOCKED, + STATE_ON, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/template/manifest.json b/homeassistant/components/template/manifest.json index 20a35f1afe7..8dfe3441edd 100644 --- a/homeassistant/components/template/manifest.json +++ b/homeassistant/components/template/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/template", "requirements": [], "dependencies": [], - "codeowners": [ - "@PhracturedBlue" - ] + "codeowners": ["@PhracturedBlue", "@tetienne"], + "quality_scale": "internal" } diff --git a/homeassistant/components/template/sensor.py b/homeassistant/components/template/sensor.py index 4ea7daa54f6..0ca5571515a 100644 --- a/homeassistant/components/template/sensor.py +++ b/homeassistant/components/template/sensor.py @@ -4,30 +4,30 @@ from typing import Optional import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.sensor import ( + DEVICE_CLASSES_SCHEMA, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, - DEVICE_CLASSES_SCHEMA, ) from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, - CONF_ENTITY_PICTURE_TEMPLATE, - ATTR_ENTITY_ID, - CONF_SENSORS, - EVENT_HOMEASSISTANT_START, - CONF_FRIENDLY_NAME_TEMPLATE, - MATCH_ALL, CONF_DEVICE_CLASS, + CONF_ENTITY_PICTURE_TEMPLATE, + CONF_FRIENDLY_NAME_TEMPLATE, + CONF_ICON_TEMPLATE, + CONF_SENSORS, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, + MATCH_ALL, ) - +from homeassistant.core import callback from homeassistant.exceptions import TemplateError 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 + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE @@ -230,7 +230,7 @@ class SensorTemplate(Entity): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render template %s," " the state is unknown.", self._name + "Could not render template %s, the state is unknown.", self._name ) else: self._state = None @@ -268,7 +268,7 @@ class SensorTemplate(Entity): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/switch.py b/homeassistant/components/template/switch.py index e06ca0c8d54..c2d8e8158c1 100644 --- a/homeassistant/components/template/switch.py +++ b/homeassistant/components/template/switch.py @@ -3,28 +3,29 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.switch import ( ENTITY_ID_FORMAT, - SwitchDevice, PLATFORM_SCHEMA, + SwitchDevice, ) from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, - CONF_VALUE_TEMPLATE, - CONF_ICON_TEMPLATE, CONF_ENTITY_PICTURE_TEMPLATE, + CONF_ICON_TEMPLATE, + CONF_SWITCHES, + CONF_VALUE_TEMPLATE, + EVENT_HOMEASSISTANT_START, STATE_OFF, STATE_ON, - ATTR_ENTITY_ID, - CONF_SWITCHES, - EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE @@ -232,7 +233,7 @@ class SwitchTemplate(SwitchDevice): ): # Common during HA startup - so just a warning _LOGGER.warning( - "Could not render %s template %s," " the state is unknown.", + "Could not render %s template %s, the state is unknown.", friendly_property_name, self._name, ) diff --git a/homeassistant/components/template/vacuum.py b/homeassistant/components/template/vacuum.py index 8201842e131..4946d54edc3 100644 --- a/homeassistant/components/template/vacuum.py +++ b/homeassistant/components/template/vacuum.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, DOMAIN, @@ -14,35 +13,37 @@ from homeassistant.components.vacuum import ( SERVICE_SET_FAN_SPEED, SERVICE_START, SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_ERROR, + STATE_IDLE, + STATE_PAUSED, + STATE_RETURNING, SUPPORT_BATTERY, SUPPORT_CLEAN_SPOT, SUPPORT_FAN_SPEED, SUPPORT_LOCATE, SUPPORT_PAUSE, SUPPORT_RETURN_HOME, - SUPPORT_STOP, - SUPPORT_STATE, SUPPORT_START, + SUPPORT_STATE, + SUPPORT_STOP, StateVacuumDevice, - STATE_CLEANING, - STATE_DOCKED, - STATE_PAUSED, - STATE_IDLE, - STATE_RETURNING, - STATE_ERROR, ) from homeassistant.const import ( + CONF_ENTITY_ID, CONF_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, - CONF_ENTITY_ID, - MATCH_ALL, EVENT_HOMEASSISTANT_START, + MATCH_ALL, STATE_UNKNOWN, ) from homeassistant.core import callback from homeassistant.exceptions import TemplateError +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.script import Script + from . import extract_entities, initialise_templates from .const import CONF_AVAILABILITY_TEMPLATE diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 627fa8d6bd6..e34e9644381 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -1,6 +1,6 @@ { "domain": "tensorflow", - "name": "Tensorflow", + "name": "TensorFlow", "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", diff --git a/homeassistant/components/tesla/.translations/ca.json b/homeassistant/components/tesla/.translations/ca.json new file mode 100644 index 00000000000..cb4840dea7a --- /dev/null +++ b/homeassistant/components/tesla/.translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error de connexi\u00f3; comprova la xarxa i torna-ho a intentar", + "identifier_exists": "Correu electr\u00f2nic ja registrat", + "invalid_credentials": "Credencials inv\u00e0lides", + "unknown_error": "Error desconegut, si us plau, envia la informaci\u00f3 del registre" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "description": "Introdueix la teva informaci\u00f3.", + "title": "Configuraci\u00f3 de Tesla" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segons entre escanejos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/da.json b/homeassistant/components/tesla/.translations/da.json new file mode 100644 index 00000000000..85091c350d8 --- /dev/null +++ b/homeassistant/components/tesla/.translations/da.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fejl ved tilslutning; tjek netv\u00e6rk og pr\u00f8v igen", + "identifier_exists": "Email er allerede registreret", + "invalid_credentials": "Ugyldige legitimationsoplysninger", + "unknown_error": "Ukendt fejl, rapporter venligst loginfo" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email-adresse" + }, + "description": "Indtast dine oplysninger.", + "title": "Tesla - Konfiguration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellem scanninger" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/de.json b/homeassistant/components/tesla/.translations/de.json new file mode 100644 index 00000000000..c2f6ba38eb9 --- /dev/null +++ b/homeassistant/components/tesla/.translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fehler beim Verbinden; \u00dcberpr\u00fcfen Sie Ihr Netzwerk und versuchen Sie es erneut", + "identifier_exists": "E-Mail bereits registriert", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", + "unknown_error": "Unbekannter Fehler, bitte Log-Info melden" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail-Adresse" + }, + "description": "Bitte geben Sie Ihre Daten ein.", + "title": "Tesla - Konfiguration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunden zwischen den Scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/en.json b/homeassistant/components/tesla/.translations/en.json new file mode 100644 index 00000000000..8c43f28e04e --- /dev/null +++ b/homeassistant/components/tesla/.translations/en.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/es.json b/homeassistant/components/tesla/.translations/es.json new file mode 100644 index 00000000000..64bab24ee3f --- /dev/null +++ b/homeassistant/components/tesla/.translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error de conexi\u00f3n; compruebe la red y vuelva a intentarlo", + "identifier_exists": "Correo electr\u00f3nico ya registrado", + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unknown_error": "Error desconocido, por favor reporte la informaci\u00f3n de registro" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "description": "Por favor, introduzca su informaci\u00f3n.", + "title": "Tesla - Configuraci\u00f3n" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Segundos entre escaneos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/fr.json b/homeassistant/components/tesla/.translations/fr.json new file mode 100644 index 00000000000..69742d3370c --- /dev/null +++ b/homeassistant/components/tesla/.translations/fr.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Erreur de connexion; v\u00e9rifier le r\u00e9seau et r\u00e9essayer", + "identifier_exists": "Email d\u00e9j\u00e0 enregistr\u00e9", + "invalid_credentials": "Informations d'identification invalides", + "unknown_error": "Erreur inconnue, veuillez signaler les informations du journal" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + }, + "description": "Veuillez saisir vos informations.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondes entre les scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/hu.json b/homeassistant/components/tesla/.translations/hu.json new file mode 100644 index 00000000000..01090bbfa9e --- /dev/null +++ b/homeassistant/components/tesla/.translations/hu.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Hiba a csatlakoz\u00e1skor; ellen\u0151rizd a h\u00e1l\u00f3zatot \u00e9s pr\u00f3b\u00e1ld \u00fajra", + "identifier_exists": "Az e-mail c\u00edm m\u00e1r regisztr\u00e1lva van", + "invalid_credentials": "\u00c9rv\u00e9nytelen hiteles\u00edt\u0151 adatok", + "unknown_error": "Ismeretlen hiba, k\u00e9rlek jelentsd a napl\u00f3f\u00e1jlban l\u00e9v\u0151 adatokat" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "Email c\u00edm" + }, + "description": "K\u00e9rlek, add meg az adataidat.", + "title": "Tesla - Konfigur\u00e1ci\u00f3" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Szkennel\u00e9sek k\u00f6z\u00f6tti m\u00e1sodpercek" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/it.json b/homeassistant/components/tesla/.translations/it.json new file mode 100644 index 00000000000..0e254cf2843 --- /dev/null +++ b/homeassistant/components/tesla/.translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Errore durante la connessione; controllare la rete e riprovare", + "identifier_exists": "E-mail gi\u00e0 registrata", + "invalid_credentials": "Credenziali non valide", + "unknown_error": "Errore sconosciuto, si prega di segnalare le informazioni del registro" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Indirizzo E-Mail" + }, + "description": "Si prega di inserire le tue informazioni.", + "title": "Tesla - Configurazione" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Secondi tra le scansioni" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/ko.json b/homeassistant/components/tesla/.translations/ko.json new file mode 100644 index 00000000000..8b7dc9ce93c --- /dev/null +++ b/homeassistant/components/tesla/.translations/ko.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\uc5f0\uacb0 \uc624\ub958; \ub124\ud2b8\uc6cc\ud06c\ub97c \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "identifier_exists": "\uc774\uba54\uc77c \uc8fc\uc18c\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_credentials": "\uc774\uba54\uc77c \uc8fc\uc18c \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub85c\uadf8 \ub0b4\uc6a9\uc744 \uc54c\ub824\uc8fc\uc138\uc694" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "description": "\uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Tesla - \uad6c\uc131" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ucd08)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/lb.json b/homeassistant/components/tesla/.translations/lb.json new file mode 100644 index 00000000000..fa63c5a289a --- /dev/null +++ b/homeassistant/components/tesla/.translations/lb.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Feeler beim verbannen, Iwwerpr\u00e9ift Netzwierk a prob\u00e9iert nach emol", + "identifier_exists": "E-Mail ass scho registr\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune", + "unknown_error": "Onbekannte Feeler, mellt w.e.g. Logbuch Info" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adress" + }, + "description": "F\u00ebllt \u00e4r Informatiounen aus.", + "title": "Tesla - Konfiguratioun" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekonnen t\u00ebscht Scannen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/nl.json b/homeassistant/components/tesla/.translations/nl.json new file mode 100644 index 00000000000..5f3e83dd248 --- /dev/null +++ b/homeassistant/components/tesla/.translations/nl.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Fout bij verbinden; controleer het netwerk en probeer het opnieuw", + "identifier_exists": "E-mail al geregistreerd", + "invalid_credentials": "Ongeldige inloggegevens", + "unknown_error": "Onbekende fout, meldt u log info" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "description": "Vul alstublieft uw gegevens in.", + "title": "Tesla - Configuratie" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconden tussen scans" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/no.json b/homeassistant/components/tesla/.translations/no.json new file mode 100644 index 00000000000..0d73908f417 --- /dev/null +++ b/homeassistant/components/tesla/.translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Feil ved tilkobling; sjekk nettverket og pr\u00f8v p\u00e5 nytt", + "identifier_exists": "E-post er allerede registrert", + "invalid_credentials": "Ugyldig brukerinformasjon", + "unknown_error": "Ukjent feil, Vennligst rapporter informasjon fra Loggen" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-postadresse" + }, + "description": "Vennligst skriv inn informasjonen din.", + "title": "Tesla - Konfigurasjon" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekunder mellom skanninger" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/pl.json b/homeassistant/components/tesla/.translations/pl.json new file mode 100644 index 00000000000..5a8a3d2ebd3 --- /dev/null +++ b/homeassistant/components/tesla/.translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "B\u0142\u0105d po\u0142\u0105czenia; sprawd\u017a sie\u0107 i spr\u00f3buj ponownie", + "identifier_exists": "Adres e-mail ju\u017c zarejestrowany", + "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia", + "unknown_error": "Nieznany b\u0142\u0105d, prosz\u0119 zg\u0142osi\u0107 dane z loga" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "description": "Wprowad\u017a dane", + "title": "Tesla - konfiguracja" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/pt-BR.json b/homeassistant/components/tesla/.translations/pt-BR.json new file mode 100644 index 00000000000..4b6de20d2d7 --- /dev/null +++ b/homeassistant/components/tesla/.translations/pt-BR.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "connection_error": "Erro na conex\u00e3o; verifique a rede e tente novamente", + "identifier_exists": "E-mail j\u00e1 registrado", + "invalid_credentials": "Credenciais inv\u00e1lidas", + "unknown_error": "Erro desconhecido, por favor, relate informa\u00e7\u00f5es do log" + }, + "step": { + "user": { + "data": { + "password": "Senha", + "username": "Endere\u00e7o de e-mail" + }, + "description": "Por favor, insira suas informa\u00e7\u00f5es.", + "title": "Tesla - Configura\u00e7\u00e3o" + } + }, + "title": "Tesla" + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/ru.json b/homeassistant/components/tesla/.translations/ru.json new file mode 100644 index 00000000000..15eeabf6136 --- /dev/null +++ b/homeassistant/components/tesla/.translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0441\u0435\u0442\u044c \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", + "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "unknown_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "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 \u0434\u0430\u043d\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438.", + "title": "Tesla" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u0441\u0435\u043a.)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/sl.json b/homeassistant/components/tesla/.translations/sl.json new file mode 100644 index 00000000000..ec6d5850d92 --- /dev/null +++ b/homeassistant/components/tesla/.translations/sl.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Napaka pri povezovanju; preverite omre\u017eje in poskusite znova", + "identifier_exists": "E-po\u0161ta je \u017ee registrirana", + "invalid_credentials": "Neveljavne poverilnice", + "unknown_error": "Neznana napaka, sporo\u010dite podatke dnevnika" + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "description": "Prosimo, vnesite svoje podatke.", + "title": "Tesla - konfiguracija" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Sekund med skeniranjem" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/zh-Hant.json b/homeassistant/components/tesla/.translations/zh-Hant.json new file mode 100644 index 00000000000..776a80da7fb --- /dev/null +++ b/homeassistant/components/tesla/.translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "\u9023\u7dda\u932f\u8aa4\uff1b\u8acb\u6aa2\u5bdf\u7db2\u8def\u5f8c\u518d\u8a66\u4e00\u6b21", + "identifier_exists": "\u90f5\u4ef6\u5df2\u8a3b\u518a", + "invalid_credentials": "\u6191\u8b49\u7121\u6548", + "unknown_error": "\u672a\u77e5\u932f\u8aa4\uff0c\u8acb\u56de\u5831\u7d00\u9304" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "description": "\u8acb\u8f38\u5165\u8cc7\u8a0a\u3002", + "title": "Tesla - \u8a2d\u5b9a" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u6383\u63cf\u9593\u9694\u79d2\u6578" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index a3d45eed01c..1ae65f66821 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -1,21 +1,32 @@ """Support for Tesla cars.""" +import asyncio from collections import defaultdict import logging -from teslajsonpy import Controller as teslaAPI, TeslaException +from teslajsonpy import Controller as TeslaAPI, TeslaException import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, + CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, + CONF_TOKEN, CONF_USERNAME, ) -from homeassistant.helpers import aiohttp_client, config_validation as cv, discovery +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from .const import DOMAIN, TESLA_COMPONENTS +from .config_flow import ( + CannotConnect, + InvalidAuth, + configured_instances, + validate_input, +) +from .const import DATA_LISTENER, DOMAIN, ICONS, TESLA_COMPONENTS _LOGGER = logging.getLogger(__name__) @@ -34,72 +45,148 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -NOTIFICATION_ID = "tesla_integration_notification" -NOTIFICATION_TITLE = "Tesla integration setup" + +@callback +def _async_save_tokens(hass, config_entry, access_token, refresh_token): + hass.config_entries.async_update_entry( + config_entry, + data={ + **config_entry.data, + CONF_ACCESS_TOKEN: access_token, + CONF_TOKEN: refresh_token, + }, + ) async def async_setup(hass, base_config): """Set up of Tesla component.""" - config = base_config.get(DOMAIN) - email = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - update_interval = config.get(CONF_SCAN_INTERVAL) - if hass.data.get(DOMAIN) is None: + def _update_entry(email, data=None, options=None): + data = data or {} + options = options or {CONF_SCAN_INTERVAL: 300} + for entry in hass.config_entries.async_entries(DOMAIN): + if email != entry.title: + continue + hass.config_entries.async_update_entry(entry, data=data, options=options) + + config = base_config.get(DOMAIN) + if not config: + return True + email = config[CONF_USERNAME] + password = config[CONF_PASSWORD] + scan_interval = config[CONF_SCAN_INTERVAL] + if email in configured_instances(hass): try: - websession = aiohttp_client.async_get_clientsession(hass) - controller = teslaAPI( - websession, - email=email, - password=password, - update_interval=update_interval, - ) - await controller.connect(test_login=False) - hass.data[DOMAIN] = {"controller": controller, "devices": defaultdict(list)} - _LOGGER.debug("Connected to the Tesla API.") - except TeslaException as ex: - if ex.code == 401: - hass.components.persistent_notification.create( - "Error:
Please check username and password." - "You will need to restart Home Assistant after fixing.", - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - else: - hass.components.persistent_notification.create( - "Error:
Can't communicate with Tesla API.
" - "Error code: {} Reason: {}" - "You will need to restart Home Assistant after fixing." - "".format(ex.code, ex.message), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) - _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) + info = await validate_input(hass, config) + except (CannotConnect, InvalidAuth): return False - all_devices = controller.get_homeassistant_components() + _update_entry( + email, + data={ + CONF_ACCESS_TOKEN: info[CONF_ACCESS_TOKEN], + CONF_TOKEN: info[CONF_TOKEN], + }, + options={CONF_SCAN_INTERVAL: scan_interval}, + ) + else: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_USERNAME: email, CONF_PASSWORD: password}, + ) + ) + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][email] = {CONF_SCAN_INTERVAL: scan_interval} + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Tesla as config entry.""" + + hass.data.setdefault(DOMAIN, {}) + config = config_entry.data + websession = aiohttp_client.async_get_clientsession(hass) + email = config_entry.title + if email in hass.data[DOMAIN] and CONF_SCAN_INTERVAL in hass.data[DOMAIN][email]: + scan_interval = hass.data[DOMAIN][email][CONF_SCAN_INTERVAL] + hass.config_entries.async_update_entry( + config_entry, options={CONF_SCAN_INTERVAL: scan_interval} + ) + hass.data[DOMAIN].pop(email) + try: + controller = TeslaAPI( + websession, + refresh_token=config[CONF_TOKEN], + update_interval=config_entry.options.get(CONF_SCAN_INTERVAL, 300), + ) + (refresh_token, access_token) = await controller.connect() + except TeslaException as ex: + _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) + return False + _async_save_tokens(hass, config_entry, access_token, refresh_token) + entry_data = hass.data[DOMAIN][config_entry.entry_id] = { + "controller": controller, + "devices": defaultdict(list), + DATA_LISTENER: [config_entry.add_update_listener(update_listener)], + } + _LOGGER.debug("Connected to the Tesla API.") + all_devices = entry_data["controller"].get_homeassistant_components() + if not all_devices: return False for device in all_devices: - hass.data[DOMAIN]["devices"][device.hass_type].append(device) + entry_data["devices"][device.hass_type].append(device) for component in TESLA_COMPONENTS: + _LOGGER.debug("Loading %s", component) hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {}, base_config) + hass.config_entries.async_forward_entry_setup(config_entry, component) ) return True +async def async_unload_entry(hass, config_entry) -> bool: + """Unload a config entry.""" + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in TESLA_COMPONENTS + ] + ) + for listener in hass.data[DOMAIN][config_entry.entry_id][DATA_LISTENER]: + listener() + username = config_entry.title + hass.data[DOMAIN].pop(config_entry.entry_id) + _LOGGER.debug("Unloaded entry for %s", username) + return True + + +async def update_listener(hass, config_entry): + """Update when config_entry options update.""" + controller = hass.data[DOMAIN][config_entry.entry_id]["controller"] + old_update_interval = controller.update_interval + controller.update_interval = config_entry.options.get(CONF_SCAN_INTERVAL) + _LOGGER.debug( + "Changing scan_interval from %s to %s", + old_update_interval, + controller.update_interval, + ) + + class TeslaDevice(Entity): """Representation of a Tesla device.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the Tesla device.""" self.tesla_device = tesla_device self.controller = controller + self.config_entry = config_entry self._name = self.tesla_device.name self.tesla_id = slugify(self.tesla_device.uniq_name) self._attributes = {} + self._icon = ICONS.get(self.tesla_device.type) @property def name(self): @@ -111,6 +198,11 @@ class TeslaDevice(Entity): """Return a unique ID.""" return self.tesla_id + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon + @property def should_poll(self): """Return the polling state.""" @@ -124,6 +216,17 @@ class TeslaDevice(Entity): attr[ATTR_BATTERY_LEVEL] = self.tesla_device.battery_level() return attr + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self.tesla_device.id())}, + "name": self.tesla_device.car_name(), + "manufacturer": "Tesla", + "model": self.tesla_device.car_type, + "sw_version": self.tesla_device.car_version, + } + async def async_added_to_hass(self): """Register state update callback.""" pass @@ -134,4 +237,10 @@ class TeslaDevice(Entity): async def async_update(self): """Update the state of the device.""" + if self.controller.is_token_refreshed(): + (refresh_token, access_token) = self.controller.get_tokens() + _async_save_tokens( + self.hass, self.config_entry, access_token, refresh_token + ) + _LOGGER.debug("Saving new tokens in config_entry") await self.tesla_device.async_update() diff --git a/homeassistant/components/tesla/binary_sensor.py b/homeassistant/components/tesla/binary_sensor.py index 738533a9b56..8f610d960b3 100644 --- a/homeassistant/components/tesla/binary_sensor.py +++ b/homeassistant/components/tesla/binary_sensor.py @@ -8,21 +8,35 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla binary sensor.""" - devices = [ - TeslaBinarySensor(device, hass.data[TESLA_DOMAIN]["controller"], "connectivity") - for device in hass.data[TESLA_DOMAIN]["devices"]["binary_sensor"] - ] - add_entities(devices, True) + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + async_add_entities( + [ + TeslaBinarySensor( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + "connectivity", + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "binary_sensor" + ] + ], + True, + ) class TeslaBinarySensor(TeslaDevice, BinarySensorDevice): """Implement an Tesla binary sensor for parking and charger.""" - def __init__(self, tesla_device, controller, sensor_type): + def __init__(self, tesla_device, controller, sensor_type, config_entry): """Initialise of a Tesla binary sensor.""" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._state = False self._sensor_type = sensor_type diff --git a/homeassistant/components/tesla/climate.py b/homeassistant/components/tesla/climate.py index 85fd8a8e258..d7f21d7895f 100644 --- a/homeassistant/components/tesla/climate.py +++ b/homeassistant/components/tesla/climate.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SUPPORT_TARGET_TEMPERATURE, ) @@ -13,24 +13,37 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_OFF] +SUPPORT_HVAC = [HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF] -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla climate platform.""" - devices = [ - TeslaThermostat(device, hass.data[TESLA_DOMAIN]["controller"]) - for device in hass.data[TESLA_DOMAIN]["devices"]["climate"] - ] - add_entities(devices, True) + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + async_add_entities( + [ + TeslaThermostat( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "climate" + ] + ], + True, + ) class TeslaThermostat(TeslaDevice, ClimateDevice): """Representation of a Tesla climate.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialize the Tesla device.""" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._target_temperature = None self._temperature = None @@ -46,7 +59,7 @@ class TeslaThermostat(TeslaDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ if self.tesla_device.is_hvac_enabled(): - return HVAC_MODE_HEAT + return HVAC_MODE_HEAT_COOL return HVAC_MODE_OFF @property @@ -95,5 +108,5 @@ class TeslaThermostat(TeslaDevice, ClimateDevice): _LOGGER.debug("Setting mode for: %s", self._name) if hvac_mode == HVAC_MODE_OFF: await self.tesla_device.set_status(False) - elif hvac_mode == HVAC_MODE_HEAT: + elif hvac_mode == HVAC_MODE_HEAT_COOL: await self.tesla_device.set_status(True) diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py new file mode 100644 index 00000000000..2d2bc0158d2 --- /dev/null +++ b/homeassistant/components/tesla/config_flow.py @@ -0,0 +1,143 @@ +"""Tesla Config Flow.""" +import logging + +from teslajsonpy import Controller as TeslaAPI, TeslaException +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_TOKEN, + CONF_USERNAME, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +@callback +def configured_instances(hass): + """Return a set of configured Tesla instances.""" + return set(entry.title for entry in hass.config_entries.async_entries(DOMAIN)) + + +class TeslaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tesla.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + + if not user_input: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={}, + description_placeholders={}, + ) + + if user_input[CONF_USERNAME] in configured_instances(self.hass): + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={CONF_USERNAME: "identifier_exists"}, + description_placeholders={}, + ) + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": "connection_error"}, + description_placeholders={}, + ) + except InvalidAuth: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors={"base": "invalid_credentials"}, + description_placeholders={}, + ) + return self.async_create_entry(title=user_input[CONF_USERNAME], data=info) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Tesla.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get(CONF_SCAN_INTERVAL, 300), + ): vol.All(cv.positive_int, vol.Clamp(min=300)) + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + config = {} + websession = aiohttp_client.async_get_clientsession(hass) + try: + controller = TeslaAPI( + websession, + email=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + update_interval=300, + ) + (config[CONF_TOKEN], config[CONF_ACCESS_TOKEN]) = await controller.connect( + test_login=True + ) + except TeslaException as ex: + if ex.code == 401: + _LOGGER.error("Invalid credentials: %s", ex) + raise InvalidAuth() + _LOGGER.error("Unable to communicate with Tesla API: %s", ex) + raise CannotConnect() + _LOGGER.debug("Credentials successfully connected to the Tesla API") + return config + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index 30a58b733ed..be460a430ac 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -9,7 +9,7 @@ TESLA_COMPONENTS = [ "device_tracker", "switch", ] -SENSOR_ICONS = { +ICONS = { "battery sensor": "mdi:battery", "range sensor": "mdi:gauge", "mileage sensor": "mdi:counter", diff --git a/homeassistant/components/tesla/device_tracker.py b/homeassistant/components/tesla/device_tracker.py index c205cc587eb..08e5d58ba6e 100644 --- a/homeassistant/components/tesla/device_tracker.py +++ b/homeassistant/components/tesla/device_tracker.py @@ -1,45 +1,70 @@ """Support for tracking Tesla cars.""" import logging -from homeassistant.helpers.event import async_track_utc_time_change -from homeassistant.util import slugify +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity -from . import DOMAIN as TESLA_DOMAIN +from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Set up the Tesla tracker.""" - tracker = TeslaDeviceTracker( - hass, config, async_see, hass.data[TESLA_DOMAIN]["devices"]["devices_tracker"] - ) - await tracker.update_info() - async_track_utc_time_change(hass, tracker.update_info, second=range(0, 60, 30)) - return True +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + entities = [ + TeslaDeviceEntity( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"][ + "devices_tracker" + ] + ] + async_add_entities(entities, True) -class TeslaDeviceTracker: +class TeslaDeviceEntity(TeslaDevice, TrackerEntity): """A class representing a Tesla device.""" - def __init__(self, hass, config, see, tesla_devices): + def __init__(self, tesla_device, controller, config_entry): """Initialize the Tesla device scanner.""" - self.hass = hass - self.see = see - self.devices = tesla_devices + super().__init__(tesla_device, controller, config_entry) + self._latitude = None + self._longitude = None + self._attributes = {"trackr_id": self.unique_id} + self._listener = None - async def update_info(self, now=None): + async def async_update(self): """Update the device info.""" - for device in self.devices: - await device.async_update() - name = device.name - _LOGGER.debug("Updating device position: %s", name) - dev_id = slugify(device.uniq_name) - location = device.get_location() - if location: - lat = location["latitude"] - lon = location["longitude"] - attrs = {"trackr_id": dev_id, "id": dev_id, "name": name} - await self.see( - dev_id=dev_id, host_name=name, gps=(lat, lon), attributes=attrs - ) + _LOGGER.debug("Updating device position: %s", self.name) + await super().async_update() + location = self.tesla_device.get_location() + if location: + self._latitude = location["latitude"] + self._longitude = location["longitude"] + self._attributes = { + "trackr_id": self.unique_id, + "heading": location["heading"], + "speed": location["speed"], + } + + @property + def latitude(self) -> float: + """Return latitude value of the device.""" + return self._latitude + + @property + def longitude(self) -> float: + """Return longitude value of the device.""" + return self._longitude + + @property + def should_poll(self): + """Return whether polling is needed.""" + return True + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS diff --git a/homeassistant/components/tesla/lock.py b/homeassistant/components/tesla/lock.py index 5e97602357d..33eed8cf7c1 100644 --- a/homeassistant/components/tesla/lock.py +++ b/homeassistant/components/tesla/lock.py @@ -9,22 +9,31 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla lock platform.""" - devices = [ - TeslaLock(device, hass.data[TESLA_DOMAIN]["controller"]) - for device in hass.data[TESLA_DOMAIN]["devices"]["lock"] + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + entities = [ + TeslaLock( + device, + hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"], + config_entry, + ) + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["lock"] ] - add_entities(devices, True) + async_add_entities(entities, True) class TeslaLock(TeslaDevice, LockDevice): """Representation of a Tesla door lock.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise of the lock.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_lock(self, **kwargs): """Send the lock command.""" diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index a2021092413..e3392074679 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -1,8 +1,9 @@ { "domain": "tesla", "name": "Tesla", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": ["teslajsonpy==0.2.0"], + "requirements": ["teslajsonpy==0.2.3"], "dependencies": [], - "codeowners": ["@zabuldon"] + "codeowners": ["@zabuldon", "@alandtse"] } diff --git a/homeassistant/components/tesla/sensor.py b/homeassistant/components/tesla/sensor.py index 1cce37f232a..a282f65f9e1 100644 --- a/homeassistant/components/tesla/sensor.py +++ b/homeassistant/components/tesla/sensor.py @@ -8,36 +8,41 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.helpers.entity import Entity +from homeassistant.util.distance import convert from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla sensor platform.""" - controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] - devices = [] + pass - for device in hass.data[TESLA_DOMAIN]["devices"]["sensor"]: - if device.bin_type == 0x4: - devices.append(TeslaSensor(device, controller, "inside")) - devices.append(TeslaSensor(device, controller, "outside")) - elif device.bin_type in [0xA, 0xB, 0x5]: - devices.append(TeslaSensor(device, controller)) - add_entities(devices, True) + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] + entities = [] + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["sensor"]: + if device.type == "temperature sensor": + entities.append(TeslaSensor(device, controller, config_entry, "inside")) + entities.append(TeslaSensor(device, controller, config_entry, "outside")) + else: + entities.append(TeslaSensor(device, controller, config_entry)) + async_add_entities(entities, True) class TeslaSensor(TeslaDevice, Entity): """Representation of Tesla sensors.""" - def __init__(self, tesla_device, controller, sensor_type=None): + def __init__(self, tesla_device, controller, config_entry, sensor_type=None): """Initialize of the sensor.""" self.current_value = None - self._unit = None + self.units = None self.last_changed_time = None self.type = sensor_type - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) if self.type: self._name = f"{self.tesla_device.name} ({self.type})" @@ -57,7 +62,7 @@ class TeslaSensor(TeslaDevice, Entity): @property def unit_of_measurement(self): """Return the unit_of_measurement of the device.""" - return self._unit + return self.units async def async_update(self): """Update the state from the sensor.""" @@ -65,32 +70,34 @@ class TeslaSensor(TeslaDevice, Entity): await super().async_update() units = self.tesla_device.measurement - if self.tesla_device.bin_type == 0x4: + if self.tesla_device.type == "temperature sensor": if self.type == "outside": self.current_value = self.tesla_device.get_outside_temp() else: self.current_value = self.tesla_device.get_inside_temp() if units == "F": - self._unit = TEMP_FAHRENHEIT + self.units = TEMP_FAHRENHEIT else: - self._unit = TEMP_CELSIUS - elif self.tesla_device.bin_type == 0xA or self.tesla_device.bin_type == 0xB: + self.units = TEMP_CELSIUS + elif self.tesla_device.type in ["range sensor", "mileage sensor"]: self.current_value = self.tesla_device.get_value() - tesla_dist_unit = self.tesla_device.measurement - if tesla_dist_unit == "LENGTH_MILES": - self._unit = LENGTH_MILES + if units == "LENGTH_MILES": + self.units = LENGTH_MILES else: - self._unit = LENGTH_KILOMETERS - self.current_value /= 0.621371 - self.current_value = round(self.current_value, 2) + self.units = LENGTH_KILOMETERS + self.current_value = round( + convert(self.current_value, LENGTH_MILES, LENGTH_KILOMETERS), 2 + ) + elif self.tesla_device.type == "charging rate sensor": + self.current_value = self.tesla_device.charging_rate + self.units = units + self._attributes = { + "time_left": self.tesla_device.time_left, + "added_range": self.tesla_device.added_range, + "charge_current_request": self.tesla_device.charge_current_request, + "charger_actual_current": self.tesla_device.charger_actual_current, + "charger_voltage": self.tesla_device.charger_voltage, + } else: self.current_value = self.tesla_device.get_value() - if self.tesla_device.bin_type == 0x5: - self._unit = units - elif self.tesla_device.bin_type in (0xA, 0xB): - if units == "LENGTH_MILES": - self._unit = LENGTH_MILES - else: - self._unit = LENGTH_KILOMETERS - self.current_value /= 0.621371 - self.current_value = round(self.current_value, 2) + self.units = units diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json new file mode 100644 index 00000000000..831406a0d63 --- /dev/null +++ b/homeassistant/components/tesla/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" + }, + "step": { + "user": { + "data": { + "username": "Email Address", + "password": "Password" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans" + } + } + } + } +} diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 5f432875aeb..fc9b5e1ba88 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -9,26 +9,31 @@ from . import DOMAIN as TESLA_DOMAIN, TeslaDevice _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Tesla switch platform.""" - controller = hass.data[TESLA_DOMAIN]["controller"] - devices = [] - for device in hass.data[TESLA_DOMAIN]["devices"]["switch"]: - if device.bin_type == 0x8: - devices.append(ChargerSwitch(device, controller)) - devices.append(UpdateSwitch(device, controller)) - elif device.bin_type == 0x9: - devices.append(RangeSwitch(device, controller)) - add_entities(devices, True) + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Tesla binary_sensors by config_entry.""" + controller = hass.data[TESLA_DOMAIN][config_entry.entry_id]["controller"] + entities = [] + for device in hass.data[TESLA_DOMAIN][config_entry.entry_id]["devices"]["switch"]: + if device.type == "charger switch": + entities.append(ChargerSwitch(device, controller, config_entry)) + entities.append(UpdateSwitch(device, controller, config_entry)) + elif device.type == "maxrange switch": + entities.append(RangeSwitch(device, controller, config_entry)) + async_add_entities(entities, True) class ChargerSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla charger switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise of the switch.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_turn_on(self, **kwargs): """Send the on command.""" @@ -55,10 +60,10 @@ class ChargerSwitch(TeslaDevice, SwitchDevice): class RangeSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla max range charging switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the switch.""" self._state = None - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) async def async_turn_on(self, **kwargs): """Send the on command.""" @@ -85,11 +90,11 @@ class RangeSwitch(TeslaDevice, SwitchDevice): class UpdateSwitch(TeslaDevice, SwitchDevice): """Representation of a Tesla update switch.""" - def __init__(self, tesla_device, controller): + def __init__(self, tesla_device, controller, config_entry): """Initialise the switch.""" self._state = None tesla_device.type = "update switch" - super().__init__(tesla_device, controller) + super().__init__(tesla_device, controller, config_entry) self._name = self._name.replace("charger", "update") self.tesla_id = self.tesla_id.replace("charger", "update") diff --git a/homeassistant/components/tfiac/manifest.json b/homeassistant/components/tfiac/manifest.json index 7c93000790c..d0b36598ce5 100644 --- a/homeassistant/components/tfiac/manifest.json +++ b/homeassistant/components/tfiac/manifest.json @@ -2,12 +2,7 @@ "domain": "tfiac", "name": "Tfiac", "documentation": "https://www.home-assistant.io/integrations/tfiac", - "requirements": [ - "pytfiac==0.4" - ], + "requirements": ["pytfiac==0.4"], "dependencies": [], - "codeowners": [ - "@fredrike", - "@mellado" - ] + "codeowners": ["@fredrike", "@mellado"] } diff --git a/homeassistant/components/thermoworks_smoke/manifest.json b/homeassistant/components/thermoworks_smoke/manifest.json index 93a334fc986..a11d3ac98ab 100644 --- a/homeassistant/components/thermoworks_smoke/manifest.json +++ b/homeassistant/components/thermoworks_smoke/manifest.json @@ -1,11 +1,8 @@ { "domain": "thermoworks_smoke", - "name": "Thermoworks smoke", + "name": "ThermoWorks Smoke", "documentation": "https://www.home-assistant.io/integrations/thermoworks_smoke", - "requirements": [ - "stringcase==1.2.0", - "thermoworks_smoke==0.1.8" - ], + "requirements": ["stringcase==1.2.0", "thermoworks_smoke==0.1.8"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/thethingsnetwork/manifest.json b/homeassistant/components/thethingsnetwork/manifest.json index 98a852dea08..d121996cb4a 100644 --- a/homeassistant/components/thethingsnetwork/manifest.json +++ b/homeassistant/components/thethingsnetwork/manifest.json @@ -1,10 +1,8 @@ { "domain": "thethingsnetwork", - "name": "Thethingsnetwork", + "name": "The Things Network", "documentation": "https://www.home-assistant.io/integrations/thethingsnetwork", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/thingspeak/manifest.json b/homeassistant/components/thingspeak/manifest.json index 9fddd941543..9e6403e6eaf 100644 --- a/homeassistant/components/thingspeak/manifest.json +++ b/homeassistant/components/thingspeak/manifest.json @@ -1,10 +1,8 @@ { "domain": "thingspeak", - "name": "Thingspeak", + "name": "ThingSpeak", "documentation": "https://www.home-assistant.io/integrations/thingspeak", - "requirements": [ - "thingspeak==1.0.0" - ], + "requirements": ["thingspeak==1.0.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/thinkingcleaner/manifest.json b/homeassistant/components/thinkingcleaner/manifest.json index c7c8aaaf16a..c82859893b2 100644 --- a/homeassistant/components/thinkingcleaner/manifest.json +++ b/homeassistant/components/thinkingcleaner/manifest.json @@ -1,10 +1,8 @@ { "domain": "thinkingcleaner", - "name": "Thinkingcleaner", + "name": "Thinking Cleaner", "documentation": "https://www.home-assistant.io/integrations/thinkingcleaner", - "requirements": [ - "pythinkingcleaner==0.0.3" - ], + "requirements": ["pythinkingcleaner==0.0.3"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/thomson/device_tracker.py b/homeassistant/components/thomson/device_tracker.py index 214c1b8cfb4..1f3fda6cc72 100644 --- a/homeassistant/components/thomson/device_tracker.py +++ b/homeassistant/components/thomson/device_tracker.py @@ -5,13 +5,13 @@ import telnetlib import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/threshold/manifest.json b/homeassistant/components/threshold/manifest.json index f3aa5405278..939c1095c2b 100644 --- a/homeassistant/components/threshold/manifest.json +++ b/homeassistant/components/threshold/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/threshold", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 99c1335517e..23bc76ee6b5 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,11 +2,8 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": [ - "pyTibber==0.12.0" - ], + "requirements": ["pyTibber==0.12.0"], "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "codeowners": ["@danielhiversen"], + "quality_scale": "silver" } diff --git a/homeassistant/components/tikteck/manifest.json b/homeassistant/components/tikteck/manifest.json index c81b6f0a0b1..2f6cec846fd 100644 --- a/homeassistant/components/tikteck/manifest.json +++ b/homeassistant/components/tikteck/manifest.json @@ -2,9 +2,7 @@ "domain": "tikteck", "name": "Tikteck", "documentation": "https://www.home-assistant.io/integrations/tikteck", - "requirements": [ - "tikteck==0.4" - ], + "requirements": ["tikteck==0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 1cb88f67c2f..8bc4fb11cdf 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -1,13 +1,13 @@ """Support for Tile® Bluetooth trackers.""" -import logging from datetime import timedelta +import logging from pytile import async_login from pytile.errors import SessionExpiredError, TileError import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_MONITORED_VARIABLES, CONF_PASSWORD +from homeassistant.const import CONF_MONITORED_VARIABLES, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 801e09fd954..b3f032e95e8 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -2,11 +2,7 @@ "domain": "tile", "name": "Tile", "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": [ - "pytile==3.0.1" - ], + "requirements": ["pytile==3.0.1"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/time_date/manifest.json b/homeassistant/components/time_date/manifest.json index 1824ea2db41..9acac2fa4bc 100644 --- a/homeassistant/components/time_date/manifest.json +++ b/homeassistant/components/time_date/manifest.json @@ -1,10 +1,9 @@ { "domain": "time_date", - "name": "Time date", + "name": "Time & Date", "documentation": "https://www.home-assistant.io/integrations/time_date", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/time_date/sensor.py b/homeassistant/components/time_date/sensor.py index cbe4c85ace3..1deb564133e 100644 --- a/homeassistant/components/time_date/sensor.py +++ b/homeassistant/components/time_date/sensor.py @@ -4,13 +4,13 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_DISPLAY_OPTIONS -from homeassistant.helpers.entity import Entity +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -20,7 +20,8 @@ OPTION_TYPES = { "time": "Time", "date": "Date", "date_time": "Date & Time", - "date_time_iso": "Date & Time ISO", + "date_time_utc": "Date & Time (UTC)", + "date_time_iso": "Date & Time (ISO)", "time_date": "Time & Date", "beat": "Internet Time", "time_utc": "Time (UTC)", @@ -102,6 +103,7 @@ class TimeDateSensor(Entity): time = dt_util.as_local(time_date).strftime(TIME_STR_FORMAT) time_utc = time_date.strftime(TIME_STR_FORMAT) date = dt_util.as_local(time_date).date().isoformat() + date_utc = time_date.date().isoformat() # Calculate Swatch Internet Time. time_bmt = time_date + timedelta(hours=1) @@ -119,6 +121,8 @@ class TimeDateSensor(Entity): self._state = date elif self.type == "date_time": self._state = f"{date}, {time}" + elif self.type == "date_time_utc": + self._state = f"{date_utc}, {time_utc}" elif self.type == "time_date": self._state = f"{time}, {date}" elif self.type == "time_utc": diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index de60752eada..ccc04d7f72a 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -4,11 +4,12 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_ICON, CONF_NAME +from homeassistant.const import CONF_ICON, CONF_NAME, SERVICE_RELOAD import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.helpers.service import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -16,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "timer" ENTITY_ID_FORMAT = DOMAIN + ".{}" -DEFAULT_DURATION = 0 +DEFAULT_DURATION = timedelta(0) ATTR_DURATION = "duration" ATTR_REMAINING = "remaining" CONF_DURATION = "duration" @@ -36,29 +37,72 @@ SERVICE_PAUSE = "pause" SERVICE_CANCEL = "cancel" SERVICE_FINISH = "finish" + +def _none_to_empty_dict(value): + if value is None: + return {} + return value + + CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( - vol.Any( + vol.All( + _none_to_empty_dict, { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ICON): cv.icon, vol.Optional( - CONF_DURATION, timedelta(DEFAULT_DURATION) + CONF_DURATION, default=DEFAULT_DURATION ): cv.time_period, }, - None, ) ) }, extra=vol.ALLOW_EXTRA, ) +RELOAD_SERVICE_SCHEMA = vol.Schema({}) + async def async_setup(hass, config): """Set up a timer.""" component = EntityComponent(_LOGGER, DOMAIN, hass) + entities = await _async_process_config(hass, config) + + async def reload_service_handler(service_call): + """Remove all input booleans and load new ones from config.""" + conf = await component.async_prepare_reload() + if conf is None: + return + new_entities = await _async_process_config(hass, conf) + if new_entities: + await component.async_add_entities(new_entities) + + homeassistant.helpers.service.async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD, + reload_service_handler, + schema=RELOAD_SERVICE_SCHEMA, + ) + component.async_register_entity_service( + SERVICE_START, + {vol.Optional(ATTR_DURATION, default=DEFAULT_DURATION): cv.time_period}, + "async_start", + ) + component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") + component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") + component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") + + if entities: + await component.async_add_entities(entities) + return True + + +async def _async_process_config(hass, config): + """Process config and create list of entities.""" entities = [] for object_id, cfg in config[DOMAIN].items(): @@ -67,28 +111,11 @@ async def async_setup(hass, config): name = cfg.get(CONF_NAME) icon = cfg.get(CONF_ICON) - duration = cfg.get(CONF_DURATION) + duration = cfg[CONF_DURATION] entities.append(Timer(hass, object_id, name, icon, duration)) - if not entities: - return False - - component.async_register_entity_service( - SERVICE_START, - { - vol.Optional( - ATTR_DURATION, default=timedelta(DEFAULT_DURATION) - ): cv.time_period - }, - "async_start", - ) - component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause") - component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel") - component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish") - - await component.async_add_entities(entities) - return True + return entities class Timer(RestoreEntity): @@ -153,7 +180,7 @@ class Timer(RestoreEntity): newduration = duration event = EVENT_TIMER_STARTED - if self._state == STATUS_PAUSED: + if self._state == STATUS_ACTIVE or self._state == STATUS_PAUSED: event = EVENT_TIMER_RESTARTED self._state = STATUS_ACTIVE diff --git a/homeassistant/components/timer/manifest.json b/homeassistant/components/timer/manifest.json index 1cf2c610014..6aa33f743cd 100644 --- a/homeassistant/components/timer/manifest.json +++ b/homeassistant/components/timer/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/timer", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/tmb/__init__.py b/homeassistant/components/tmb/__init__.py new file mode 100644 index 00000000000..5a0f5aa54fb --- /dev/null +++ b/homeassistant/components/tmb/__init__.py @@ -0,0 +1 @@ +"""Support for Transports Metropolitans de Barcelona.""" diff --git a/homeassistant/components/tmb/manifest.json b/homeassistant/components/tmb/manifest.json new file mode 100644 index 00000000000..bb76b3193fc --- /dev/null +++ b/homeassistant/components/tmb/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "tmb", + "name": "Transports Metropolitans de Barcelona", + "documentation": "https://www.home-assistant.io/integrations/tmb", + "requirements": [ + "tmb==0.0.4" + ], + "dependencies": [], + "codeowners": [ + "@alemuro" + ] +} \ No newline at end of file diff --git a/homeassistant/components/tmb/sensor.py b/homeassistant/components/tmb/sensor.py new file mode 100644 index 00000000000..6d8bdc7eac7 --- /dev/null +++ b/homeassistant/components/tmb/sensor.py @@ -0,0 +1,120 @@ +"""Support for TMB (Transports Metropolitans de Barcelona) Barcelona public transport.""" +from datetime import timedelta +import logging + +from requests import HTTPError +from tmb import IBus +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data provided by Transport Metropolitans de Barcelona" + +ICON = "mdi:bus-clock" + +CONF_APP_ID = "app_id" +CONF_APP_KEY = "app_key" +CONF_LINE = "line" +CONF_BUS_STOP = "stop" +CONF_BUS_STOPS = "stops" +ATTR_BUS_STOP = "stop" +ATTR_LINE = "line" + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +LINE_STOP_SCHEMA = vol.Schema( + { + vol.Required(CONF_BUS_STOP): cv.string, + vol.Required(CONF_LINE): cv.string, + vol.Optional(CONF_NAME): cv.string, + } +) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_APP_ID): cv.string, + vol.Required(CONF_APP_KEY): cv.string, + vol.Required(CONF_BUS_STOPS): vol.All(cv.ensure_list, [LINE_STOP_SCHEMA]), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensors.""" + ibus_client = IBus(config[CONF_APP_ID], config[CONF_APP_KEY]) + + sensors = [] + + for line_stop in config.get(CONF_BUS_STOPS): + line = line_stop[CONF_LINE] + stop = line_stop[CONF_BUS_STOP] + if line_stop.get(CONF_NAME): + name = f"{line} - {line_stop[CONF_NAME]} ({stop})" + else: + name = f"{line} - {stop}" + sensors.append(TMBSensor(ibus_client, stop, line, name)) + + add_entities(sensors, True) + + +class TMBSensor(Entity): + """Implementation of a TMB line/stop Sensor.""" + + def __init__(self, ibus_client, stop, line, name): + """Initialize the sensor.""" + self._ibus_client = ibus_client + self._stop = stop + self._line = line.upper() + self._name = name + self._unit = "minutes" + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the icon for the frontend.""" + return ICON + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return f"{self._stop}_{self._line}" + + @property + def state(self): + """Return the next departure time.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes of the last update.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_BUS_STOP: self._stop, + ATTR_LINE: self._line, + } + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Get the next bus information.""" + try: + self._state = self._ibus_client.get_stop_forecast(self._stop, self._line) + except HTTPError: + _LOGGER.error( + "Unable to fetch data from TMB API. Please check your API keys are valid." + ) diff --git a/homeassistant/components/tod/manifest.json b/homeassistant/components/tod/manifest.json index ff582b33cef..8a3b3bc8540 100644 --- a/homeassistant/components/tod/manifest.json +++ b/homeassistant/components/tod/manifest.json @@ -1,8 +1,9 @@ { "domain": "tod", - "name": "Tod", + "name": "Times of the Day", "documentation": "https://www.home-assistant.io/integrations/tod", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index eabec37a053..fd246520696 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -16,8 +16,8 @@ from .const import ( ALL_TASKS, CHECKED, COMPLETED, - CONF_PROJECT_DUE_DATE, CONF_EXTRA_PROJECTS, + CONF_PROJECT_DUE_DATE, CONF_PROJECT_LABEL_WHITELIST, CONF_PROJECT_WHITELIST, CONTENT, @@ -152,7 +152,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(project_devices) def handle_new_task(call): - """Call when a user creates a new Todoist Task from HASS.""" + """Call when a user creates a new Todoist Task from Home Assistant.""" project_name = call.data[PROJECT_NAME] project_id = project_id_lookup[project_name] @@ -571,7 +571,7 @@ class TodoistProjectData: if self.event[END] is not None: self.event[END] = {DATETIME: self.event[END].strftime(DATE_STR_FORMAT)} else: - # HASS gets cranky if a calendar event never ends + # Home Assistant gets cranky if a calendar event never ends # Let's set our "due date" to tomorrow self.event[END] = { DATETIME: (datetime.utcnow() + timedelta(days=1)).strftime( diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index e7876c953cc..d9f14172ff1 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -2,9 +2,7 @@ "domain": "todoist", "name": "Todoist", "documentation": "https://www.home-assistant.io/integrations/todoist", - "requirements": [ - "todoist-python==8.0.0" - ], + "requirements": ["todoist-python==8.0.0"], "dependencies": [], "codeowners": ["@boralyl"] } diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index c2d23cc4bec..3382e27693d 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -21,5 +21,5 @@ new_task: example: en due_date: description: The day this task is due, in format YYYY-MM-DD. - example: 2019-10-22 + example: "2019-10-22" diff --git a/homeassistant/components/tof/manifest.json b/homeassistant/components/tof/manifest.json index dc87b6c2fc7..8edae0026de 100644 --- a/homeassistant/components/tof/manifest.json +++ b/homeassistant/components/tof/manifest.json @@ -1,12 +1,8 @@ { "domain": "tof", - "name": "Tof", + "name": "Time of Flight", "documentation": "https://www.home-assistant.io/integrations/tof", - "requirements": [ - "VL53L1X2==0.1.5" - ], - "dependencies": [ - "rpi_gpio" - ], + "requirements": ["VL53L1X2==0.1.5"], + "dependencies": ["rpi_gpio"], "codeowners": [] } diff --git a/homeassistant/components/tomato/device_tracker.py b/homeassistant/components/tomato/device_tracker.py index 57348c9710a..5a5f1b1985b 100644 --- a/homeassistant/components/tomato/device_tracker.py +++ b/homeassistant/components/tomato/device_tracker.py @@ -6,7 +6,6 @@ import re import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -14,12 +13,13 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_VERIFY_SSL, - CONF_PASSWORD, CONF_USERNAME, + CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv CONF_HTTP_ID = "http_id" @@ -124,7 +124,7 @@ class TomatoDeviceScanner(DeviceScanner): # We get this if we could not connect to the router or # an invalid http_id was supplied. _LOGGER.exception( - "Failed to connect to the router or " "invalid http_id supplied" + "Failed to connect to the router or invalid http_id supplied" ) return False diff --git a/homeassistant/components/toon/.translations/da.json b/homeassistant/components/toon/.translations/da.json index 9200f80add0..e4f73bc7c6b 100644 --- a/homeassistant/components/toon/.translations/da.json +++ b/homeassistant/components/toon/.translations/da.json @@ -3,9 +3,9 @@ "abort": { "client_id": "Klient-id'et fra konfigurationen er ugyldigt.", "client_secret": "Klientens hemmelighed fra konfigurationen er ugyldig.", - "no_agreements": "Denne konto har ingen Toon sk\u00e6rme.", + "no_agreements": "Denne konto har ingen Toon-sk\u00e6rme.", "no_app": "Du skal konfigurere Toon f\u00f8r du kan godkende med det. [L\u00e6s venligst vejledningen](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Der opstod en uventet fejl under autentificering." + "unknown_auth_fail": "Der opstod en uventet fejl under godkendelse." }, "error": { "credentials": "De angivne legitimationsoplysninger er ugyldige.", @@ -18,8 +18,8 @@ "tenant": "Tenant", "username": "Brugernavn" }, - "description": "Godkend med din Eneco Toon konto (ikke udviklerkontoen).", - "title": "Link din Toon konto" + "description": "Godkend med din Eneco Toon-konto (ikke udviklerkontoen).", + "title": "Forbind din Toon-konto" }, "display": { "data": { diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 427f717e3ad..75b46d3f600 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -9,7 +9,7 @@ }, "error": { "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { "authenticate": { diff --git a/homeassistant/components/toon/__init__.py b/homeassistant/components/toon/__init__.py index 19466ba49c4..348826a1264 100644 --- a/homeassistant/components/toon/__init__.py +++ b/homeassistant/components/toon/__init__.py @@ -1,17 +1,18 @@ """Support for Toon van Eneco devices.""" +from functools import partial import logging from typing import Any, Dict -from functools import partial +from toonapilib import Toon import voluptuous as vol +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import config_flow # noqa: F401 from .const import ( @@ -19,10 +20,10 @@ from .const import ( CONF_CLIENT_SECRET, CONF_DISPLAY, CONF_TENANT, + DATA_TOON, DATA_TOON_CLIENT, DATA_TOON_CONFIG, DATA_TOON_UPDATED, - DATA_TOON, DEFAULT_SCAN_INTERVAL, DOMAIN, ) @@ -63,7 +64,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigType) -> bool: """Set up Toon from a config entry.""" - from toonapilib import Toon conf = hass.data.get(DATA_TOON_CONFIG) diff --git a/homeassistant/components/toon/binary_sensor.py b/homeassistant/components/toon/binary_sensor.py index 9962e2c32d3..7cf52919efe 100644 --- a/homeassistant/components/toon/binary_sensor.py +++ b/homeassistant/components/toon/binary_sensor.py @@ -8,11 +8,11 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType from . import ( - ToonData, - ToonEntity, - ToonDisplayDeviceEntity, ToonBoilerDeviceEntity, ToonBoilerModuleDeviceEntity, + ToonData, + ToonDisplayDeviceEntity, + ToonEntity, ) from .const import DATA_TOON, DOMAIN diff --git a/homeassistant/components/toon/climate.py b/homeassistant/components/toon/climate.py index cfe07adfda3..9ce9991c371 100644 --- a/homeassistant/components/toon/climate.py +++ b/homeassistant/components/toon/climate.py @@ -5,6 +5,8 @@ from typing import Any, Dict, List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_HEAT, PRESET_AWAY, PRESET_COMFORT, @@ -12,8 +14,6 @@ from homeassistant.components.climate.const import ( PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -21,8 +21,8 @@ from homeassistant.helpers.typing import HomeAssistantType from . import ToonData, ToonDisplayDeviceEntity from .const import ( - DATA_TOON_CLIENT, DATA_TOON, + DATA_TOON_CLIENT, DEFAULT_MAX_TEMP, DEFAULT_MIN_TEMP, DOMAIN, diff --git a/homeassistant/components/toon/config_flow.py b/homeassistant/components/toon/config_flow.py index 62c141b003a..ce4f347eaf2 100644 --- a/homeassistant/components/toon/config_flow.py +++ b/homeassistant/components/toon/config_flow.py @@ -1,8 +1,15 @@ """Config flow to configure the Toon component.""" from collections import OrderedDict -import logging from functools import partial +import logging +from toonapilib import Toon +from toonapilib.toonapilibexceptions import ( + AgreementsRetrievalError, + InvalidConsumerKey, + InvalidConsumerSecret, + InvalidCredentials, +) import voluptuous as vol from homeassistant import config_entries @@ -67,13 +74,6 @@ class ToonFlowHandler(config_entries.ConfigFlow): async def async_step_authenticate(self, user_input=None): """Attempt to authenticate with the Toon account.""" - from toonapilib import Toon - from toonapilib.toonapilibexceptions import ( - InvalidConsumerSecret, - InvalidConsumerKey, - InvalidCredentials, - AgreementsRetrievalError, - ) if user_input is None: return await self._show_authenticaticate_form() @@ -129,7 +129,6 @@ class ToonFlowHandler(config_entries.ConfigFlow): async def async_step_display(self, user_input=None): """Select Toon display to add.""" - from toonapilib import Toon if not self.displays: return self.async_abort(reason="no_displays") diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index 721f7037fce..78c0c6cf57f 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -3,11 +3,7 @@ "name": "Toon", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/toon", - "requirements": [ - "toonapilib==3.2.4" - ], + "requirements": ["toonapilib==3.2.4"], "dependencies": [], - "codeowners": [ - "@frenck" - ] + "codeowners": ["@frenck"] } diff --git a/homeassistant/components/toon/sensor.py b/homeassistant/components/toon/sensor.py index f82bcb7ac1b..79a8fa28540 100644 --- a/homeassistant/components/toon/sensor.py +++ b/homeassistant/components/toon/sensor.py @@ -2,18 +2,18 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.helpers.typing import HomeAssistantType from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT +from homeassistant.helpers.typing import HomeAssistantType from . import ( + ToonBoilerDeviceEntity, ToonData, - ToonEntity, ToonElectricityMeterDeviceEntity, + ToonEntity, ToonGasMeterDeviceEntity, ToonSolarDeviceEntity, - ToonBoilerDeviceEntity, ) -from .const import CURRENCY_EUR, DATA_TOON, DOMAIN, VOLUME_CM3, VOLUME_M3, RATIO_PERCENT +from .const import CURRENCY_EUR, DATA_TOON, DOMAIN, RATIO_PERCENT, VOLUME_CM3, VOLUME_M3 _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/torque/manifest.json b/homeassistant/components/torque/manifest.json index fbc788d252d..14b41ed82de 100644 --- a/homeassistant/components/torque/manifest.json +++ b/homeassistant/components/torque/manifest.json @@ -3,8 +3,6 @@ "name": "Torque", "documentation": "https://www.home-assistant.io/integrations/torque", "requirements": [], - "dependencies": [ - "http" - ], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/torque/sensor.py b/homeassistant/components/torque/sensor.py index 10161856a47..f084c135e47 100644 --- a/homeassistant/components/torque/sensor.py +++ b/homeassistant/components/torque/sensor.py @@ -4,12 +4,12 @@ import re import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_EMAIL, CONF_NAME -from homeassistant.helpers.entity import Entity +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index c14ac36057e..020f2d9c07f 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -1,13 +1,12 @@ """The totalconnect component.""" import logging -import voluptuous as vol from total_connect_client import TotalConnectClient +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import discovery from homeassistant.const import CONF_PASSWORD, CONF_USERNAME - +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index b8b4236806f..ed77fc4eea0 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -119,7 +119,7 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): attr["triggered_source"] = "Carbon Monoxide" else: logging.info( - "Total Connect Client returned unknown " "status code: %s", status + "Total Connect Client returned unknown status code: %s", status ) state = None diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 39cd0e0f1e6..6b2119f1cf5 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -1,10 +1,8 @@ { "domain": "totalconnect", - "name": "Totalconnect", + "name": "Honeywell Total Connect Alarm", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": [ - "total_connect_client==0.28" - ], + "requirements": ["total_connect_client==0.28"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/touchline/manifest.json b/homeassistant/components/touchline/manifest.json index 7c0b36b50fe..95415e70fa1 100644 --- a/homeassistant/components/touchline/manifest.json +++ b/homeassistant/components/touchline/manifest.json @@ -1,10 +1,8 @@ { "domain": "touchline", - "name": "Touchline", + "name": "Roth Touchline", "documentation": "https://www.home-assistant.io/integrations/touchline", - "requirements": [ - "pytouchline==0.7" - ], + "requirements": ["pytouchline==0.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tplink/.translations/da.json b/homeassistant/components/tplink/.translations/da.json index cdd953ff5c3..5225a89fb95 100644 --- a/homeassistant/components/tplink/.translations/da.json +++ b/homeassistant/components/tplink/.translations/da.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere TP-Link smart devices?", + "description": "Vil du konfigurere TP-Link-smartenheder?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/tplink/.translations/ko.json b/homeassistant/components/tplink/.translations/ko.json index 05bebdd1455..89255d78518 100644 --- a/homeassistant/components/tplink/.translations/ko.json +++ b/homeassistant/components/tplink/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "TP-Link \uc2a4\ub9c8\ud2b8 \uae30\uae30\ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 7aa261564f3..764060135a2 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -13,8 +13,8 @@ from .common import ( CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, - CONF_SWITCH, CONF_STRIP, + CONF_SWITCH, SmartDevices, async_discover_devices, get_static_devices, diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 548edc6822c..0e06babbd52 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -32,7 +32,7 @@ class SmartDevices: def __init__( self, lights: List[SmartDevice] = None, switches: List[SmartDevice] = None ): - """Constructor.""" + """Initialize device holder.""" self._lights = lights or [] self._switches = switches or [] diff --git a/homeassistant/components/tplink/const.py b/homeassistant/components/tplink/const.py index 583c25e285c..8b85b8afd74 100644 --- a/homeassistant/components/tplink/const.py +++ b/homeassistant/components/tplink/const.py @@ -1,3 +1,5 @@ """Const for TP-Link.""" +import datetime DOMAIN = "tplink" +MIN_TIME_BETWEEN_UPDATES = datetime.timedelta(seconds=8) diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 117ebf75025..ec3307fc87e 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -126,23 +126,27 @@ class TPLinkSmartBulb(Light): def turn_on(self, **kwargs): """Turn the light on.""" + self._state = True self.smartbulb.state = SmartBulb.BULB_STATE_ON if ATTR_COLOR_TEMP in kwargs: - self.smartbulb.color_temp = mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) + self._color_temp = kwargs.get(ATTR_COLOR_TEMP) + self.smartbulb.color_temp = mired_to_kelvin(self._color_temp) - brightness = brightness_to_percentage( - kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) - ) + brightness_value = kwargs.get(ATTR_BRIGHTNESS, self.brightness or 255) + brightness_pct = brightness_to_percentage(brightness_value) if ATTR_HS_COLOR in kwargs: - hue, sat = kwargs.get(ATTR_HS_COLOR) - hsv = (int(hue), int(sat), brightness) + self._hs = kwargs.get(ATTR_HS_COLOR) + hue, sat = self._hs + hsv = (int(hue), int(sat), brightness_pct) self.smartbulb.hsv = hsv elif ATTR_BRIGHTNESS in kwargs: - self.smartbulb.brightness = brightness + self._brightness = brightness_value + self.smartbulb.brightness = brightness_pct def turn_off(self, **kwargs): """Turn the light off.""" + self._state = False self.smartbulb.state = SmartBulb.BULB_STATE_OFF @property @@ -177,6 +181,15 @@ class TPLinkSmartBulb(Light): def update(self): """Update the TP-Link Bulb's state.""" + if self._supported_features is None: + # First run, update by blocking. + self.do_update() + else: + # Not first run, update in the background. + self.hass.add_job(self.do_update) + + def do_update(self): + """Update states.""" try: if self._supported_features is None: self.get_features() diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index c2a2197c844..8b55ad7da71 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -1,13 +1,9 @@ { "domain": "tplink", - "name": "Tplink", + "name": "TP-Link Kasa Smart", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", - "requirements": [ - "pyHS100==0.3.5" - ], + "requirements": ["pyHS100==0.3.5"], "dependencies": [], - "codeowners": [ - "@rytilahti" - ] + "codeowners": ["@rytilahti"] } diff --git a/homeassistant/components/tplink_lte/manifest.json b/homeassistant/components/tplink_lte/manifest.json index e1cdacde6d8..249fb4db1fa 100644 --- a/homeassistant/components/tplink_lte/manifest.json +++ b/homeassistant/components/tplink_lte/manifest.json @@ -1,10 +1,8 @@ { "domain": "tplink_lte", - "name": "Tplink lte", + "name": "TP-Link LTE", "documentation": "https://www.home-assistant.io/integrations/tplink_lte", - "requirements": [ - "tp-connected==0.0.4" - ], + "requirements": ["tp-connected==0.0.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/traccar/.translations/da.json b/homeassistant/components/traccar/.translations/da.json index af3963f8c0f..b1ab350c905 100644 --- a/homeassistant/components/traccar/.translations/da.json +++ b/homeassistant/components/traccar/.translations/da.json @@ -1,11 +1,11 @@ { "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." + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Traccar-meddelelser.", + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "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." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere webhook-funktionen i Traccar.\n\nBrug f\u00f8lgende webadresse: `{webhook_url}`\n \nSe [dokumentationen]({docs_url}) for yderligere oplysninger." }, "step": { "user": { diff --git a/homeassistant/components/traccar/.translations/ko.json b/homeassistant/components/traccar/.translations/ko.json index d9f31967e68..40e1aaf4d6b 100644 --- a/homeassistant/components/traccar/.translations/ko.json +++ b/homeassistant/components/traccar/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Traccar \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Traccar \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Traccar \uc124\uc815" } }, diff --git a/homeassistant/components/traccar/.translations/tr.json b/homeassistant/components/traccar/.translations/tr.json new file mode 100644 index 00000000000..22944e1c4cc --- /dev/null +++ b/homeassistant/components/traccar/.translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Traccar'\u0131 kur" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index ffc82ee13e8..898113d1b76 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -3,14 +3,7 @@ "name": "Traccar", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/traccar", - "requirements": [ - "pytraccar==0.9.0", - "stringcase==1.2.0" - ], - "dependencies": [ - "webhook" - ], - "codeowners": [ - "@ludeeus" - ] + "requirements": ["pytraccar==0.9.0", "stringcase==1.2.0"], + "dependencies": ["webhook"], + "codeowners": ["@ludeeus"] } diff --git a/homeassistant/components/trackr/manifest.json b/homeassistant/components/trackr/manifest.json index 638d63cb487..6b3368382c8 100644 --- a/homeassistant/components/trackr/manifest.json +++ b/homeassistant/components/trackr/manifest.json @@ -1,10 +1,8 @@ { "domain": "trackr", - "name": "Trackr", + "name": "TrackR", "documentation": "https://www.home-assistant.io/integrations/trackr", - "requirements": [ - "pytrackr==0.0.5" - ], + "requirements": ["pytrackr==0.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json index 2e3dc8331be..7d2925fd3f2 100644 --- a/homeassistant/components/tradfri/.translations/ru.json +++ b/homeassistant/components/tradfri/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json index 229db67becd..7948b96d7e1 100644 --- a/homeassistant/components/tradfri/manifest.json +++ b/homeassistant/components/tradfri/manifest.json @@ -1,6 +1,6 @@ { "domain": "tradfri", - "name": "Tradfri", + "name": "IKEA TRÅDFRI (TRADFRI)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tradfri", "requirements": ["pytradfri[async]==6.4.0"], diff --git a/homeassistant/components/trafikverket_train/manifest.json b/homeassistant/components/trafikverket_train/manifest.json index 1e24895af44..33f634e279f 100644 --- a/homeassistant/components/trafikverket_train/manifest.json +++ b/homeassistant/components/trafikverket_train/manifest.json @@ -1,12 +1,8 @@ { "domain": "trafikverket_train", - "name": "Trafikverket train information", + "name": "Trafikverket Train", "documentation": "https://www.home-assistant.io/integrations/trafikverket_train", - "requirements": [ - "pytrafikverket==0.1.5.9" - ], + "requirements": ["pytrafikverket==0.1.5.9"], "dependencies": [], - "codeowners": [ - "@endor-force" - ] -} \ No newline at end of file + "codeowners": ["@endor-force"] +} diff --git a/homeassistant/components/trafikverket_train/sensor.py b/homeassistant/components/trafikverket_train/sensor.py index e6789ca5aee..12f3cf73e50 100644 --- a/homeassistant/components/trafikverket_train/sensor.py +++ b/homeassistant/components/trafikverket_train/sensor.py @@ -11,8 +11,8 @@ from homeassistant.const import ( CONF_API_KEY, CONF_NAME, CONF_WEEKDAY, - WEEKDAYS, DEVICE_CLASS_TIMESTAMP, + WEEKDAYS, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/trafikverket_weatherstation/manifest.json b/homeassistant/components/trafikverket_weatherstation/manifest.json index 64f1d636a1a..652cebf6730 100644 --- a/homeassistant/components/trafikverket_weatherstation/manifest.json +++ b/homeassistant/components/trafikverket_weatherstation/manifest.json @@ -1,10 +1,8 @@ { "domain": "trafikverket_weatherstation", - "name": "Trafikverket weatherstation", + "name": "Trafikverket Weather Station", "documentation": "https://www.home-assistant.io/integrations/trafikverket_weatherstation", - "requirements": [ - "pytrafikverket==0.1.5.9" - ], + "requirements": ["pytrafikverket==0.1.5.9"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json index b14fca00c2c..e84ec938ee2 100644 --- a/homeassistant/components/transmission/.translations/da.json +++ b/homeassistant/components/transmission/.translations/da.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "V\u00e6rten er allerede konfigureret.", - "one_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning." + "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", @@ -14,7 +14,7 @@ "data": { "scan_interval": "Opdateringsfrekvens" }, - "title": "Konfigurer indstillinger" + "title": "Konfigurationsmuligheder" }, "user": { "data": { @@ -24,7 +24,7 @@ "port": "Port", "username": "Brugernavn" }, - "title": "Konfigurer Transmission klient" + "title": "Konfigurer Transmission-klient" } }, "title": "Transmission" diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json index 1847c7186db..4c0a3146eb8 100644 --- a/homeassistant/components/transmission/.translations/de.json +++ b/homeassistant/components/transmission/.translations/de.json @@ -35,7 +35,8 @@ "data": { "scan_interval": "Aktualisierungsfrequenz" }, - "description": "Konfigurieren von Optionen f\u00fcr Transmission" + "description": "Konfigurieren von Optionen f\u00fcr Transmission", + "title": "Konfigurieren Sie die Optionen f\u00fcr die \u00dcbertragung" } } } diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 7bbc61a192f..3e6f2407d17 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -22,12 +22,12 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( ATTR_TORRENT, + DATA_UPDATED, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN, SERVICE_ADD_TORRENT, - DATA_UPDATED, ) from .errors import AuthenticationError, CannotConnect, UnknownError diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 5540f718ba1..9a9250dbed6 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -2,14 +2,14 @@ DOMAIN = "transmission" SENSOR_TYPES = { - "active_torrents": ["Active Torrents", None], + "active_torrents": ["Active Torrents", "Torrents"], "current_status": ["Status", None], "download_speed": ["Down Speed", "MB/s"], - "paused_torrents": ["Paused Torrents", None], - "total_torrents": ["Total Torrents", None], + "paused_torrents": ["Paused Torrents", "Torrents"], + "total_torrents": ["Total Torrents", "Torrents"], "upload_speed": ["Up Speed", "MB/s"], - "completed_torrents": ["Completed Torrents", None], - "started_torrents": ["Started Torrents", None], + "completed_torrents": ["Completed Torrents", "Torrents"], + "started_torrents": ["Started Torrents", "Torrents"], } SWITCH_TYPES = {"on_off": "Switch", "turtle_mode": "Turtle Mode"} diff --git a/homeassistant/components/transmission/manifest.json b/homeassistant/components/transmission/manifest.json index 9618a5677ad..117dd3cc246 100644 --- a/homeassistant/components/transmission/manifest.json +++ b/homeassistant/components/transmission/manifest.json @@ -3,12 +3,7 @@ "name": "Transmission", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/transmission", - "requirements": [ - "transmissionrpc==0.11" - ], + "requirements": ["transmissionrpc==0.11"], "dependencies": [], - "codeowners": [ - "@engrbm87", - "@JPHutchins" - ] + "codeowners": ["@engrbm87", "@JPHutchins"] } diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index c51d48eb532..6bedc793ed9 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -8,7 +8,6 @@ from homeassistant.helpers.entity import Entity from .const import DOMAIN, SENSOR_TYPES, STATE_ATTR_TORRENT_INFO - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/transport_nsw/manifest.json b/homeassistant/components/transport_nsw/manifest.json index 9d2e675c757..34baf54c9ff 100644 --- a/homeassistant/components/transport_nsw/manifest.json +++ b/homeassistant/components/transport_nsw/manifest.json @@ -1,10 +1,8 @@ { "domain": "transport_nsw", - "name": "Transport nsw", + "name": "Transport NSW", "documentation": "https://www.home-assistant.io/integrations/transport_nsw", - "requirements": [ - "PyTransportNSW==0.1.1" - ], + "requirements": ["PyTransportNSW==0.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 79df41ac489..7c6990de085 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -2,13 +2,13 @@ from datetime import timedelta import logging -import voluptuous as vol from TransportNSW import TransportNSW +import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_MODE, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_MODE, CONF_NAME, CONF_API_KEY, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/travisci/manifest.json b/homeassistant/components/travisci/manifest.json index f0d5d54c8cf..3dba3733f70 100644 --- a/homeassistant/components/travisci/manifest.json +++ b/homeassistant/components/travisci/manifest.json @@ -1,10 +1,8 @@ { "domain": "travisci", - "name": "Travisci", + "name": "Travis-CI", "documentation": "https://www.home-assistant.io/integrations/travisci", - "requirements": [ - "TravisPy==0.3.5" - ], + "requirements": ["TravisPy==0.3.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 8842b03b594..2b9e7a4eccf 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/trend", "requirements": ["numpy==1.17.4"], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/tts/manifest.json b/homeassistant/components/tts/manifest.json index cb780523977..215a16fd4cf 100644 --- a/homeassistant/components/tts/manifest.json +++ b/homeassistant/components/tts/manifest.json @@ -1,14 +1,9 @@ { "domain": "tts", - "name": "Tts", + "name": "Text-to-Speech (TTS)", "documentation": "https://www.home-assistant.io/integrations/tts", - "requirements": [ - "mutagen==1.43.0" - ], - "dependencies": [ - "http" - ], - "codeowners": [ - "@robbiet480" - ] + "requirements": ["mutagen==1.43.0"], + "dependencies": ["http"], + "after_dependencies": ["media_player"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 6450920b806..eb0ef5eca2f 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -1,6 +1,9 @@ """Support for the Tuya climate devices.""" from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_FAN_ONLY, @@ -9,7 +12,6 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.fan import SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM from homeassistant.const import ( ATTR_TEMPERATURE, PRECISION_WHOLE, @@ -30,7 +32,7 @@ HA_STATE_TO_TUYA = { TUYA_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_TUYA.items()} -FAN_MODES = {SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH} +FAN_MODES = {FAN_LOW, FAN_MEDIUM, FAN_HIGH} def setup_platform(hass, config, add_entities, discovery_info=None): diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index cf16d587e87..e249fb3f89f 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -2,9 +2,7 @@ "domain": "tuya", "name": "Tuya", "documentation": "https://www.home-assistant.io/integrations/tuya", - "requirements": [ - "tuyaha==0.0.4" - ], + "requirements": ["tuyaha==0.0.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/twentemilieu/.translations/tr.json b/homeassistant/components/twentemilieu/.translations/tr.json new file mode 100644 index 00000000000..ebe13a37003 --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "address_exists": "Adres zaten kurulmu\u015f." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index 4eebba28ef6..9444e33700e 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -1,13 +1,9 @@ { - "domain": "twentemilieu", - "name": "Twente Milieu", - "config_flow": true, - "documentation": "https://www.home-assistant.io/integrations/twentemilieu", - "requirements": [ - "twentemilieu==0.1.0" - ], - "dependencies": [], - "codeowners": [ - "@frenck" - ] -} \ No newline at end of file + "domain": "twentemilieu", + "name": "Twente Milieu", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/twentemilieu", + "requirements": ["twentemilieu==0.2.0"], + "dependencies": [], + "codeowners": ["@frenck"] +} diff --git a/homeassistant/components/twilio/.translations/da.json b/homeassistant/components/twilio/.translations/da.json index 3c1ab7c01b5..d5f40d56446 100644 --- a/homeassistant/components/twilio/.translations/da.json +++ b/homeassistant/components/twilio/.translations/da.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "not_internet_accessible": "Dit Home Assistant system skal v\u00e6re tilg\u00e6ngeligt fra internettet for at modtage Twilio meddelelser.", + "not_internet_accessible": "Din Home Assistant-instans skal v\u00e6re tilg\u00e6ngelig fra internettet for at modtage Twilio-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 [Webhooks med Twilio]({twilio_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - URL: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/x-www-form-urlencoded\n\n Se [dokumentationen]({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." + "default": "For at sende h\u00e6ndelser til Home Assistant skal du konfigurere [Webhooks med Twilio]({twilio_url}).\n\n Udfyld f\u00f8lgende oplysninger: \n\n - Webadresse: `{webhook_url}`\n - Metode: POST\n - Indholdstype: application/x-www-form-urlencoded\n\nSe [dokumentationen]({docs_url}) om hvordan du konfigurerer automatiseringer til at h\u00e5ndtere indg\u00e5ende data." }, "step": { "user": { diff --git a/homeassistant/components/twilio/.translations/ko.json b/homeassistant/components/twilio/.translations/ko.json index 4e4c80801d4..b8e88820590 100644 --- a/homeassistant/components/twilio/.translations/ko.json +++ b/homeassistant/components/twilio/.translations/ko.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Twilio \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Twilio \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Twilio Webhook \uc124\uc815" } }, diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index 8f4ed125fb6..c0b44995281 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -3,11 +3,7 @@ "name": "Twilio", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twilio", - "requirements": [ - "twilio==6.32.0" - ], - "dependencies": [ - "webhook" - ], + "requirements": ["twilio==6.32.0"], + "dependencies": ["webhook"], "codeowners": [] } diff --git a/homeassistant/components/twilio_call/manifest.json b/homeassistant/components/twilio_call/manifest.json index 3fe8f129b5d..626a8fea89a 100644 --- a/homeassistant/components/twilio_call/manifest.json +++ b/homeassistant/components/twilio_call/manifest.json @@ -1,12 +1,8 @@ { "domain": "twilio_call", - "name": "Twilio call", + "name": "Twilio Call", "documentation": "https://www.home-assistant.io/integrations/twilio_call", "requirements": [], - "dependencies": [ - "twilio" - ], - "codeowners": [ - "@robbiet480" - ] + "dependencies": ["twilio"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/twilio_sms/manifest.json b/homeassistant/components/twilio_sms/manifest.json index 00c843d2dff..35d549c5268 100644 --- a/homeassistant/components/twilio_sms/manifest.json +++ b/homeassistant/components/twilio_sms/manifest.json @@ -1,12 +1,8 @@ { "domain": "twilio_sms", - "name": "Twilio sms", + "name": "Twilio SMS", "documentation": "https://www.home-assistant.io/integrations/twilio_sms", "requirements": [], - "dependencies": [ - "twilio" - ], - "codeowners": [ - "@robbiet480" - ] + "dependencies": ["twilio"], + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/twitch/manifest.json b/homeassistant/components/twitch/manifest.json index f64182f6e4d..639624c352f 100644 --- a/homeassistant/components/twitch/manifest.json +++ b/homeassistant/components/twitch/manifest.json @@ -2,9 +2,7 @@ "domain": "twitch", "name": "Twitch", "documentation": "https://www.home-assistant.io/integrations/twitch", - "requirements": [ - "python-twitch-client==0.6.0" - ], + "requirements": ["python-twitch-client==0.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index d63259bcbee..514f976df34 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,9 +2,7 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": [ - "TwitterAPI==2.5.10" - ], + "requirements": ["TwitterAPI==2.5.10"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/twitter/notify.py b/homeassistant/components/twitter/notify.py index 39faf987ae0..768e1ee7316 100644 --- a/homeassistant/components/twitter/notify.py +++ b/homeassistant/components/twitter/notify.py @@ -9,15 +9,14 @@ import os from TwitterAPI import TwitterAPI import voluptuous as vol -from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_point_in_time - from homeassistant.components.notify import ( ATTR_DATA, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_USERNAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_point_in_time _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json index 0bf29beb0dc..920937142a2 100644 --- a/homeassistant/components/ubee/manifest.json +++ b/homeassistant/components/ubee/manifest.json @@ -1,10 +1,8 @@ { "domain": "ubee", - "name": "Ubee", + "name": "Ubee Router", "documentation": "https://www.home-assistant.io/integrations/ubee", - "requirements": [ - "pyubee==0.7" - ], + "requirements": ["pyubee==0.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/ubus/manifest.json b/homeassistant/components/ubus/manifest.json index 664ae861442..d48e55d5e2a 100644 --- a/homeassistant/components/ubus/manifest.json +++ b/homeassistant/components/ubus/manifest.json @@ -1,6 +1,6 @@ { "domain": "ubus", - "name": "Ubus", + "name": "OpenWrt (ubus)", "documentation": "https://www.home-assistant.io/integrations/ubus", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/ue_smart_radio/manifest.json b/homeassistant/components/ue_smart_radio/manifest.json index 3711d7cbeb4..7ddb8d69284 100644 --- a/homeassistant/components/ue_smart_radio/manifest.json +++ b/homeassistant/components/ue_smart_radio/manifest.json @@ -1,6 +1,6 @@ { "domain": "ue_smart_radio", - "name": "Ue smart radio", + "name": "Logitech UE Smart Radio", "documentation": "https://www.home-assistant.io/integrations/ue_smart_radio", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/ue_smart_radio/media_player.py b/homeassistant/components/ue_smart_radio/media_player.py index ae54eb76d72..d25c52608e1 100644 --- a/homeassistant/components/ue_smart_radio/media_player.py +++ b/homeassistant/components/ue_smart_radio/media_player.py @@ -5,7 +5,7 @@ import logging import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, diff --git a/homeassistant/components/uk_transport/manifest.json b/homeassistant/components/uk_transport/manifest.json index cf349a20571..a9924715373 100644 --- a/homeassistant/components/uk_transport/manifest.json +++ b/homeassistant/components/uk_transport/manifest.json @@ -1,6 +1,6 @@ { "domain": "uk_transport", - "name": "Uk transport", + "name": "UK Transport", "documentation": "https://www.home-assistant.io/integrations/uk_transport", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/uk_transport/sensor.py b/homeassistant/components/uk_transport/sensor.py index eb325d32212..e3c5440c450 100644 --- a/homeassistant/components/uk_transport/sensor.py +++ b/homeassistant/components/uk_transport/sensor.py @@ -3,19 +3,19 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.uk_transport/ """ +from datetime import datetime, timedelta import logging import re -from datetime import datetime, timedelta 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 +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 0d0315e49c7..46a94cc4047 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -33,9 +33,15 @@ "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" } }, + "init": { + "data": { + "one": "EN", + "other": "ANDEN" + } + }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Opret b\u00e5ndbredde sensorer for netv\u00e6rksklienter" + "allow_bandwidth_sensors": "Opret b\u00e5ndbredde-forbrugssensorer for netv\u00e6rksklienter" } } } diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index b01cdb84fbf..3a67d483c0c 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -33,6 +33,14 @@ "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" } }, + "init": { + "data": { + "few": "\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e", + "many": "\u043c\u043d\u043e\u0433\u043e", + "one": "\u043e\u0434\u043d\u0438", + "other": "\u0434\u0440\u0443\u0433\u0438\u0435" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 4f3edf9ce79..65015b357a7 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -3,9 +3,8 @@ import voluptuous as vol from homeassistant.const import CONF_HOST from homeassistant.core import callback -from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC - import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .config_flow import get_controller_id_from_config_entry from .const import ( diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 01b97a78366..52ecab08856 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -10,6 +9,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import callback from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -21,10 +21,10 @@ from .const import ( CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_DETECTION_TIME, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, - DEFAULT_DETECTION_TIME, DOMAIN, LOGGER, ) diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 3deb2e9040a..826491f6ba6 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,16 +1,14 @@ """UniFi Controller abstraction.""" -from datetime import timedelta - import asyncio +from datetime import timedelta import ssl -import async_timeout from aiohttp import CookieJar - import aiounifi +import async_timeout -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.const import CONF_HOST +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -22,19 +20,19 @@ from .const import ( CONF_DONT_TRACK_CLIENTS, CONF_DONT_TRACK_DEVICES, CONF_DONT_TRACK_WIRED_CLIENTS, + CONF_SITE_ID, + CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONF_SITE_ID, - CONF_SSID_FILTER, CONTROLLER_ID, DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_BLOCK_CLIENTS, + DEFAULT_DETECTION_TIME, + DEFAULT_SSID_FILTER, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, - DEFAULT_DETECTION_TIME, - DEFAULT_SSID_FILTER, DOMAIN, LOGGER, UNIFI_CONFIG, diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index e449d5035b7..8b45a0f227b 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -2,16 +2,15 @@ import logging from pprint import pformat -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry 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.entity_registry import DISABLED_CONFIG_ENTRY - import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index ecbeb002f04..e2bcd5b68a5 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -1,13 +1,10 @@ { "domain": "unifi", - "name": "Unifi", + "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": [ - "aiounifi==11" - ], + "requirements": ["aiounifi==11"], "dependencies": [], - "codeowners": [ - "@kane610" - ] + "codeowners": ["@kane610"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 45f74f8882f..b1f62131eb4 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -2,8 +2,8 @@ import logging from pprint import pformat -from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.components.switch import SwitchDevice +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry from homeassistant.core import callback from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -42,12 +42,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _, mac = entity.unique_id.split("-", 1) - if mac in controller.api.clients or mac not in controller.api.clients_all: + if mac in controller.api.clients: + switches_off.append(entity.unique_id) continue - client = controller.api.clients_all[mac] - controller.api.clients.process_raw([client.raw]) - switches_off.append(entity.unique_id) + if mac in controller.api.clients_all: + client = controller.api.clients_all[mac] + controller.api.clients.process_raw([client.raw]) + switches_off.append(entity.unique_id) + continue @callback def update_controller(): diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index 805dc6638bb..3de376a831d 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -1,10 +1,8 @@ { "domain": "unifi_direct", - "name": "Unifi direct", + "name": "Ubiquiti UniFi AP", "documentation": "https://www.home-assistant.io/integrations/unifi_direct", - "requirements": [ - "pexpect==4.6.0" - ], + "requirements": ["pexpect==4.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/unifiled/manifest.json b/homeassistant/components/unifiled/manifest.json index 927798bd9ce..a031b8b2ec3 100644 --- a/homeassistant/components/unifiled/manifest.json +++ b/homeassistant/components/unifiled/manifest.json @@ -1,6 +1,6 @@ { "domain": "unifiled", - "name": "Unifi LED", + "name": "Ubiquiti UniFi LED", "documentation": "https://www.home-assistant.io/integrations/unifiled", "dependencies": [], "codeowners": ["@florisvdk"], diff --git a/homeassistant/components/universal/manifest.json b/homeassistant/components/universal/manifest.json index 3e066b48598..43acbadb450 100644 --- a/homeassistant/components/universal/manifest.json +++ b/homeassistant/components/universal/manifest.json @@ -1,8 +1,9 @@ { "domain": "universal", - "name": "Universal", + "name": "Universal Media Player", "documentation": "https://www.home-assistant.io/integrations/universal", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/universal/media_player.py b/homeassistant/components/universal/media_player.py index 63e3ff7448d..37d4cf138f2 100644 --- a/homeassistant/components/universal/media_player.py +++ b/homeassistant/components/universal/media_player.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_APP_ID, ATTR_APP_NAME, diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index cd5d327f2c2..904b48fbdd9 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -1,6 +1,6 @@ { "domain": "upc_connect", - "name": "Upc connect", + "name": "UPC Connect Box", "documentation": "https://www.home-assistant.io/integrations/upc_connect", "requirements": ["connect-box==0.2.5"], "dependencies": [], diff --git a/homeassistant/components/upcloud/manifest.json b/homeassistant/components/upcloud/manifest.json index 0499ce1e9ad..14ad0359364 100644 --- a/homeassistant/components/upcloud/manifest.json +++ b/homeassistant/components/upcloud/manifest.json @@ -2,11 +2,7 @@ "domain": "upcloud", "name": "UpCloud", "documentation": "https://www.home-assistant.io/integrations/upcloud", - "requirements": [ - "upcloud-api==0.4.5" - ], + "requirements": ["upcloud-api==0.4.5"], "dependencies": [], - "codeowners": [ - "@scop" - ] + "codeowners": ["@scop"] } diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 08f08e1bb64..42eb988ed56 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -1,8 +1,6 @@ """Support to check for available updates.""" import asyncio from datetime import timedelta - -# pylint: disable=import-error from distutils.version import StrictVersion import json import logging @@ -10,15 +8,14 @@ import uuid import aiohttp import async_timeout -from distro import linux_distribution +from distro import linux_distribution # pylint: disable=import-error import voluptuous as vol from homeassistant.const import __version__ as current_version -from homeassistant.helpers import event +from homeassistant.helpers import discovery, event from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -112,7 +109,7 @@ async def async_setup(hass, config): if newest is None or "dev" in current_version: return - # Load data from supervisor on hass.io + # Load data from supervisor on Hass.io if hass.components.hassio.is_hassio(): newest = hass.components.hassio.get_homeassistant_version() diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py index cae3ae32e3c..3e026a87d4d 100644 --- a/homeassistant/components/updater/binary_sensor.py +++ b/homeassistant/components/updater/binary_sensor.py @@ -1,7 +1,7 @@ """Support for Home Assistant Updater binary sensors.""" -from homeassistant.core import callback from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater diff --git a/homeassistant/components/updater/manifest.json b/homeassistant/components/updater/manifest.json index eb26d6e36b7..377ca24cd38 100644 --- a/homeassistant/components/updater/manifest.json +++ b/homeassistant/components/updater/manifest.json @@ -2,11 +2,8 @@ "domain": "updater", "name": "Updater", "documentation": "https://www.home-assistant.io/integrations/updater", - "requirements": [ - "distro==1.4.0" - ], + "requirements": ["distro==1.4.0"], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/upnp/.translations/da.json b/homeassistant/components/upnp/.translations/da.json index 1d0097c2f1f..c41741b8635 100644 --- a/homeassistant/components/upnp/.translations/da.json +++ b/homeassistant/components/upnp/.translations/da.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "UPnP/IGD er allerede konfigureret", "incomplete_device": "Ignorerer ufuldst\u00e6ndig UPnP-enhed", - "no_devices_discovered": "Ingen UPnP/IGD enheder fundet.", + "no_devices_discovered": "Ingen UPnP/IGD-enheder fundet.", "no_devices_found": "Ingen UPnP/IGD enheder kunne findes p\u00e5 netv\u00e6rket.", "no_sensors_or_port_mapping": "Aktiv\u00e9r enten sensorer eller porttilknytning", "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af UPnP/IGD." @@ -23,7 +23,7 @@ "user": { "data": { "enable_port_mapping": "Aktiv\u00e9r porttilknytning til Home Assistent", - "enable_sensors": "Tilf\u00f8j trafik sensorer", + "enable_sensors": "Tilf\u00f8j trafiksensorer", "igd": "UPnP/IGD" }, "title": "Konfigurationsindstillinger for UPnP/IGD" diff --git a/homeassistant/components/upnp/.translations/ko.json b/homeassistant/components/upnp/.translations/ko.json index d846a5e38ce..bd6aaeef4e2 100644 --- a/homeassistant/components/upnp/.translations/ko.json +++ b/homeassistant/components/upnp/.translations/ko.json @@ -10,7 +10,7 @@ }, "step": { "confirm": { - "description": "UPnP/IGD \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "UPnP/IGD \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "UPnP/IGD" }, "init": { diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 9599832799f..6dce1b3d76c 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -8,6 +8,12 @@ "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432.", "single_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." }, + "error": { + "few": "\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e", + "many": "\u043c\u043d\u043e\u0433\u043e", + "one": "\u043e\u0434\u0438\u043d", + "other": "\u0434\u0440\u0443\u0433\u0438\u0435" + }, "step": { "confirm": { "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 UPnP / IGD?", diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index fffee57b411..b144d2b96ed 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,4 +1,4 @@ -"""Hass representation of an UPnP/IGD.""" +"""Home Assistant representation of an UPnP/IGD.""" import asyncio from ipaddress import IPv4Address @@ -14,16 +14,16 @@ from .const import CONF_LOCAL_IP, DOMAIN, LOGGER as _LOGGER class Device: - """Hass representation of an UPnP/IGD.""" + """Home Assistant representation of an UPnP/IGD.""" def __init__(self, igd_device): - """Initializer.""" + """Initialize UPnP/IGD device.""" self._igd_device = igd_device self._mapped_ports = [] @classmethod async def async_discover(cls, hass: HomeAssistantType): - """Discovery UPNP/IGD devices.""" + """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") local_ip = None if DOMAIN in hass.data and "config" in hass.data[DOMAIN]: diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index a78fec3610e..1e55d60f95e 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -1,13 +1,9 @@ { "domain": "upnp", - "name": "Upnp", + "name": "UPnP", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": [ - "async-upnp-client==0.14.12" - ], + "requirements": ["async-upnp-client==0.14.12"], "dependencies": [], - "codeowners": [ - "@robbiet480" - ] + "codeowners": ["@robbiet480"] } diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 4c85e904b1d..81fd5c025b9 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -126,6 +126,9 @@ class RawUPnPIGDSensor(UpnpSensor): @property def state(self) -> str: """Return the state of the device.""" + if self._state is None: + return None + return format(self._state, "d") @property @@ -154,7 +157,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor): """Abstract representation of a X Sent/Received per second sensor.""" def __init__(self, device, direction): - """Initializer.""" + """Initialize sensor.""" super().__init__(device) self._direction = direction @@ -189,7 +192,7 @@ class PerSecondUPnPIGDSensor(UpnpSensor): @property def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" - return f"{self.unit}/sec" + return f"{self.unit}/s" def _is_overflowed(self, new_value) -> bool: """Check if value has overflowed.""" @@ -222,7 +225,7 @@ class KBytePerSecondUPnPIGDSensor(PerSecondUPnPIGDSensor): @property def unit(self) -> str: """Get unit we are measuring in.""" - return "kbyte" + return "kB" async def _async_fetch_value(self) -> float: """Fetch value from device.""" diff --git a/homeassistant/components/uptime/manifest.json b/homeassistant/components/uptime/manifest.json index 5997916e2c3..4d42d2e5bcb 100644 --- a/homeassistant/components/uptime/manifest.json +++ b/homeassistant/components/uptime/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/uptime", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "quality_scale": "internal" } diff --git a/homeassistant/components/uptimerobot/manifest.json b/homeassistant/components/uptimerobot/manifest.json index cc2d1b6c2e8..c835dd425fd 100644 --- a/homeassistant/components/uptimerobot/manifest.json +++ b/homeassistant/components/uptimerobot/manifest.json @@ -1,12 +1,8 @@ { "domain": "uptimerobot", - "name": "Uptimerobot", + "name": "Uptime Robot", "documentation": "https://www.home-assistant.io/integrations/uptimerobot", - "requirements": [ - "pyuptimerobot==0.0.5" - ], + "requirements": ["pyuptimerobot==0.0.5"], "dependencies": [], - "codeowners": [ - "@ludeeus" - ] + "codeowners": ["@ludeeus"] } diff --git a/homeassistant/components/uscis/manifest.json b/homeassistant/components/uscis/manifest.json index 707b7f27860..4a1b26d4e7a 100644 --- a/homeassistant/components/uscis/manifest.json +++ b/homeassistant/components/uscis/manifest.json @@ -1,10 +1,8 @@ { "domain": "uscis", - "name": "Uscis", + "name": "U.S. Citizenship and Immigration Services (USCIS)", "documentation": "https://www.home-assistant.io/integrations/uscis", - "requirements": [ - "uscisstatus==0.1.1" - ], + "requirements": ["uscisstatus==0.1.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index 3f5175ad09d..270023bf99a 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -1,16 +1,15 @@ """Support for USCIS Case Status.""" -import logging from datetime import timedelta +import logging import uscisstatus import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_FRIENDLY_NAME +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_FRIENDLY_NAME - _LOGGER = logging.getLogger(__name__) @@ -25,13 +24,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the platform in HASS and Case Information.""" + """Set up the platform in Home Assistant and Case Information.""" uscis = UscisSensor(config["case_id"], config[CONF_FRIENDLY_NAME]) uscis.update() if uscis.valid_case_id: add_entities([uscis]) else: - _LOGGER.error("Setup USCIS Sensor Fail" " check if your Case ID is Valid") + _LOGGER.error("Setup USCIS Sensor Fail check if your Case ID is Valid") class UscisSensor(Entity): diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index d1ae97b550a..5e4dbba3fe4 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -1,12 +1,8 @@ { "domain": "usgs_earthquakes_feed", - "name": "Usgs earthquakes feed", + "name": "U.S. Geological Survey Earthquake Hazards (USGS)", "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", - "requirements": [ - "geojson_client==0.4" - ], + "requirements": ["geojson_client==0.4"], "dependencies": [], - "codeowners": [ - "@exxamalte" - ] + "codeowners": ["@exxamalte"] } diff --git a/homeassistant/components/utility_meter/__init__.py b/homeassistant/components/utility_meter/__init__.py index 04e472a7828..ef9d9b1ddce 100644 --- a/homeassistant/components/utility_meter/__init__.py +++ b/homeassistant/components/utility_meter/__init__.py @@ -1,33 +1,34 @@ """Support for tracking consumption over given periods of time.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_NAME -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN + from .const import ( - DOMAIN, - SIGNAL_RESET_METER, - METER_TYPES, - CONF_METER_TYPE, - CONF_METER_OFFSET, - CONF_METER_NET_CONSUMPTION, - CONF_SOURCE_SENSOR, - CONF_TARIFF_ENTITY, - CONF_TARIFF, - CONF_TARIFFS, - CONF_METER, - DATA_UTILITY, - SERVICE_RESET, - SERVICE_SELECT_TARIFF, - SERVICE_SELECT_NEXT_TARIFF, ATTR_TARIFF, + CONF_METER, + CONF_METER_NET_CONSUMPTION, + CONF_METER_OFFSET, + CONF_METER_TYPE, + CONF_SOURCE_SENSOR, + CONF_TARIFF, + CONF_TARIFF_ENTITY, + CONF_TARIFFS, + DATA_UTILITY, + DOMAIN, + METER_TYPES, + SERVICE_RESET, + SERVICE_SELECT_NEXT_TARIFF, + SERVICE_SELECT_TARIFF, + SIGNAL_RESET_METER, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/utility_meter/const.py b/homeassistant/components/utility_meter/const.py index 87721dfbf81..23d39204f9c 100644 --- a/homeassistant/components/utility_meter/const.py +++ b/homeassistant/components/utility_meter/const.py @@ -5,9 +5,10 @@ HOURLY = "hourly" DAILY = "daily" WEEKLY = "weekly" MONTHLY = "monthly" +QUARTERLY = "quarterly" YEARLY = "yearly" -METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY] +METER_TYPES = [HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY] DATA_UTILITY = "utility_meter_data" diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index 7a470037ba4..b71bb324773 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -1,10 +1,9 @@ { "domain": "utility_meter", - "name": "Utility meter", + "name": "Utility Meter", "documentation": "https://www.home-assistant.io/integrations/utility_meter", "requirements": [], "dependencies": [], - "codeowners": [ - "@dgomes" - ] + "codeowners": ["@dgomes"], + "quality_scale": "internal" } diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 1ad4300b28b..3dab92b89f8 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -1,38 +1,40 @@ """Utility meter from sensors providing raw data.""" -import logging from datetime import date, timedelta from decimal import Decimal, DecimalException +import logging -import homeassistant.util.dt as dt_util from homeassistant.const import ( - CONF_NAME, ATTR_UNIT_OF_MEASUREMENT, + CONF_NAME, EVENT_HOMEASSISTANT_START, - STATE_UNKNOWN, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import ( async_track_state_change, async_track_time_change, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util + from .const import ( - DATA_UTILITY, - SIGNAL_RESET_METER, - HOURLY, - DAILY, - WEEKLY, - MONTHLY, - YEARLY, - CONF_SOURCE_SENSOR, - CONF_METER_TYPE, - CONF_METER_OFFSET, + CONF_METER, CONF_METER_NET_CONSUMPTION, + CONF_METER_OFFSET, + CONF_METER_TYPE, + CONF_SOURCE_SENSOR, CONF_TARIFF, CONF_TARIFF_ENTITY, - CONF_METER, + DAILY, + DATA_UTILITY, + HOURLY, + MONTHLY, + QUARTERLY, + SIGNAL_RESET_METER, + WEEKLY, + YEARLY, ) _LOGGER = logging.getLogger(__name__) @@ -184,6 +186,12 @@ class UtilityMeterSensor(RestoreEntity): and now != date(now.year, now.month, 1) + self._period_offset ): return + if ( + self._period == QUARTERLY + and now + != date(now.year, (((now.month - 1) // 3) * 3 + 1), 1) + self._period_offset + ): + return if self._period == YEARLY and now != date(now.year, 1, 1) + self._period_offset: return await self.async_reset_meter(self._tariff_entity) @@ -209,7 +217,7 @@ class UtilityMeterSensor(RestoreEntity): minute=self._period_offset.seconds // 60, second=self._period_offset.seconds % 60, ) - elif self._period in [DAILY, WEEKLY, MONTHLY, YEARLY]: + elif self._period in [DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY]: async_track_time_change( self.hass, self._async_reset_meter, diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index b9a6262cd4f..cd6875cdcdc 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -6,7 +6,7 @@ import requests from uvcclient import camera as uvc_camera, nvr import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import CONF_PORT, CONF_SSL from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -92,6 +92,17 @@ class UnifiVideoCamera(Camera): """Return the name of this camera.""" return self._name + @property + def supported_features(self): + """Return supported features.""" + caminfo = self._nvr.get_camera(self._uuid) + channels = caminfo["channels"] + for channel in channels: + if channel["isRtspEnabled"]: + return SUPPORT_STREAM + + return 0 + @property def is_recording(self): """Return true if the camera is recording.""" @@ -199,3 +210,13 @@ class UnifiVideoCamera(Camera): def disable_motion_detection(self): """Disable motion detection in camera.""" self.set_motion_detection(False) + + async def stream_source(self): + """Return the source of the stream.""" + caminfo = self._nvr.get_camera(self._uuid) + channels = caminfo["channels"] + for channel in channels: + if channel["isRtspEnabled"]: + return channel["rtspUris"][0] + + return None diff --git a/homeassistant/components/uvc/manifest.json b/homeassistant/components/uvc/manifest.json index 497bdac656e..7c29edd51c6 100644 --- a/homeassistant/components/uvc/manifest.json +++ b/homeassistant/components/uvc/manifest.json @@ -1,10 +1,8 @@ { "domain": "uvc", - "name": "Uvc", + "name": "Ubiquiti UniFi Video", "documentation": "https://www.home-assistant.io/integrations/uvc", - "requirements": [ - "uvcclient==0.11.0" - ], + "requirements": ["uvcclient==0.11.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/vacuum/.translations/bg.json b/homeassistant/components/vacuum/.translations/bg.json index 2d422284a38..1ab7fce7abe 100644 --- a/homeassistant/components/vacuum/.translations/bg.json +++ b/homeassistant/components/vacuum/.translations/bg.json @@ -4,7 +4,7 @@ "clean": "\u041d\u0435\u043a\u0430 {entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0438", "dock": "\u041d\u0435\u043a\u0430 {entity_name} \u0434\u0430 \u0441\u0435 \u0432\u044a\u0440\u043d\u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u043f\u043e\u0447\u0438\u0441\u0442\u0432\u0430", "is_docked": "{entity_name} \u0435 \u0432 \u0431\u0430\u0437\u043e\u0432\u0430\u0442\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f" }, diff --git a/homeassistant/components/vacuum/.translations/ca.json b/homeassistant/components/vacuum/.translations/ca.json index ee69152ed5c..b3cdbb2f6c7 100644 --- a/homeassistant/components/vacuum/.translations/ca.json +++ b/homeassistant/components/vacuum/.translations/ca.json @@ -4,7 +4,7 @@ "clean": "Fes que {entity_name} netegi", "dock": "Fes que {entity_name} torni a la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e0 netejant", "is_docked": "{entity_name} est\u00e0 acoblada" }, diff --git a/homeassistant/components/vacuum/.translations/da.json b/homeassistant/components/vacuum/.translations/da.json new file mode 100644 index 00000000000..fac748ca464 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/da.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "Lad {entity_name} g\u00f8re rent", + "dock": "Lad {entity_name} vende tilbage til dock" + }, + "condition_type": { + "is_cleaning": "{entity_name} g\u00f8r rent", + "is_docked": "{entity_name} er i dock" + }, + "trigger_type": { + "cleaning": "{entity_name} begyndte at reng\u00f8re", + "docked": "{entity_name} er i dock" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/de.json b/homeassistant/components/vacuum/.translations/de.json index 7aed7da23e3..3fe2d57eb01 100644 --- a/homeassistant/components/vacuum/.translations/de.json +++ b/homeassistant/components/vacuum/.translations/de.json @@ -4,7 +4,7 @@ "clean": "Lass {entity_name} reinigen", "dock": "Lass {entity_name} zum Dock zur\u00fcckkehren" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} reinigt", "is_docked": "{entity_name} ist angedockt" }, diff --git a/homeassistant/components/vacuum/.translations/en.json b/homeassistant/components/vacuum/.translations/en.json index 396c6a83be9..3feb8eada72 100644 --- a/homeassistant/components/vacuum/.translations/en.json +++ b/homeassistant/components/vacuum/.translations/en.json @@ -4,7 +4,7 @@ "clean": "Let {entity_name} clean", "dock": "Let {entity_name} return to the dock" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} is cleaning", "is_docked": "{entity_name} is docked" }, diff --git a/homeassistant/components/vacuum/.translations/es.json b/homeassistant/components/vacuum/.translations/es.json index 9ecf3ade99c..376058faafa 100644 --- a/homeassistant/components/vacuum/.translations/es.json +++ b/homeassistant/components/vacuum/.translations/es.json @@ -4,9 +4,9 @@ "clean": "Deje que {entity_name} limpie", "dock": "Deje que {entity_name} regrese a la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e1 limpiando", - "is_docked": "{entity_name} est\u00e1 acoplado" + "is_docked": "{entity_name} en la base" }, "trigger_type": { "cleaning": "{entity_name} empez\u00f3 a limpiar", diff --git a/homeassistant/components/vacuum/.translations/fr.json b/homeassistant/components/vacuum/.translations/fr.json index 4a0ab7f8de7..84d5e17bda1 100644 --- a/homeassistant/components/vacuum/.translations/fr.json +++ b/homeassistant/components/vacuum/.translations/fr.json @@ -4,7 +4,7 @@ "clean": "Laisser {entity_name} vide", "dock": "Laisser {entity_name} retourner \u00e0 la base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} nettoie", "is_docked": "{entity_name} est connect\u00e9" }, diff --git a/homeassistant/components/vacuum/.translations/hu.json b/homeassistant/components/vacuum/.translations/hu.json new file mode 100644 index 00000000000..81a39802c55 --- /dev/null +++ b/homeassistant/components/vacuum/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "{entity_name} takar\u00edt\u00e1s ind\u00edt\u00e1sa", + "dock": "{entity_name} visszak\u00fcld\u00e9se a dokkol\u00f3ra" + }, + "condition_type": { + "is_cleaning": "{entity_name} takar\u00edt", + "is_docked": "{entity_name} dokkolva van" + }, + "trigger_type": { + "cleaning": "{entity_name} elkezdett takar\u00edtani", + "docked": "{entity_name} dokkolt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/it.json b/homeassistant/components/vacuum/.translations/it.json index 0b879f154fa..32ecd1e0377 100644 --- a/homeassistant/components/vacuum/.translations/it.json +++ b/homeassistant/components/vacuum/.translations/it.json @@ -4,7 +4,7 @@ "clean": "Lascia pulire {entity_name}", "dock": "Lascia che {entity_name} ritorni alla base" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} sta pulendo", "is_docked": "{entity_name} \u00e8 agganciato alla base" }, diff --git a/homeassistant/components/vacuum/.translations/ko.json b/homeassistant/components/vacuum/.translations/ko.json new file mode 100644 index 00000000000..0197329abda --- /dev/null +++ b/homeassistant/components/vacuum/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "device_automation": { + "action_type": { + "clean": "{entity_name} \uc744(\ub97c) \uccad\uc18c\uc2dc\ud0a4\uae30", + "dock": "{entity_name} \uc744(\ub97c) \ucda9\uc804\uc2a4\ud14c\uc774\uc158\uc73c\ub85c \ubcf5\uadc0\uc2dc\ud0a4\uae30" + }, + "condition_type": { + "is_cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c \uc911\uc774\uba74", + "is_docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub418\uc5b4\uc788\uc73c\uba74" + }, + "trigger_type": { + "cleaning": "{entity_name} \uc774(\uac00) \uccad\uc18c\ub97c \uc2dc\uc791\ud560 \ub54c", + "docked": "{entity_name} \uc774(\uac00) \ub3c4\ud0b9\ub420 \ub54c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/lb.json b/homeassistant/components/vacuum/.translations/lb.json index 6d984b997fa..d6776ccd619 100644 --- a/homeassistant/components/vacuum/.translations/lb.json +++ b/homeassistant/components/vacuum/.translations/lb.json @@ -4,7 +4,7 @@ "clean": "Looss {entity_name} botzen", "dock": "Sch\u00e9ck {entity_name} z\u00e9reck zur Statioun" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} botzt", "is_docked": "{entity_name} ass an der Statioun" }, diff --git a/homeassistant/components/vacuum/.translations/nl.json b/homeassistant/components/vacuum/.translations/nl.json index 3032fc22508..8ef0588796c 100644 --- a/homeassistant/components/vacuum/.translations/nl.json +++ b/homeassistant/components/vacuum/.translations/nl.json @@ -1,10 +1,16 @@ { "device_automation": { - "condtion_type": { - "is_cleaning": "{entity_name} is aan het schoonmaken" + "action_type": { + "clean": "Laat {entity_name} schoonmaken", + "dock": "Laat {entity_name} terugkeren naar het basisstation" + }, + "condition_type": { + "is_cleaning": "{entity_name} is aan het schoonmaken", + "is_docked": "{entity_name} is bij basisstation" }, "trigger_type": { - "cleaning": "{entity_name} begon met schoonmaken" + "cleaning": "{entity_name} begon met schoonmaken", + "docked": "{entity_name} is bij basisstation" } } } \ No newline at end of file diff --git a/homeassistant/components/vacuum/.translations/no.json b/homeassistant/components/vacuum/.translations/no.json index 7d6475f8cef..0c34081cb2f 100644 --- a/homeassistant/components/vacuum/.translations/no.json +++ b/homeassistant/components/vacuum/.translations/no.json @@ -4,7 +4,7 @@ "clean": "La {entity_name} rengj\u00f8res", "dock": "La {entity_name} tilbake til dock" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} rengj\u00f8res", "is_docked": "{entity_name} er docked" }, diff --git a/homeassistant/components/vacuum/.translations/pl.json b/homeassistant/components/vacuum/.translations/pl.json index e637c26b3ed..09eef23ac9a 100644 --- a/homeassistant/components/vacuum/.translations/pl.json +++ b/homeassistant/components/vacuum/.translations/pl.json @@ -4,7 +4,7 @@ "clean": "niech {entity_name} sprz\u0105ta", "dock": "niech {entity_name} wr\u00f3ci do bazy" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} sprz\u0105ta", "is_docked": "{entity_name} jest w bazie" }, diff --git a/homeassistant/components/vacuum/.translations/pt.json b/homeassistant/components/vacuum/.translations/pt.json index 42b8bdabc0f..15b8ac3fd19 100644 --- a/homeassistant/components/vacuum/.translations/pt.json +++ b/homeassistant/components/vacuum/.translations/pt.json @@ -3,7 +3,7 @@ "action_type": { "clean": "Deixar {entity_name} limpar" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} est\u00e1 a limpar" } } diff --git a/homeassistant/components/vacuum/.translations/ru.json b/homeassistant/components/vacuum/.translations/ru.json index c727e8f6ea3..c42f0310fae 100644 --- a/homeassistant/components/vacuum/.translations/ru.json +++ b/homeassistant/components/vacuum/.translations/ru.json @@ -4,7 +4,7 @@ "clean": "\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c {entity_name} \u0434\u0435\u043b\u0430\u0442\u044c \u0443\u0431\u043e\u0440\u043a\u0443", "dock": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c {entity_name} \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u044e" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u0434\u0435\u043b\u0430\u0435\u0442 \u0443\u0431\u043e\u0440\u043a\u0443", "is_docked": "{entity_name} \u0443 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0438\u0438" }, diff --git a/homeassistant/components/vacuum/.translations/sl.json b/homeassistant/components/vacuum/.translations/sl.json index 25de303b157..c594c4f1bdd 100644 --- a/homeassistant/components/vacuum/.translations/sl.json +++ b/homeassistant/components/vacuum/.translations/sl.json @@ -4,7 +4,7 @@ "clean": "Naj {entity_name} \u010disti", "dock": "Pustite, da se {entity_name} vrne na dok" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u010disti", "is_docked": "{entity_name} je priklju\u010den" }, diff --git a/homeassistant/components/vacuum/.translations/zh-Hant.json b/homeassistant/components/vacuum/.translations/zh-Hant.json index f0ad431afc9..b406e1baede 100644 --- a/homeassistant/components/vacuum/.translations/zh-Hant.json +++ b/homeassistant/components/vacuum/.translations/zh-Hant.json @@ -4,7 +4,7 @@ "clean": "\u555f\u52d5 {entity_name} \u6e05\u9664", "dock": "\u555f\u52d5 {entity_name} \u56de\u5230\u5145\u96fb\u7ad9" }, - "condtion_type": { + "condition_type": { "is_cleaning": "{entity_name} \u6b63\u5728\u6e05\u6383", "is_docked": "{entity_name} \u65bc\u5145\u96fb\u7ad9" }, diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 85b3d665e17..225a6ed72bc 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -5,28 +5,26 @@ import logging import voluptuous as vol -from homeassistant.components import group from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API ATTR_BATTERY_LEVEL, ATTR_COMMAND, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + STATE_IDLE, STATE_ON, STATE_PAUSED, - STATE_IDLE, ) -from homeassistant.loader import bind_hass import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 - make_entity_service_schema, PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, + make_entity_service_schema, ) +from homeassistant.helpers.entity import Entity, ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import ToggleEntity, Entity from homeassistant.helpers.icon import icon_for_battery_level - +from homeassistant.loader import bind_hass # mypy: allow-untyped-defs, no-check-untyped-defs @@ -35,9 +33,6 @@ _LOGGER = logging.getLogger(__name__) DOMAIN = "vacuum" SCAN_INTERVAL = timedelta(seconds=20) -GROUP_NAME_ALL_VACUUMS = "all vacuum cleaners" -ENTITY_ID_ALL_VACUUMS = group.ENTITY_ID_FORMAT.format("all_vacuum_cleaners") - ATTR_BATTERY_ICON = "battery_icon" ATTR_CLEANED_AREA = "cleaned_area" ATTR_FAN_SPEED = "fan_speed" @@ -82,16 +77,15 @@ SUPPORT_START = 8192 @bind_hass -def is_on(hass, entity_id=None): +def is_on(hass, entity_id): """Return if the vacuum is on based on the statemachine.""" - entity_id = entity_id or ENTITY_ID_ALL_VACUUMS return hass.states.is_state(entity_id, STATE_ON) async def async_setup(hass, config): """Set up the vacuum component.""" component = hass.data[DOMAIN] = EntityComponent( - _LOGGER, DOMAIN, hass, SCAN_INTERVAL, GROUP_NAME_ALL_VACUUMS + _LOGGER, DOMAIN, hass, SCAN_INTERVAL ) await component.async_setup(config) @@ -252,6 +246,12 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): battery_level=self.battery_level, charging=charging ) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + if self.fan_speed is not None: + return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} + @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" @@ -266,7 +266,6 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): if self.fan_speed is not None: data[ATTR_FAN_SPEED] = self.fan_speed - data[ATTR_FAN_SPEED_LIST] = self.fan_speed_list return data @@ -329,6 +328,12 @@ class StateVacuumDevice(_BaseVacuum): battery_level=self.battery_level, charging=charging ) + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + if self.fan_speed is not None: + return {ATTR_FAN_SPEED_LIST: self.fan_speed_list} + @property def state_attributes(self): """Return the state attributes of the vacuum cleaner.""" diff --git a/homeassistant/components/vacuum/device_action.py b/homeassistant/components/vacuum/device_action.py index e5f8c162fbd..ed25289da10 100644 --- a/homeassistant/components/vacuum/device_action.py +++ b/homeassistant/components/vacuum/device_action.py @@ -1,18 +1,20 @@ """Provides device automations for Vacuum.""" -from typing import Optional, List +from typing import List, Optional + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv -from . import DOMAIN, SERVICE_START, SERVICE_RETURN_TO_BASE + +from . import DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START ACTION_TYPES = {"clean", "dock"} diff --git a/homeassistant/components/vacuum/device_condition.py b/homeassistant/components/vacuum/device_condition.py index 6a41fe0490e..5a2eefd94f2 100644 --- a/homeassistant/components/vacuum/device_condition.py +++ b/homeassistant/components/vacuum/device_condition.py @@ -1,20 +1,22 @@ """Provide the device automations for Vacuum.""" from typing import Dict, List + import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, - CONF_DOMAIN, - CONF_TYPE, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_TYPE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import condition, config_validation as cv, entity_registry -from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA -from . import DOMAIN, STATE_DOCKED, STATE_CLEANING, STATE_RETURNING +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATE_RETURNING CONDITION_TYPES = {"is_cleaning", "is_docked"} diff --git a/homeassistant/components/vacuum/device_trigger.py b/homeassistant/components/vacuum/device_trigger.py index 328db54b1b9..ee225ab3caa 100644 --- a/homeassistant/components/vacuum/device_trigger.py +++ b/homeassistant/components/vacuum/device_trigger.py @@ -1,19 +1,21 @@ """Provides device automations for Vacuum.""" from typing import List + import voluptuous as vol +from homeassistant.components.automation import AutomationActionType, state +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( - CONF_DOMAIN, - CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, + CONF_DOMAIN, CONF_ENTITY_ID, + CONF_PLATFORM, + CONF_TYPE, ) -from homeassistant.core import HomeAssistant, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType -from homeassistant.components.automation import state, AutomationActionType -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA + from . import DOMAIN, STATE_CLEANING, STATE_DOCKED, STATES TRIGGER_TYPES = {"cleaning", "docked"} diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index 69edc40b15b..895311ae5b6 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -3,8 +3,6 @@ "name": "Vacuum", "documentation": "https://www.home-assistant.io/integrations/vacuum", "requirements": [], - "dependencies": [ - "group" - ], + "dependencies": ["group"], "codeowners": [] } diff --git a/homeassistant/components/vacuum/strings.json b/homeassistant/components/vacuum/strings.json index 0300242a506..4eee3f359b5 100644 --- a/homeassistant/components/vacuum/strings.json +++ b/homeassistant/components/vacuum/strings.json @@ -1,6 +1,6 @@ { "device_automation": { - "condtion_type": { + "condition_type": { "is_docked": "{entity_name} is docked", "is_cleaning": "{entity_name} is cleaning" }, diff --git a/homeassistant/components/vallox/manifest.json b/homeassistant/components/vallox/manifest.json index 51ecda91404..875cc6f8787 100644 --- a/homeassistant/components/vallox/manifest.json +++ b/homeassistant/components/vallox/manifest.json @@ -1,10 +1,8 @@ { "domain": "vallox", - "name": "Vallox", + "name": "Valloxs", "documentation": "https://www.home-assistant.io/integrations/vallox", - "requirements": [ - "vallox-websocket-api==2.2.0" - ], + "requirements": ["vallox-websocket-api==2.2.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/vasttrafik/manifest.json b/homeassistant/components/vasttrafik/manifest.json index c4e3a2c97bb..9d339d64dd8 100644 --- a/homeassistant/components/vasttrafik/manifest.json +++ b/homeassistant/components/vasttrafik/manifest.json @@ -1,10 +1,8 @@ { "domain": "vasttrafik", - "name": "Vasttrafik", + "name": "Västtrafik", "documentation": "https://www.home-assistant.io/integrations/vasttrafik", - "requirements": [ - "vtjp==0.1.14" - ], + "requirements": ["vtjp==0.1.14"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index d13383a0832..54fd0a5503e 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -5,9 +5,9 @@ import logging import vasttrafik import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.dt import now diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index 9946f06446f..8e00bc3fee5 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -1,13 +1,14 @@ """Support for Velbus devices.""" import asyncio import logging + import velbus import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_PORT, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -21,7 +22,7 @@ CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA ) -COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate"] +COMPONENT_TYPES = ["switch", "sensor", "binary_sensor", "cover", "climate", "light"] async def async_setup(hass, config): @@ -66,9 +67,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.data[DOMAIN][entry.entry_id] = discovery_info for category in COMPONENT_TYPES: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, category) - ) + hass.add_job(hass.config_entries.async_forward_entry_setup(entry, category)) try: controller = velbus.Controller(entry.data[CONF_PORT]) diff --git a/homeassistant/components/velbus/binary_sensor.py b/homeassistant/components/velbus/binary_sensor.py index 9230632e442..505303ded24 100644 --- a/homeassistant/components/velbus/binary_sensor.py +++ b/homeassistant/components/velbus/binary_sensor.py @@ -3,8 +3,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/climate.py b/homeassistant/components/velbus/climate.py index eb5ed00c395..812e4605d95 100644 --- a/homeassistant/components/velbus/climate.py +++ b/homeassistant/components/velbus/climate.py @@ -10,8 +10,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index e9cbe14ce25..9325acf0608 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -3,7 +3,7 @@ import velbus import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PORT, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant, callback from homeassistant.util import slugify diff --git a/homeassistant/components/velbus/cover.py b/homeassistant/components/velbus/cover.py index cf73af593b8..aea02331ead 100644 --- a/homeassistant/components/velbus/cover.py +++ b/homeassistant/components/velbus/cover.py @@ -4,14 +4,14 @@ import logging from velbus.util import VelbusException from homeassistant.components.cover import ( - CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP, + CoverDevice, ) -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py new file mode 100644 index 00000000000..7db79e74d5b --- /dev/null +++ b/homeassistant/components/velbus/light.py @@ -0,0 +1,119 @@ +"""Support for Velbus light.""" +import logging + +from velbus.util import VelbusException + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_FLASH, + ATTR_TRANSITION, + FLASH_LONG, + FLASH_SHORT, + SUPPORT_BRIGHTNESS, + SUPPORT_FLASH, + SUPPORT_TRANSITION, + Light, +) + +from . import VelbusEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Old way.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Velbus light based on config_entry.""" + cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] + modules_data = hass.data[DOMAIN][entry.entry_id]["light"] + entities = [] + for address, channel in modules_data: + module = cntrl.get_module(address) + entities.append(VelbusLight(module, channel)) + async_add_entities(entities) + + +class VelbusLight(VelbusEntity, Light): + """Representation of a Velbus light.""" + + @property + def name(self): + """Return the display name of this entity.""" + if self._module.light_is_buttonled(self._channel): + return f"LED {self._module.get_name(self._channel)}" + return self._module.get_name(self._channel) + + @property + def supported_features(self): + """Flag supported features.""" + if self._module.light_is_buttonled(self._channel): + return SUPPORT_FLASH + return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + + @property + def entity_registry_enabled_default(self): + """Disable Button LEDs by default.""" + if self._module.light_is_buttonled(self._channel): + return False + return True + + @property + def is_on(self): + """Return true if the light is on.""" + return self._module.is_on(self._channel) + + @property + def brightness(self): + """Return the brightness of the light.""" + return self._module.get_dimmer_state(self._channel) + + def turn_on(self, **kwargs): + """Instruct the Velbus light to turn on.""" + if self._module.light_is_buttonled(self._channel): + if ATTR_FLASH in kwargs: + if kwargs[ATTR_FLASH] == FLASH_LONG: + attr, *args = "set_led_state", self._channel, "slow" + elif kwargs[ATTR_FLASH] == FLASH_SHORT: + attr, *args = "set_led_state", self._channel, "fast" + else: + attr, *args = "set_led_state", self._channel, "on" + else: + attr, *args = "set_led_state", self._channel, "on" + else: + if ATTR_BRIGHTNESS in kwargs: + attr, *args = ( + "set_dimmer_state", + self._channel, + kwargs[ATTR_BRIGHTNESS], + kwargs.get(ATTR_TRANSITION, 0), + ) + else: + attr, *args = ( + "restore_dimmer_state", + self._channel, + kwargs.get(ATTR_TRANSITION, 0), + ) + try: + getattr(self._module, attr)(*args) + except VelbusException as err: + _LOGGER.error("A Velbus error occurred: %s", err) + + def turn_off(self, **kwargs): + """Instruct the velbus light to turn off.""" + if self._module.light_is_buttonled(self._channel): + attr, *args = "set_led_state", self._channel, "off" + else: + attr, *args = ( + "set_dimmer_state", + self._channel, + 0, + kwargs.get(ATTR_TRANSITION, 0), + ) + try: + getattr(self._module, attr)(*args) + except VelbusException as err: + _LOGGER.error("A Velbus error occurred: %s", err) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 1d9401f6cfe..250b2c01e4e 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,9 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": [ - "python-velbus==2.0.27" - ], + "requirements": ["python-velbus==2.0.35"], "config_flow": true, "dependencies": [], "codeowners": ["@cereal2nd"] diff --git a/homeassistant/components/velbus/sensor.py b/homeassistant/components/velbus/sensor.py index 3b7f2b6f5bc..8af5df9e165 100644 --- a/homeassistant/components/velbus/sensor.py +++ b/homeassistant/components/velbus/sensor.py @@ -1,8 +1,8 @@ """Support for Velbus sensors.""" import logging -from .const import DOMAIN from . import VelbusEntity +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/velbus/services.yaml b/homeassistant/components/velbus/services.yaml index 40916a08418..273cc8b4caa 100644 --- a/homeassistant/components/velbus/services.yaml +++ b/homeassistant/components/velbus/services.yaml @@ -1,2 +1,2 @@ sync_clock: - description: Sync the velbus modules clock to the HASS clock, this is the same as the 'sync clock' from VelbusLink + description: Sync the velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index 7d4adc7350c..c9b4aa53fe5 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -1,6 +1,6 @@ """Support for Velux covers.""" from pyvlx import OpeningDevice, Position -from pyvlx.opening_device import Awning, Blind, RollerShutter, Window +from pyvlx.opening_device import Awning, Blind, GarageDoor, RollerShutter, Window from homeassistant.components.cover import ( ATTR_POSITION, @@ -77,6 +77,8 @@ class VeluxCover(CoverDevice): return "shutter" if isinstance(self.node, Awning): return "awning" + if isinstance(self.node, GarageDoor): + return "garage" return "window" @property diff --git a/homeassistant/components/velux/manifest.json b/homeassistant/components/velux/manifest.json index 783e23a8171..7ecc2ac6ded 100644 --- a/homeassistant/components/velux/manifest.json +++ b/homeassistant/components/velux/manifest.json @@ -2,11 +2,7 @@ "domain": "velux", "name": "Velux", "documentation": "https://www.home-assistant.io/integrations/velux", - "requirements": [ - "pyvlx==0.2.11" - ], + "requirements": ["pyvlx==0.2.12"], "dependencies": [], - "codeowners": [ - "@Julius2342" - ] + "codeowners": ["@Julius2342"] } diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index de26d236649..effecd7244c 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -4,37 +4,38 @@ import logging from venstarcolortouch import VenstarColorTouch import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_AUTO, + FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL, - CURRENT_HVAC_IDLE, - CURRENT_HVAC_OFF, - SUPPORT_FAN_MODE, - FAN_ON, - FAN_AUTO, - SUPPORT_TARGET_HUMIDITY, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, PRESET_AWAY, PRESET_NONE, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, + CONF_PIN, CONF_SSL, CONF_TIMEOUT, CONF_USERNAME, - PRECISION_WHOLE, + PRECISION_HALVES, STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT, @@ -66,6 +67,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Coerce(int), vol.Range(min=1) ), vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PIN): cv.string, } ) @@ -75,6 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) + pin = config.get(CONF_PIN) host = config.get(CONF_HOST) timeout = config.get(CONF_TIMEOUT) humidifier = config.get(CONF_HUMIDIFIER) @@ -85,7 +88,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): proto = "http" client = VenstarColorTouch( - addr=host, timeout=timeout, user=username, password=password, proto=proto + addr=host, + timeout=timeout, + user=username, + password=password, + pin=pin, + proto=proto, ) add_entities([VenstarThermostat(client, humidifier)], True) @@ -134,9 +142,9 @@ class VenstarThermostat(ClimateDevice): """Return the precision of the system. Venstar temperature values are passed back and forth in the - API as whole degrees C or F. + API in C or F, with half-degree accuracy. """ - return PRECISION_WHOLE + return PRECISION_HALVES @property def temperature_unit(self): diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index e8e36d04467..e723e16d41d 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -2,9 +2,7 @@ "domain": "venstar", "name": "Venstar", "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": [ - "venstarcolortouch==0.9" - ], + "requirements": ["venstarcolortouch==0.12"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 8fcc8a4a2fe..1c9d412d974 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -1,25 +1,24 @@ """Support for Vera devices.""" -import logging from collections import defaultdict +import logging import pyvera as veraApi -import voluptuous as vol from requests.exceptions import RequestException +import voluptuous as vol -from homeassistant.util.dt import utc_from_timestamp -from homeassistant.util import convert, slugify -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv from homeassistant.const import ( ATTR_ARMED, ATTR_BATTERY_LEVEL, ATTR_LAST_TRIP_TIME, ATTR_TRIPPED, - EVENT_HOMEASSISTANT_STOP, - CONF_LIGHTS, CONF_EXCLUDE, + CONF_LIGHTS, + EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity +from homeassistant.util import convert, slugify +from homeassistant.util.dt import utc_from_timestamp _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 50c897d0fc1..60e73d48978 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -92,6 +92,8 @@ class VeraThermostat(VeraDevice, ClimateDevice): else: self.vera_device.fan_auto() + self.schedule_update_ha_state() + @property def current_power_w(self): """Return the current power usage in W.""" @@ -129,6 +131,8 @@ class VeraThermostat(VeraDevice, ClimateDevice): if kwargs.get(ATTR_TEMPERATURE) is not None: self.vera_device.set_temperature(kwargs.get(ATTR_TEMPERATURE)) + self.schedule_update_ha_state() + def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if hvac_mode == HVAC_MODE_OFF: @@ -139,3 +143,5 @@ class VeraThermostat(VeraDevice, ClimateDevice): self.vera_device.turn_cool_on() elif hvac_mode == HVAC_MODE_HEAT: self.vera_device.turn_heat_on() + + self.schedule_update_ha_state() diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 120ec241d60..63102c29687 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -2,9 +2,7 @@ "domain": "vera", "name": "Vera", "documentation": "https://www.home-assistant.io/integrations/vera", - "requirements": [ - "pyvera==0.3.6" - ], + "requirements": ["pyvera==0.3.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 3ab98a6560e..32735bf06c1 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -1,11 +1,10 @@ """Support for Verisure devices.""" +from datetime import timedelta import logging import threading -from datetime import timedelta from jsonpath import jsonpath import verisure - import voluptuous as vol from homeassistant.const import ( @@ -15,8 +14,8 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 47ec3c536b3..bbdd9f54e83 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -2,8 +2,8 @@ import logging from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_CONNECTIVITY, + BinarySensorDevice, ) from . import CONF_DOOR_WINDOW, HUB as hub diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 13962e81b7b..3e0073c1770 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,10 +2,7 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": [ - "jsonpath==0.82", - "vsure==1.5.4" - ], + "requirements": ["jsonpath==0.82", "vsure==1.5.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/versasense/__init__.py b/homeassistant/components/versasense/__init__.py index 4f378f4ab00..d2081d715d5 100644 --- a/homeassistant/components/versasense/__init__.py +++ b/homeassistant/components/versasense/__init__.py @@ -10,14 +10,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from .const import ( + KEY_CONSUMER, + KEY_IDENTIFIER, + KEY_MEASUREMENT, + KEY_PARENT_MAC, + KEY_PARENT_NAME, + KEY_UNIT, PERIPHERAL_CLASS_SENSOR, PERIPHERAL_CLASS_SENSOR_ACTUATOR, - KEY_IDENTIFIER, - KEY_PARENT_NAME, - KEY_PARENT_MAC, - KEY_UNIT, - KEY_MEASUREMENT, - KEY_CONSUMER, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/versasense/manifest.json b/homeassistant/components/versasense/manifest.json index 3e2be6131d1..231588c8bf3 100644 --- a/homeassistant/components/versasense/manifest.json +++ b/homeassistant/components/versasense/manifest.json @@ -1,8 +1,8 @@ { - "domain": "versasense", - "name": "VersaSense", - "documentation": "https://www.home-assistant.io/components/versasense", - "dependencies": [], - "codeowners": ["@flamm3blemuff1n"], - "requirements": ["pyversasense==0.0.6"] + "domain": "versasense", + "name": "VersaSense", + "documentation": "https://www.home-assistant.io/components/versasense", + "dependencies": [], + "codeowners": ["@flamm3blemuff1n"], + "requirements": ["pyversasense==0.0.6"] } diff --git a/homeassistant/components/versasense/sensor.py b/homeassistant/components/versasense/sensor.py index 4253bfcbba4..e598093cd37 100644 --- a/homeassistant/components/versasense/sensor.py +++ b/homeassistant/components/versasense/sensor.py @@ -5,12 +5,12 @@ from homeassistant.helpers.entity import Entity from . import DOMAIN from .const import ( - KEY_IDENTIFIER, - KEY_PARENT_NAME, - KEY_PARENT_MAC, - KEY_UNIT, - KEY_MEASUREMENT, KEY_CONSUMER, + KEY_IDENTIFIER, + KEY_MEASUREMENT, + KEY_PARENT_MAC, + KEY_PARENT_NAME, + KEY_UNIT, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/versasense/switch.py b/homeassistant/components/versasense/switch.py index 4ea118a6c97..4b44cb7aa2a 100644 --- a/homeassistant/components/versasense/switch.py +++ b/homeassistant/components/versasense/switch.py @@ -5,14 +5,14 @@ from homeassistant.components.switch import SwitchDevice from . import DOMAIN from .const import ( - PERIPHERAL_STATE_ON, - PERIPHERAL_STATE_OFF, - KEY_IDENTIFIER, - KEY_PARENT_NAME, - KEY_PARENT_MAC, - KEY_UNIT, - KEY_MEASUREMENT, KEY_CONSUMER, + KEY_IDENTIFIER, + KEY_MEASUREMENT, + KEY_PARENT_MAC, + KEY_PARENT_NAME, + KEY_UNIT, + PERIPHERAL_STATE_OFF, + PERIPHERAL_STATE_ON, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 3f35b0dc9a5..aaa96fb0b96 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,11 +2,8 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": [ - "pyhaversion==3.1.0" - ], + "requirements": ["pyhaversion==3.1.0"], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index a6e43251b54..636e564b816 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -1,20 +1,20 @@ """Sensor that can display the current Home Assistant versions.""" -import logging from datetime import timedelta +import logging from pyhaversion import ( - LocalVersion, DockerVersion, - HassioVersion, - PyPiVersion, HaIoVersion, + HassioVersion, + LocalVersion, + PyPiVersion, ) import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_SOURCE +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 diff --git a/homeassistant/components/vesync/.translations/da.json b/homeassistant/components/vesync/.translations/da.json index 43e56328f99..f2be5792f33 100644 --- a/homeassistant/components/vesync/.translations/da.json +++ b/homeassistant/components/vesync/.translations/da.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Adgangskode", - "username": "Email adresse" + "username": "Emailadresse" }, "title": "Indtast brugernavn og adgangskode" } diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 9ed71dbc5ee..0f905b8d7ef 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -1,20 +1,23 @@ """Etekcity VeSync integration.""" import logging -import voluptuous as vol + from pyvesync import VeSync -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.config_entries import SOURCE_IMPORT + from .common import async_process_devices from .config_flow import configured_instances from .const import ( DOMAIN, - VS_DISPATCHERS, - VS_DISCOVERY, - VS_SWITCHES, SERVICE_UPDATE_DEVS, + VS_DISCOVERY, + VS_DISPATCHERS, VS_MANAGER, + VS_SWITCHES, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index 361b3913283..d2ffa5281e9 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -1,6 +1,8 @@ """Common utilities for VeSync Component.""" import logging + from homeassistant.helpers.entity import ToggleEntity + from .const import VS_SWITCHES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index 168a3568392..8b0e8ae6781 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -1,11 +1,14 @@ """Config flow utilities.""" -import logging from collections import OrderedDict -import voluptuous as vol +import logging + from pyvesync import VeSync +import voluptuous as vol + from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD + from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index d52380c1025..1563ee0ce2b 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -1,6 +1,6 @@ { "domain": "vesync", - "name": "VeSync", + "name": "Etekcity VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "dependencies": [], "codeowners": ["@markperdue", "@webdjoe"], diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index 5ca76a77254..6ab5c0c4368 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -1,10 +1,12 @@ """Support for Etekcity VeSync switches.""" import logging -from homeassistant.core import callback + from homeassistant.components.switch import SwitchDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import VS_DISCOVERY, VS_DISPATCHERS, VS_SWITCHES, DOMAIN + from .common import VeSyncDevice +from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_SWITCHES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/viaggiatreno/manifest.json b/homeassistant/components/viaggiatreno/manifest.json index 99857fc2f7e..4825441707b 100644 --- a/homeassistant/components/viaggiatreno/manifest.json +++ b/homeassistant/components/viaggiatreno/manifest.json @@ -1,6 +1,6 @@ { "domain": "viaggiatreno", - "name": "Viaggiatreno", + "name": "Trenitalia ViaggiaTreno", "documentation": "https://www.home-assistant.io/integrations/viaggiatreno", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index e091ff99970..282e234811a 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -2,15 +2,14 @@ import enum import logging -import voluptuous as vol - from PyViCare.PyViCareDevice import Device from PyViCare.PyViCareGazBoiler import GazBoiler from PyViCare.PyViCareHeatPump import HeatPump +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 4f6f0cedcd9..1b101cc7612 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,26 +1,29 @@ """Viessmann ViCare climate device.""" import logging + import requests 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, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + HVAC_MODE_OFF, + PRESET_COMFORT, + PRESET_ECO, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from . import DOMAIN as VICARE_DOMAIN -from . import VICARE_API -from . import VICARE_NAME -from . import VICARE_HEATING_TYPE -from . import HeatingType +from . import ( + DOMAIN as VICARE_DOMAIN, + VICARE_API, + VICARE_HEATING_TYPE, + VICARE_NAME, + HeatingType, +) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 03bb16fa9bb..cefc244e5b8 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -6,4 +6,3 @@ "codeowners": ["@oischinger"], "requirements": ["PyViCare==0.1.2"] } - diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index eefacf99c39..f31e4f65170 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,17 +1,15 @@ """Viessmann ViCare water_heater device.""" import logging + import requests from homeassistant.components.water_heater import ( SUPPORT_TARGET_TEMPERATURE, WaterHeaterDevice, ) -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE, PRECISION_WHOLE +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS -from . import DOMAIN as VICARE_DOMAIN -from . import VICARE_API -from . import VICARE_NAME -from . import VICARE_HEATING_TYPE +from . import DOMAIN as VICARE_DOMAIN, VICARE_API, VICARE_HEATING_TYPE, VICARE_NAME _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/vivotek/camera.py b/homeassistant/components/vivotek/camera.py index 2e604199dd8..f4a195f5b0c 100644 --- a/homeassistant/components/vivotek/camera.py +++ b/homeassistant/components/vivotek/camera.py @@ -2,18 +2,21 @@ import logging -import voluptuous as vol from libpyvivotek import VivotekCamera +import voluptuous as vol +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.const import ( + CONF_AUTHENTICATION, CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_SSL, CONF_USERNAME, CONF_VERIFY_SSL, + HTTP_BASIC_AUTHENTICATION, + HTTP_DIGEST_AUTHENTICATION, ) -from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -34,6 +37,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In( + [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] + ), 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, @@ -54,6 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): verify_ssl=config[CONF_VERIFY_SSL], usr=config[CONF_USERNAME], pwd=config[CONF_PASSWORD], + digest_auth=config[CONF_AUTHENTICATION] == HTTP_DIGEST_AUTHENTICATION, sec_lvl=config[CONF_SECURITY_LEVEL], ), stream_source=f"rtsp://{creds}@{config[CONF_IP_ADDRESS]}:554/{config[CONF_STREAM_PATH]}", diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index c97a8461da9..9246bc4c89b 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -2,11 +2,7 @@ "domain": "vivotek", "name": "Vivotek", "documentation": "https://www.home-assistant.io/integrations/vivotek", - "requirements": [ - "libpyvivotek==0.3.1" - ], + "requirements": ["libpyvivotek==0.4.0"], "dependencies": [], - "codeowners": [ - "@HarlemSquirrel" - ] + "codeowners": ["@HarlemSquirrel"] } diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index 3575f2cf648..3ffbf46f928 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -1 +1,41 @@ """The vizio component.""" +import voluptuous as vol + +from homeassistant.const import ( + CONF_ACCESS_TOKEN, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, +) +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_VOLUME_STEP, + DEFAULT_DEVICE_CLASS, + DEFAULT_NAME, + DEFAULT_VOLUME_STEP, +) + + +def validate_auth(config): + """Validate presence of CONF_ACCESS_TOKEN when CONF_DEVICE_CLASS=tv.""" + token = config.get(CONF_ACCESS_TOKEN) + if config[CONF_DEVICE_CLASS] == "tv" and not token: + raise vol.Invalid( + f"When '{CONF_DEVICE_CLASS}' is 'tv' then '{CONF_ACCESS_TOKEN}' is required.", + path=[CONF_ACCESS_TOKEN], + ) + return config + + +VIZIO_SCHEMA = { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.All( + cv.string, vol.Lower, vol.In(["tv", "soundbar"]) + ), + vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): vol.All( + vol.Coerce(int), vol.Range(min=1, max=10) + ), +} diff --git a/homeassistant/components/vizio/const.py b/homeassistant/components/vizio/const.py new file mode 100644 index 00000000000..828c4e600e0 --- /dev/null +++ b/homeassistant/components/vizio/const.py @@ -0,0 +1,13 @@ +"""Constants used by vizio component.""" + +CONF_VOLUME_STEP = "volume_step" + +DEFAULT_NAME = "Vizio SmartCast" +DEFAULT_VOLUME_STEP = 1 +DEFAULT_DEVICE_CLASS = "tv" +DEVICE_ID = "pyvizio" +DEVICE_NAME = "Python Vizio" + +DOMAIN = "vizio" + +ICON = {"tv": "mdi:television", "soundbar": "mdi:speaker"} diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 682405a375b..7ae0570fa86 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -1,10 +1,8 @@ { "domain": "vizio", - "name": "Vizio", + "name": "Vizio SmartCast TV", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": [ - "pyvizio==0.0.7" - ], + "requirements": ["pyvizio==0.0.12"], "dependencies": [], "codeowners": ["@raman325"] } diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 94601216f23..418cf8e3835 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging from pyvizio import Vizio -from requests.packages import urllib3 import voluptuous as vol from homeassistant import util @@ -26,21 +25,12 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.helpers import config_validation as cv + +from . import VIZIO_SCHEMA, validate_auth +from .const import CONF_VOLUME_STEP, DEFAULT_NAME, DEVICE_ID, ICON _LOGGER = logging.getLogger(__name__) -CONF_SUPPRESS_WARNING = "suppress_warning" -CONF_VOLUME_STEP = "volume_step" - -DEFAULT_NAME = "Vizio SmartCast" -DEFAULT_VOLUME_STEP = 1 -DEFAULT_DEVICE_CLASS = "tv" -DEVICE_ID = "pyvizio" -DEVICE_NAME = "Python Vizio" - -ICON = "mdi:television" - MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) @@ -59,36 +49,7 @@ SUPPORTED_COMMANDS = { } -def validate_auth(config): - """Validate presence of CONF_ACCESS_TOKEN when CONF_DEVICE_CLASS=tv.""" - token = config.get(CONF_ACCESS_TOKEN) - if config[CONF_DEVICE_CLASS] == "tv" and (token is None or token == ""): - raise vol.Invalid( - "When '{}' is 'tv' then '{}' is required.".format( - CONF_DEVICE_CLASS, CONF_ACCESS_TOKEN - ), - path=[CONF_ACCESS_TOKEN], - ) - return config - - -PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SUPPRESS_WARNING, default=False): cv.boolean, - vol.Optional(CONF_DEVICE_CLASS, default=DEFAULT_DEVICE_CLASS): vol.All( - cv.string, vol.Lower, vol.In(["tv", "soundbar"]) - ), - vol.Optional(CONF_VOLUME_STEP, default=DEFAULT_VOLUME_STEP): vol.All( - vol.Coerce(int), vol.Range(min=1, max=10) - ), - } - ), - validate_auth, -) +PLATFORM_SCHEMA = vol.All(PLATFORM_SCHEMA.extend(VIZIO_SCHEMA), validate_auth) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -99,9 +60,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): volume_step = config[CONF_VOLUME_STEP] device_type = config[CONF_DEVICE_CLASS] device = VizioDevice(host, token, name, volume_step, device_type) - if device.validate_setup() is False: + if not device.validate_setup(): fail_auth_msg = "" - if token is not None and token != "": + if token: fail_auth_msg = " and auth token is correct" _LOGGER.error( "Failed to set up Vizio platform, please check if host " @@ -110,12 +71,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) return - if config[CONF_SUPPRESS_WARNING]: - _LOGGER.warning( - "InsecureRequestWarning is disabled " - "because of Vizio platform configuration" - ) - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) add_entities([device], True) @@ -135,12 +90,17 @@ class VizioDevice(MediaPlayerDevice): self._supported_commands = SUPPORTED_COMMANDS[device_type] self._device = Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type) self._max_volume = float(self._device.get_max_volume()) + self._unique_id = None + self._icon = ICON[device_type] @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): """Retrieve latest state of the device.""" is_on = self._device.get_power_state() + if not self._unique_id: + self._unique_id = self._device.get_esn() + if is_on: self._state = STATE_ON @@ -176,6 +136,11 @@ class VizioDevice(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def icon(self): + """Return the icon of the device.""" + return self._icon + @property def volume_level(self): """Return the volume level of the device.""" @@ -196,6 +161,11 @@ class VizioDevice(MediaPlayerDevice): """Flag device features that are supported.""" return self._supported_commands + @property + def unique_id(self): + """Return the unique id of the device.""" + return self._unique_id + def turn_on(self): """Turn the device on.""" self._device.pow_on() @@ -241,7 +211,7 @@ class VizioDevice(MediaPlayerDevice): def validate_setup(self): """Validate if host is available and auth token is correct.""" - return self._device.get_current_volume() is not None + return self._device.can_connect() def set_volume_level(self, volume): """Set volume level.""" diff --git a/homeassistant/components/vlc/manifest.json b/homeassistant/components/vlc/manifest.json index 0dfc9f1ceb9..f9fbfeabb0d 100644 --- a/homeassistant/components/vlc/manifest.json +++ b/homeassistant/components/vlc/manifest.json @@ -1,10 +1,8 @@ { "domain": "vlc", - "name": "Vlc", + "name": "VLC media player", "documentation": "https://www.home-assistant.io/integrations/vlc", - "requirements": [ - "python-vlc==1.1.2" - ], + "requirements": ["python-vlc==1.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index 30b316cb4e8..c7a3d49fabc 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -1,10 +1,10 @@ """Provide functionality to interact with vlc devices on the network.""" import logging -import voluptuous as vol import vlc +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_PAUSE, @@ -18,7 +18,6 @@ from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYI import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util - _LOGGER = logging.getLogger(__name__) CONF_ARGUMENTS = "arguments" diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index 7894eb2982b..fdc974878ec 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -1,10 +1,8 @@ { "domain": "vlc_telnet", - "name": "VLC telnet", + "name": "VLC media player Telnet", "documentation": "https://www.home-assistant.io/integrations/vlc-telnet", - "requirements": [ - "python-telnet-vlc==1.0.4" - ], + "requirements": ["python-telnet-vlc==1.0.4"], "dependencies": [], "codeowners": ["@rodripf"] } diff --git a/homeassistant/components/vlc_telnet/media_player.py b/homeassistant/components/vlc_telnet/media_player.py index 1adb4eaebb3..45b0971ad9f 100644 --- a/homeassistant/components/vlc_telnet/media_player.py +++ b/homeassistant/components/vlc_telnet/media_player.py @@ -1,33 +1,33 @@ """Provide functionality to interact with the vlc telnet interface.""" import logging + +from python_telnet_vlc import ConnectionError as ConnErr, VLCTelnet import voluptuous as vol -from python_telnet_vlc import VLCTelnet, ConnectionError as ConnErr - -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, + SUPPORT_CLEAR_PLAYLIST, + SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SEEK, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_SEEK, - SUPPORT_NEXT_TRACK, - SUPPORT_CLEAR_PLAYLIST, - SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( + CONF_HOST, CONF_NAME, + CONF_PASSWORD, + CONF_PORT, STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, - CONF_HOST, - CONF_PORT, - CONF_PASSWORD, ) import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/voicerss/manifest.json b/homeassistant/components/voicerss/manifest.json index 7a079d9ccf2..aef86267425 100644 --- a/homeassistant/components/voicerss/manifest.json +++ b/homeassistant/components/voicerss/manifest.json @@ -1,6 +1,6 @@ { "domain": "voicerss", - "name": "Voicerss", + "name": "VoiceRSS", "documentation": "https://www.home-assistant.io/integrations/voicerss", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index 0b210bc780b..dd361de5f96 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -2,9 +2,7 @@ "domain": "volkszaehler", "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", - "requirements": [ - "volkszaehler==0.1.2" - ], + "requirements": ["volkszaehler==0.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/volkszaehler/sensor.py b/homeassistant/components/volkszaehler/sensor.py index a2da620c1a5..a418780c165 100644 --- a/homeassistant/components/volkszaehler/sensor.py +++ b/homeassistant/components/volkszaehler/sensor.py @@ -9,11 +9,11 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, + CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PORT, - CONF_MONITORED_CONDITIONS, - POWER_WATT, ENERGY_WATT_HOUR, + POWER_WATT, ) from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 7c13488c3f5..f62a74345b1 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -14,22 +14,21 @@ import socket import aiohttp import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE, + SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, - SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( CONF_HOST, @@ -61,7 +60,6 @@ SUPPORT_VOLUMIO = ( | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | SUPPORT_SEEK - | SUPPORT_PLAY_MEDIA | SUPPORT_STOP | SUPPORT_PLAY | SUPPORT_VOLUME_STEP diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 3f75c391115..bc629eafaad 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -1,10 +1,8 @@ { "domain": "volvooncall", - "name": "Volvooncall", + "name": "Volvo On Call", "documentation": "https://www.home-assistant.io/integrations/volvooncall", - "requirements": [ - "volvooncall==0.8.7" - ], + "requirements": ["volvooncall==0.8.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/vultr/manifest.json b/homeassistant/components/vultr/manifest.json index 0a0afe3d71b..f9e9d8d2894 100644 --- a/homeassistant/components/vultr/manifest.json +++ b/homeassistant/components/vultr/manifest.json @@ -2,9 +2,7 @@ "domain": "vultr", "name": "Vultr", "documentation": "https://www.home-assistant.io/integrations/vultr", - "requirements": [ - "vultr==0.1.2" - ], + "requirements": ["vultr==0.1.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/w800rf32/__init__.py b/homeassistant/components/w800rf32/__init__.py index 805cca47023..bbd3fdac23d 100644 --- a/homeassistant/components/w800rf32/__init__.py +++ b/homeassistant/components/w800rf32/__init__.py @@ -1,15 +1,14 @@ """Support for w800rf32 devices.""" import logging -import voluptuous as vol import W800rf32 as w800 +import voluptuous as vol from homeassistant.const import ( CONF_DEVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ) - import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index e08111da8ba..9c83dbce804 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -1,8 +1,8 @@ """Support for w800rf32 binary sensors.""" import logging -import voluptuous as vol import W800rf32 as w800 +import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, diff --git a/homeassistant/components/w800rf32/manifest.json b/homeassistant/components/w800rf32/manifest.json index 89b0ac591ea..5fe6cb70110 100644 --- a/homeassistant/components/w800rf32/manifest.json +++ b/homeassistant/components/w800rf32/manifest.json @@ -1,10 +1,8 @@ { "domain": "w800rf32", - "name": "W800rf32", + "name": "WGL Designs W800RF32", "documentation": "https://www.home-assistant.io/integrations/w800rf32", - "requirements": [ - "pyW800rf32==0.1" - ], + "requirements": ["pyW800rf32==0.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/wake_on_lan/manifest.json b/homeassistant/components/wake_on_lan/manifest.json index ef6dbd06470..526be2a33f2 100644 --- a/homeassistant/components/wake_on_lan/manifest.json +++ b/homeassistant/components/wake_on_lan/manifest.json @@ -1,10 +1,8 @@ { "domain": "wake_on_lan", - "name": "Wake on lan", + "name": "Wake on LAN", "documentation": "https://www.home-assistant.io/integrations/wake_on_lan", - "requirements": [ - "wakeonlan==1.1.6" - ], + "requirements": ["wakeonlan==1.1.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/waqi/manifest.json b/homeassistant/components/waqi/manifest.json index 8ce03e2e8e2..6d37030d1dd 100644 --- a/homeassistant/components/waqi/manifest.json +++ b/homeassistant/components/waqi/manifest.json @@ -1,12 +1,8 @@ { "domain": "waqi", - "name": "Waqi", + "name": "World Air Quality Index (WAQI)", "documentation": "https://www.home-assistant.io/integrations/waqi", - "requirements": [ - "waqiasync==1.0.0" - ], + "requirements": ["waqiasync==1.0.0"], "dependencies": [], - "codeowners": [ - "@andrey-git" - ] + "codeowners": ["@andrey-git"] } diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index b53723a29b6..8f16c216b37 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -1,21 +1,21 @@ """Support for the World Air Quality Index service.""" import asyncio -import logging from datetime import timedelta +import logging import aiohttp import voluptuous as vol from waqiasync import WaqiClient -from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( ATTR_ATTRIBUTION, - ATTR_TIME, ATTR_TEMPERATURE, + ATTR_TIME, CONF_TOKEN, ) +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index 6e7b918c289..8da94ff1098 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -1,32 +1,31 @@ """Support for water heater devices.""" from datetime import timedelta -import logging import functools as ft +import logging import voluptuous as vol -from homeassistant.helpers.temperature import display_temp as show_temp -from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.entity import Entity +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_TEMPERATURE, + PRECISION_TENTHS, + PRECISION_WHOLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -import homeassistant.helpers.config_validation as cv -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - SERVICE_TURN_ON, - SERVICE_TURN_OFF, - STATE_ON, - STATE_OFF, - TEMP_CELSIUS, - PRECISION_WHOLE, - PRECISION_TENTHS, - TEMP_FAHRENHEIT, -) - +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.temperature import display_temp as show_temp +from homeassistant.util.temperature import convert as convert_temperature # mypy: allow-untyped-defs, no-check-untyped-defs @@ -144,6 +143,25 @@ class WaterHeaterDevice(Entity): return PRECISION_TENTHS return PRECISION_WHOLE + @property + def capability_attributes(self): + """Return capabilitiy attributes.""" + supported_features = self.supported_features + + data = { + ATTR_MIN_TEMP: show_temp( + self.hass, self.min_temp, self.temperature_unit, self.precision + ), + ATTR_MAX_TEMP: show_temp( + self.hass, self.max_temp, self.temperature_unit, self.precision + ), + } + + if supported_features & SUPPORT_OPERATION_MODE: + data[ATTR_OPERATION_LIST] = self.operation_list + + return data + @property def state_attributes(self): """Return the optional state attributes.""" @@ -154,12 +172,6 @@ class WaterHeaterDevice(Entity): self.temperature_unit, self.precision, ), - ATTR_MIN_TEMP: show_temp( - self.hass, self.min_temp, self.temperature_unit, self.precision - ), - ATTR_MAX_TEMP: show_temp( - self.hass, self.max_temp, self.temperature_unit, self.precision - ), ATTR_TEMPERATURE: show_temp( self.hass, self.target_temperature, @@ -184,8 +196,6 @@ class WaterHeaterDevice(Entity): if supported_features & SUPPORT_OPERATION_MODE: data[ATTR_OPERATION_MODE] = self.current_operation - if self.operation_list: - data[ATTR_OPERATION_LIST] = self.operation_list if supported_features & SUPPORT_AWAY_MODE: is_away = self.is_away_mode_on diff --git a/homeassistant/components/water_heater/manifest.json b/homeassistant/components/water_heater/manifest.json index 7ed6493b843..7b9adbda1f7 100644 --- a/homeassistant/components/water_heater/manifest.json +++ b/homeassistant/components/water_heater/manifest.json @@ -1,6 +1,6 @@ { "domain": "water_heater", - "name": "Water heater", + "name": "Water Heater", "documentation": "https://www.home-assistant.io/integrations/water_heater", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index b6eb22c89ae..942ab8a14ac 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -1,16 +1,15 @@ """Support for Waterfurnaces.""" from datetime import timedelta import logging -import time import threading +import time import voluptuous as vol from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/waterfurnace/manifest.json b/homeassistant/components/waterfurnace/manifest.json index e2982bd0145..05a38f57892 100644 --- a/homeassistant/components/waterfurnace/manifest.json +++ b/homeassistant/components/waterfurnace/manifest.json @@ -1,10 +1,8 @@ { "domain": "waterfurnace", - "name": "Waterfurnace", + "name": "WaterFurnace", "documentation": "https://www.home-assistant.io/integrations/waterfurnace", - "requirements": [ - "waterfurnace==1.1.0" - ], + "requirements": ["waterfurnace==1.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/watson_iot/manifest.json b/homeassistant/components/watson_iot/manifest.json index 20834bf0bf4..d12c40e4def 100644 --- a/homeassistant/components/watson_iot/manifest.json +++ b/homeassistant/components/watson_iot/manifest.json @@ -1,10 +1,8 @@ { "domain": "watson_iot", - "name": "Watson iot", + "name": "IBM Watson IoT Platform", "documentation": "https://www.home-assistant.io/integrations/watson_iot", - "requirements": [ - "ibmiotf==0.3.4" - ], + "requirements": ["ibmiotf==0.3.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index e5f3efae04c..c1d708a20f9 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -2,11 +2,7 @@ "domain": "watson_tts", "name": "IBM Watson TTS", "documentation": "https://www.home-assistant.io/integrations/watson_tts", - "requirements": [ - "ibm-watson==4.0.1" - ], + "requirements": ["ibm-watson==4.0.1"], "dependencies": [], - "codeowners": [ - "@rutkai" - ] + "codeowners": ["@rutkai"] } diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index 520151cf20e..74803464484 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,8 +1,8 @@ """Support for IBM Watson TTS integration.""" import logging -from ibm_watson import TextToSpeechV1 from ibm_cloud_sdk_core.authenticators import IAMAuthenticator +from ibm_watson import TextToSpeechV1 import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider diff --git a/homeassistant/components/waze_travel_time/manifest.json b/homeassistant/components/waze_travel_time/manifest.json index 32083ca8ca8..b34c4f88191 100644 --- a/homeassistant/components/waze_travel_time/manifest.json +++ b/homeassistant/components/waze_travel_time/manifest.json @@ -1,10 +1,8 @@ { "domain": "waze_travel_time", - "name": "Waze travel time", + "name": "Waze Travel Time", "documentation": "https://www.home-assistant.io/integrations/waze_travel_time", - "requirements": [ - "WazeRouteCalculator==0.12" - ], + "requirements": ["WazeRouteCalculator==0.12"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index bdeedd4cd6b..01ca8ed6790 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -11,7 +11,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weather/manifest.json b/homeassistant/components/weather/manifest.json index 568ce57ed62..4e6290a8c69 100644 --- a/homeassistant/components/weather/manifest.json +++ b/homeassistant/components/weather/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/weather", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 5a41bfa9851..217baf42f3a 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,14 +1,14 @@ """Webhooks for Home Assistant.""" import logging +import secrets -from aiohttp.web import Response, Request +from aiohttp.web import Request, Response import voluptuous as vol -from homeassistant.core import callback -from homeassistant.loader import bind_hass -from homeassistant.auth.util import generate_secret from homeassistant.components import websocket_api from homeassistant.components.http.view import HomeAssistantView +from homeassistant.core import callback +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) @@ -46,7 +46,7 @@ def async_unregister(hass, webhook_id): @callback def async_generate_id(): """Generate a webhook_id.""" - return generate_secret(entropy=32) + return secrets.token_hex(32) @callback diff --git a/homeassistant/components/webhook/manifest.json b/homeassistant/components/webhook/manifest.json index f93d8a5e295..ff31698fefc 100644 --- a/homeassistant/components/webhook/manifest.json +++ b/homeassistant/components/webhook/manifest.json @@ -3,8 +3,6 @@ "name": "Webhook", "documentation": "https://www.home-assistant.io/integrations/webhook", "requirements": [], - "dependencies": [ - "http" - ], + "dependencies": ["http"], "codeowners": [] } diff --git a/homeassistant/components/weblink/__init__.py b/homeassistant/components/weblink/__init__.py index a6ee72fa147..be6814da30c 100644 --- a/homeassistant/components/weblink/__init__.py +++ b/homeassistant/components/weblink/__init__.py @@ -3,10 +3,10 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_ICON, CONF_URL +from homeassistant.const import CONF_ICON, CONF_NAME, CONF_URL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/weblink/manifest.json b/homeassistant/components/weblink/manifest.json index 7cdf8bea4ff..28ffa581bb8 100644 --- a/homeassistant/components/weblink/manifest.json +++ b/homeassistant/components/weblink/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/weblink", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/webostv/__init__.py b/homeassistant/components/webostv/__init__.py index f0b3c2c5f7e..e03fea68fd7 100644 --- a/homeassistant/components/webostv/__init__.py +++ b/homeassistant/components/webostv/__init__.py @@ -1 +1,183 @@ """Support for WebOS TV.""" +import asyncio +import logging + +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException, WebOsClient +import voluptuous as vol +from websockets.exceptions import ConnectionClosed + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CUSTOMIZE, + CONF_HOST, + CONF_ICON, + CONF_NAME, + EVENT_HOMEASSISTANT_STOP, +) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send + +DOMAIN = "webostv" + +CONF_SOURCES = "sources" +CONF_ON_ACTION = "turn_on_action" +CONF_STANDBY_CONNECTION = "standby_connection" +DEFAULT_NAME = "LG webOS Smart TV" +WEBOSTV_CONFIG_FILE = "webostv.conf" + +SERVICE_BUTTON = "button" +ATTR_BUTTON = "button" + +SERVICE_COMMAND = "command" +ATTR_COMMAND = "command" + +CUSTOMIZE_SCHEMA = vol.Schema( + {vol.Optional(CONF_SOURCES, default=[]): vol.All(cv.ensure_list, [cv.string])} +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional( + CONF_STANDBY_CONNECTION, default=False + ): cv.boolean, + vol.Optional(CONF_ICON): cv.string, + } + ) + ], + ) + }, + extra=vol.ALLOW_EXTRA, +) + +CALL_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.comp_entity_ids}) + +BUTTON_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_BUTTON): cv.string}) + +COMMAND_SCHEMA = CALL_SCHEMA.extend({vol.Required(ATTR_COMMAND): cv.string}) + +SERVICE_TO_METHOD = { + SERVICE_BUTTON: {"method": "async_button", "schema": BUTTON_SCHEMA}, + SERVICE_COMMAND: {"method": "async_command", "schema": COMMAND_SCHEMA}, +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the LG WebOS TV platform.""" + hass.data[DOMAIN] = {} + + async def async_service_handler(service): + method = SERVICE_TO_METHOD.get(service.service) + data = service.data.copy() + data["method"] = method["method"] + async_dispatcher_send(hass, DOMAIN, data) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]["schema"] + hass.services.async_register( + DOMAIN, service, async_service_handler, schema=schema + ) + + tasks = [async_setup_tv(hass, config, conf) for conf in config[DOMAIN]] + if tasks: + await asyncio.gather(*tasks) + + return True + + +async def async_setup_tv(hass, config, conf): + """Set up a LG WebOS TV based on host parameter.""" + + host = conf[CONF_HOST] + config_file = hass.config.path(WEBOSTV_CONFIG_FILE) + standby_connection = conf[CONF_STANDBY_CONNECTION] + + client = WebOsClient(host, config_file, standby_connection=standby_connection) + hass.data[DOMAIN][host] = {"client": client} + + if client.is_registered(): + await async_setup_tv_finalize(hass, config, conf, client) + else: + _LOGGER.warning("LG webOS TV %s needs to be paired", host) + await async_request_configuration(hass, config, conf, client) + + +async def async_connect(client): + """Attempt a connection, but fail gracefully if tv is off for example.""" + try: + await client.connect() + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVPairException, + PyLGTVCmdException, + ): + pass + + +async def async_setup_tv_finalize(hass, config, conf, client): + """Make initial connection attempt and call platform setup.""" + + async def async_on_stop(event): + """Unregister callbacks and disconnect.""" + client.clear_state_update_callbacks() + await client.disconnect() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_on_stop) + + await async_connect(client) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("media_player", DOMAIN, conf, config) + ) + hass.async_create_task( + hass.helpers.discovery.async_load_platform("notify", DOMAIN, conf, config) + ) + + +async def async_request_configuration(hass, config, conf, client): + """Request configuration steps from the user.""" + host = conf.get(CONF_HOST) + name = conf.get(CONF_NAME) + configurator = hass.components.configurator + + async def lgtv_configuration_callback(data): + """Handle actions when configuration callback is called.""" + try: + await client.connect() + except PyLGTVPairException: + _LOGGER.warning("Connected to LG webOS TV %s but not paired", host) + return + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ): + _LOGGER.error("Unable to connect to host %s", host) + return + + await async_setup_tv_finalize(hass, config, conf, client) + configurator.async_request_done(request_id) + + request_id = configurator.async_request_config( + name, + lgtv_configuration_callback, + description="Click start and accept the pairing request on your TV.", + description_image="/static/images/config_webos.png", + submit_caption="Start pairing request", + ) diff --git a/homeassistant/components/webostv/manifest.json b/homeassistant/components/webostv/manifest.json index dcf908cd603..ddd7be6f3da 100644 --- a/homeassistant/components/webostv/manifest.json +++ b/homeassistant/components/webostv/manifest.json @@ -1,11 +1,8 @@ { "domain": "webostv", - "name": "Webostv", + "name": "LG webOS Smart TV", "documentation": "https://www.home-assistant.io/integrations/webostv", - "requirements": [ - "pylgtv==0.1.9", - "websockets==6.0" - ], + "requirements": ["aiopylgtv==0.2.6"], "dependencies": ["configurator"], - "codeowners": [] + "codeowners": ["@bendavid"] } diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py index 3bf0011907d..4652d6385c1 100644 --- a/homeassistant/components/webostv/media_player.py +++ b/homeassistant/components/webostv/media_player.py @@ -1,16 +1,14 @@ """Support for interface with an LG webOS Smart TV.""" import asyncio from datetime import timedelta +from functools import wraps import logging -from typing import Dict -from urllib.parse import urlparse -from pylgtv import PyLGTVPairException, WebOsClient -import voluptuous as vol +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException from websockets.exceptions import ConnectionClosed from homeassistant import util -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, SUPPORT_NEXT_TRACK, @@ -26,28 +24,24 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CUSTOMIZE, - CONF_FILENAME, CONF_HOST, CONF_NAME, - CONF_TIMEOUT, + ENTITY_MATCH_ALL, STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, + STATE_ON, ) -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.script import Script -_CONFIGURING: Dict[str, str] = {} +from . import CONF_ON_ACTION, CONF_SOURCES, DOMAIN + _LOGGER = logging.getLogger(__name__) -CONF_SOURCES = "sources" -CONF_ON_ACTION = "turn_on_action" -DEFAULT_NAME = "LG webOS Smart TV" LIVETV_APP_ID = "com.webos.app.livetv" -WEBOSTV_CONFIG_FILE = "webostv.conf" SUPPORT_WEBOSTV = ( SUPPORT_TURN_OFF @@ -65,199 +59,166 @@ SUPPORT_WEBOSTV = ( MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) -CUSTOMIZE_SCHEMA = vol.Schema( - {vol.Optional(CONF_SOURCES): vol.All(cv.ensure_list, [cv.string])} -) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_CUSTOMIZE, default={}): CUSTOMIZE_SCHEMA, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_ON_ACTION): cv.SCRIPT_SCHEMA, - vol.Optional(CONF_TIMEOUT, default=8): cv.positive_int, - } -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the LG WebOS TV platform.""" - if discovery_info is not None: - host = urlparse(discovery_info[1]).hostname - else: - host = config.get(CONF_HOST) - if host is None: - _LOGGER.error("No TV found in configuration file or with discovery") - return False - - # Only act if we are not already configuring this host - if host in _CONFIGURING: + if discovery_info is None: return - name = config.get(CONF_NAME) - customize = config.get(CONF_CUSTOMIZE) - timeout = config.get(CONF_TIMEOUT) - turn_on_action = config.get(CONF_ON_ACTION) + host = discovery_info[CONF_HOST] + name = discovery_info[CONF_NAME] + customize = discovery_info[CONF_CUSTOMIZE] + turn_on_action = discovery_info.get(CONF_ON_ACTION) - config = hass.config.path(config.get(CONF_FILENAME)) + client = hass.data[DOMAIN][host]["client"] + on_script = Script(hass, turn_on_action) if turn_on_action else None - setup_tv(host, name, customize, config, timeout, hass, add_entities, turn_on_action) + entity = LgWebOSMediaPlayerEntity(client, name, customize, on_script) + + async_add_entities([entity], update_before_add=False) -def setup_tv( - host, name, customize, config, timeout, hass, add_entities, turn_on_action -): - """Set up a LG WebOS TV based on host parameter.""" +def cmd(func): + """Catch command exceptions.""" - client = WebOsClient(host, config, timeout) - - if not client.is_registered(): - if host in _CONFIGURING: - # Try to pair. - try: - client.register() - except PyLGTVPairException: - _LOGGER.warning("Connected to LG webOS TV %s but not paired", host) - return - except (OSError, ConnectionClosed, asyncio.TimeoutError): - _LOGGER.error("Unable to connect to host %s", host) - return - else: - # Not registered, request configuration. - _LOGGER.warning("LG webOS TV %s needs to be paired", host) - request_configuration( - host, - name, - customize, - config, - timeout, - hass, - add_entities, - turn_on_action, + @wraps(func) + async def wrapper(obj, *args, **kwargs): + """Wrap all command methods.""" + try: + await func(obj, *args, **kwargs) + except ( + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ) as exc: + # If TV is off, we expect calls to fail. + if obj.state == STATE_OFF: + level = logging.INFO + else: + level = logging.ERROR + _LOGGER.log( + level, + "Error calling %s on entity %s: %r", + func.__name__, + obj.entity_id, + exc, ) - return - # If we came here and configuring this host, mark as done. - if client.is_registered() and host in _CONFIGURING: - request_id = _CONFIGURING.pop(host) - configurator = hass.components.configurator - configurator.request_done(request_id) - - add_entities( - [LgWebOSDevice(host, name, customize, config, timeout, hass, turn_on_action)], - True, - ) + return wrapper -def request_configuration( - host, name, customize, config, timeout, hass, add_entities, turn_on_action -): - """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 pair, please try again." - ) - return - - def lgtv_configuration_callback(data): - """Handle actions when configuration callback is called.""" - setup_tv( - host, name, customize, config, timeout, hass, add_entities, turn_on_action - ) - - _CONFIGURING[host] = configurator.request_config( - name, - lgtv_configuration_callback, - description="Click start and accept the pairing request on your TV.", - description_image="/static/images/config_webos.png", - submit_caption="Start pairing request", - ) - - -class LgWebOSDevice(MediaPlayerDevice): +class LgWebOSMediaPlayerEntity(MediaPlayerDevice): """Representation of a LG WebOS TV.""" - def __init__(self, host, name, customize, config, timeout, hass, on_action): + def __init__(self, client, name, customize, on_script=None): """Initialize the webos device.""" - - self._client = WebOsClient(host, config, timeout) - self._on_script = Script(hass, on_action) if on_action else None - self._customize = customize - + self._client = client self._name = name + self._customize = customize + self._on_script = on_script + # Assume that the TV is not muted self._muted = False - # Assume that the TV is in Play mode - self._playing = True self._volume = 0 self._current_source = None self._current_source_id = None self._state = None self._source_list = {} self._app_list = {} + self._input_list = {} self._channel = None self._last_icon = None - @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) - def update(self): - """Retrieve the latest data.""" + async def async_added_to_hass(self): + """Connect and subscribe to dispatcher signals and state updates.""" + async_dispatcher_connect(self.hass, DOMAIN, self.async_signal_handler) - try: - current_input = self._client.get_input() - if current_input is not None: - self._current_source_id = current_input - if self._state in (None, STATE_OFF): - self._state = STATE_PLAYING - else: - self._state = STATE_OFF - self._current_source = None - self._current_source_id = None - self._channel = None + await self._client.register_state_update_callback( + self.async_handle_state_update + ) - if self._state is not STATE_OFF: - self._muted = self._client.get_muted() - self._volume = self._client.get_volume() - self._channel = self._client.get_current_channel() + # force state update if needed + if self._state is None: + await self.async_handle_state_update() - self._source_list = {} - self._app_list = {} - conf_sources = self._customize.get(CONF_SOURCES, []) + async def async_will_remove_from_hass(self): + """Call disconnect on removal.""" + self._client.unregister_state_update_callback(self.async_handle_state_update) - for app in self._client.get_apps(): - self._app_list[app["id"]] = app - if app["id"] == self._current_source_id: - self._current_source = app["title"] - self._source_list[app["title"]] = app - elif ( - not conf_sources - or app["id"] in conf_sources - or any(word in app["title"] for word in conf_sources) - or any(word in app["id"] for word in conf_sources) - ): - self._source_list[app["title"]] = app + async def async_signal_handler(self, data): + """Handle domain-specific signal by calling appropriate method.""" + entity_ids = data[ATTR_ENTITY_ID] + if entity_ids == ENTITY_MATCH_ALL or self.entity_id in entity_ids: + params = { + key: value + for key, value in data.items() + if key not in ["entity_id", "method"] + } + await getattr(self, data["method"])(**params) - for source in self._client.get_inputs(): - if source["id"] == self._current_source_id: - self._current_source = source["label"] - self._source_list[source["label"]] = source - elif ( - not conf_sources - or source["label"] in conf_sources - or any( - source["label"].find(word) != -1 for word in conf_sources - ) - ): - self._source_list[source["label"]] = source - except (OSError, ConnectionClosed, TypeError, asyncio.TimeoutError): + async def async_handle_state_update(self): + """Update state from WebOsClient.""" + self._current_source_id = self._client.current_appId + self._muted = self._client.muted + self._volume = self._client.volume + self._channel = self._client.current_channel + self._app_list = self._client.apps + self._input_list = self._client.inputs + + if self._current_source_id == "": self._state = STATE_OFF - self._current_source = None - self._current_source_id = None - self._channel = None + else: + self._state = STATE_ON + + self.update_sources() + + self.async_schedule_update_ha_state(False) + + def update_sources(self): + """Update list of sources from current source, apps, inputs and configured list.""" + self._source_list = {} + conf_sources = self._customize[CONF_SOURCES] + + for app in self._app_list.values(): + if app["id"] == self._current_source_id: + self._current_source = app["title"] + self._source_list[app["title"]] = app + elif ( + not conf_sources + or app["id"] in conf_sources + or any(word in app["title"] for word in conf_sources) + or any(word in app["id"] for word in conf_sources) + ): + self._source_list[app["title"]] = app + + for source in self._input_list.values(): + if source["appId"] == self._current_source_id: + self._current_source = source["label"] + self._source_list[source["label"]] = source + elif ( + not conf_sources + or source["label"] in conf_sources + or any(source["label"].find(word) != -1 for word in conf_sources) + ): + self._source_list[source["label"]] = source + + @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) + async def async_update(self): + """Connect.""" + if not self._client.is_connected(): + try: + await self._client.connect() + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVPairException, + PyLGTVCmdException, + ): + pass @property def name(self): @@ -326,60 +287,62 @@ class LgWebOSDevice(MediaPlayerDevice): return SUPPORT_WEBOSTV | SUPPORT_TURN_ON return SUPPORT_WEBOSTV - def turn_off(self): + @cmd + async def async_turn_off(self): """Turn off media player.""" + await self._client.power_off() - self._state = STATE_OFF - try: - self._client.power_off() - except (OSError, ConnectionClosed, TypeError, asyncio.TimeoutError): - pass - - def turn_on(self): + async def async_turn_on(self): """Turn on the media player.""" + connected = self._client.is_connected() if self._on_script: - self._on_script.run() + await self._on_script.async_run() - def volume_up(self): + # if connection was already active + # ensure is still alive + if connected: + await self._client.get_current_app() + + @cmd + async def async_volume_up(self): """Volume up the media player.""" - self._client.volume_up() + await self._client.volume_up() - def volume_down(self): + @cmd + async def async_volume_down(self): """Volume down media player.""" - self._client.volume_down() + await self._client.volume_down() - def set_volume_level(self, volume): + @cmd + async def async_set_volume_level(self, volume): """Set volume level, range 0..1.""" tv_volume = volume * 100 - self._client.set_volume(tv_volume) + await self._client.set_volume(tv_volume) - def mute_volume(self, mute): + @cmd + async def async_mute_volume(self, mute): """Send mute command.""" - self._muted = mute - self._client.set_mute(mute) + await self._client.set_mute(mute) - def media_play_pause(self): - """Simulate play pause media player.""" - if self._playing: - self.media_pause() - else: - self.media_play() + @cmd + async def async_media_play_pause(self): + """Client pause command acts as a play-pause toggle.""" + await self._client.pause() - def select_source(self, source): + @cmd + async def async_select_source(self, source): """Select input source.""" source_dict = self._source_list.get(source) if source_dict is None: _LOGGER.warning("Source %s not found for %s", source, self.name) return - self._current_source_id = source_dict["id"] if source_dict.get("title"): - self._current_source = source_dict["title"] - self._client.launch_app(source_dict["id"]) + await self._client.launch_app(source_dict["id"]) elif source_dict.get("label"): - self._current_source = source_dict["label"] - self._client.set_input(source_dict["id"]) + await self._client.set_input(source_dict["id"]) - def play_media(self, media_type, media_id, **kwargs): + @cmd + async def async_play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id) @@ -405,40 +368,53 @@ class LgWebOSDevice(MediaPlayerDevice): "Switching to channel <%s> with perfect match", perfect_match_channel_id, ) - self._client.set_channel(perfect_match_channel_id) + await self._client.set_channel(perfect_match_channel_id) elif partial_match_channel_id is not None: _LOGGER.info( "Switching to channel <%s> with partial match", partial_match_channel_id, ) - self._client.set_channel(partial_match_channel_id) + await self._client.set_channel(partial_match_channel_id) - return - - def media_play(self): + @cmd + async def async_media_play(self): """Send play command.""" - self._playing = True - self._state = STATE_PLAYING - self._client.play() + await self._client.play() - def media_pause(self): + @cmd + async def async_media_pause(self): """Send media pause command to media player.""" - self._playing = False - self._state = STATE_PAUSED - self._client.pause() + await self._client.pause() - def media_next_track(self): + @cmd + async def async_media_stop(self): + """Send stop command to media player.""" + await self._client.stop() + + @cmd + async def async_media_next_track(self): """Send next track command.""" current_input = self._client.get_input() if current_input == LIVETV_APP_ID: - self._client.channel_up() + await self._client.channel_up() else: - self._client.fast_forward() + await self._client.fast_forward() - def media_previous_track(self): + @cmd + async def async_media_previous_track(self): """Send the previous track command.""" current_input = self._client.get_input() if current_input == LIVETV_APP_ID: - self._client.channel_down() + await self._client.channel_down() else: - self._client.rewind() + await self._client.rewind() + + @cmd + async def async_button(self, button): + """Send a button press.""" + await self._client.button(button) + + @cmd + async def async_command(self, command): + """Send a command.""" + await self._client.request(command) diff --git a/homeassistant/components/webostv/notify.py b/homeassistant/components/webostv/notify.py index f62c41e9a95..e75fafbfe23 100644 --- a/homeassistant/components/webostv/notify.py +++ b/homeassistant/components/webostv/notify.py @@ -1,47 +1,29 @@ """Support for LG WebOS TV notification service.""" +import asyncio import logging -from pylgtv import PyLGTVPairException, WebOsClient -import voluptuous as vol +from aiopylgtv import PyLGTVCmdException, PyLGTVPairException +from websockets.exceptions import ConnectionClosed -from homeassistant.components.notify import ( - ATTR_DATA, - PLATFORM_SCHEMA, - BaseNotificationService, -) -from homeassistant.const import CONF_FILENAME, CONF_HOST, CONF_ICON -import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ATTR_DATA, BaseNotificationService +from homeassistant.const import CONF_HOST, CONF_ICON + +from . import DOMAIN _LOGGER = logging.getLogger(__name__) -WEBOSTV_CONFIG_FILE = "webostv.conf" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_FILENAME, default=WEBOSTV_CONFIG_FILE): cv.string, - vol.Optional(CONF_ICON): cv.string, - } -) - - -def get_service(hass, config, discovery_info=None): +async def async_get_service(hass, config, discovery_info=None): """Return the notify service.""" - path = hass.config.path(config.get(CONF_FILENAME)) - client = WebOsClient(config.get(CONF_HOST), key_file_path=path, timeout_connect=8) + host = discovery_info.get(CONF_HOST) + icon_path = discovery_info.get(CONF_ICON) - if not client.is_registered(): - try: - client.register() - except PyLGTVPairException: - _LOGGER.error("Pairing with TV failed") - return None - except OSError: - _LOGGER.error("TV unreachable") - return None + client = hass.data[DOMAIN][host]["client"] - return LgWebOSNotificationService(client, config.get(CONF_ICON)) + svc = LgWebOSNotificationService(client, icon_path) + + return svc class LgWebOSNotificationService(BaseNotificationService): @@ -52,18 +34,27 @@ class LgWebOSNotificationService(BaseNotificationService): self._client = client self._icon_path = icon_path - def send_message(self, message="", **kwargs): + async def async_send_message(self, message="", **kwargs): """Send a message to the tv.""" - try: + if not self._client.is_connected(): + await self._client.connect() + data = kwargs.get(ATTR_DATA) icon_path = ( data.get(CONF_ICON, self._icon_path) if data else self._icon_path ) - self._client.send_message(message, icon_path=icon_path) + await self._client.send_message(message, icon_path=icon_path) except PyLGTVPairException: _LOGGER.error("Pairing with TV failed") except FileNotFoundError: _LOGGER.error("Icon %s not found", icon_path) - except OSError: + except ( + OSError, + ConnectionClosed, + ConnectionRefusedError, + asyncio.TimeoutError, + asyncio.CancelledError, + PyLGTVCmdException, + ): _LOGGER.error("TV unreachable") diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml new file mode 100644 index 00000000000..137a6026eda --- /dev/null +++ b/homeassistant/components/webostv/services.yaml @@ -0,0 +1,26 @@ +# Describes the format for available webostv services + +button: + description: 'Send a button press command.' + fields: + entity_id: + description: Name(s) of the webostv entities where to run the API method. + example: 'media_player.living_room_tv' + button: + description: Name of the button to press. Known possible values are + LEFT, RIGHT, DOWN, UP, HOME, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, + MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + example: 'LEFT' + +command: + description: 'Send a command.' + fields: + entity_id: + description: Name(s) of the webostv entities where to run the API method. + example: 'media_player.living_room_tv' + command: + description: Endpoint of the command. Known valid endpoints are listed in + https://github.com/TheRealLink/pylgtv/blob/master/pylgtv/endpoints.py + example: 'media.controls/rewind' + diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index 1ec758ebd4d..60177fcde90 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -1,10 +1,13 @@ """WebSocket based API for Home Assistant.""" -from homeassistant.core import callback +from typing import Optional, Union, cast + +import voluptuous as vol + +from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages - # mypy: allow-untyped-calls, allow-untyped-defs DOMAIN = const.DOMAIN @@ -27,13 +30,18 @@ websocket_command = decorators.websocket_command @bind_hass @callback -def async_register_command(hass, command_or_handler, handler=None, schema=None): +def async_register_command( + hass: HomeAssistant, + command_or_handler: Union[str, const.WebSocketCommandHandler], + handler: Optional[const.WebSocketCommandHandler] = None, + schema: Optional[vol.Schema] = None, +) -> None: """Register a websocket command.""" # pylint: disable=protected-access if handler is None: - handler = command_or_handler - command = handler._ws_command - schema = handler._ws_schema + handler = cast(const.WebSocketCommandHandler, command_or_handler) + command = handler._ws_command # type: ignore + schema = handler._ws_schema # type: ignore else: command = command_or_handler handlers = hass.data.get(DOMAIN) diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 3971d39ee73..9e33ed74fd4 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -3,13 +3,12 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User -from homeassistant.components.http.ban import process_wrong_login, process_success_login +from homeassistant.components.http.ban import process_success_login, process_wrong_login from homeassistant.const import __version__ from .connection import ActiveConnection from .error import Disconnect - # mypy: allow-untyped-calls, allow-untyped-defs TYPE_AUTH = "auth" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index f30ee816914..3e43f824e69 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -2,16 +2,15 @@ import voluptuous as vol from homeassistant.auth.permissions.const import POLICY_READ -from homeassistant.const import MATCH_ALL, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED -from homeassistant.core import callback, DOMAIN as HASS_DOMAIN -from homeassistant.exceptions import Unauthorized, ServiceNotFound, HomeAssistantError +from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL +from homeassistant.core import DOMAIN as HASS_DOMAIN, callback +from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, Unauthorized from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.service import async_get_all_descriptions from . import const, decorators, messages - # mypy: allow-untyped-calls, allow-untyped-defs @@ -45,6 +44,8 @@ def handle_subscribe_events(hass, connection, msg): Async friendly. """ + # Circular dep + # pylint: disable=import-outside-toplevel from .permissions import SUBSCRIBE_WHITELIST event_type = msg["event_type"] @@ -106,7 +107,6 @@ def handle_unsubscribe_events(hass, connection, msg): ) -@decorators.async_response @decorators.websocket_command( { vol.Required("type"): "call_service", @@ -115,6 +115,7 @@ def handle_unsubscribe_events(hass, connection, msg): vol.Optional("service_data"): dict, } ) +@decorators.async_response async def handle_call_service(hass, connection, msg): """Handle call service command. @@ -180,8 +181,8 @@ def handle_get_states(hass, connection, msg): connection.send_message(messages.result_message(msg["id"], states)) -@decorators.async_response @decorators.websocket_command({vol.Required("type"): "get_services"}) +@decorators.async_response async def handle_get_services(hass, connection, msg): """Handle get services command. diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 5a0284a34d4..ae2bb16c6d2 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -1,15 +1,14 @@ """Connection session.""" import asyncio -from typing import Any, Callable, Dict, Hashable +from typing import Any, Callable, Dict, Hashable, Optional import voluptuous as vol -from homeassistant.core import callback, Context +from homeassistant.core import Context, callback from homeassistant.exceptions import Unauthorized from . import const, messages - # mypy: allow-untyped-calls, allow-untyped-defs @@ -38,7 +37,7 @@ class ActiveConnection: return Context(user_id=user.id) @callback - def send_result(self, msg_id, result=None): + def send_result(self, msg_id: int, result: Optional[Any] = None) -> None: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) @@ -50,7 +49,7 @@ class ActiveConnection: self.send_message(content) @callback - def send_error(self, msg_id, code, message): + def send_error(self, msg_id: int, code: str, message: str) -> None: """Send a error message.""" self.send_message(messages.error_message(msg_id, code, message)) diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index fe9792c4ab3..b1fa1263a99 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -3,8 +3,20 @@ import asyncio from concurrent import futures from functools import partial import json +from typing import TYPE_CHECKING, Callable + +from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder +if TYPE_CHECKING: + from .connection import ActiveConnection # noqa + + +WebSocketCommandHandler = Callable[ + [HomeAssistant, "ActiveConnection", dict], None +] # pylint: disable=invalid-name + + DOMAIN = "websocket_api" URL = "/api/websocket" MAX_PENDING_MSG = 512 diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index 025131643e8..87b5d5baf92 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -1,12 +1,13 @@ """Decorators for the Websocket API.""" from functools import wraps import logging +from typing import Awaitable, Callable -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized -from . import messages - +from . import const, messages +from .connection import ActiveConnection # mypy: allow-untyped-calls, allow-untyped-defs @@ -21,7 +22,9 @@ async def _handle_async_response(func, hass, connection, msg): connection.async_handle_exception(msg, err) -def async_response(func): +def async_response( + func: Callable[[HomeAssistant, ActiveConnection, dict], Awaitable[None]] +) -> const.WebSocketCommandHandler: """Decorate an async function to handle WebSocket API messages.""" @callback @@ -33,7 +36,7 @@ def async_response(func): return schedule_handler -def require_admin(func): +def require_admin(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Websocket decorator to require user to be an admin.""" @wraps(func) @@ -105,7 +108,9 @@ def ws_require_user( return validator -def websocket_command(schema): +def websocket_command( + schema: dict, +) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Tag a function as a websocket command.""" command = schema["type"] diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index be1830aa07b..3921413fd28 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -4,28 +4,27 @@ from contextlib import suppress import logging from typing import Optional -from aiohttp import web, WSMsgType +from aiohttp import WSMsgType, web import async_timeout +from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView +from .auth import AuthPhase, auth_required_message from .const import ( - MAX_PENDING_MSG, CANCELLATION_ERRORS, - URL, + DATA_CONNECTIONS, ERR_UNKNOWN_ERROR, + JSON_DUMP, + MAX_PENDING_MSG, SIGNAL_WEBSOCKET_CONNECTED, SIGNAL_WEBSOCKET_DISCONNECTED, - DATA_CONNECTIONS, - JSON_DUMP, + URL, ) -from .auth import AuthPhase, auth_required_message from .error import Disconnect from .messages import error_message - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index 9826b11ec45..2751f8343bf 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -1,12 +1,9 @@ { "domain": "websocket_api", - "name": "Websocket api", + "name": "Home Asssitant WebSocket API", "documentation": "https://www.home-assistant.io/integrations/websocket_api", "requirements": [], - "dependencies": [ - "http" - ], - "codeowners": [ - "@home-assistant/core" - ] + "dependencies": ["http"], + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index c8c760a6549..27d557e8110 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -6,7 +6,6 @@ from homeassistant.helpers import config_validation as cv from . import const - # mypy: allow-untyped-defs # Minimal requirements of a message diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index ffbb80fa19e..c270c0f0ccc 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -2,22 +2,22 @@ Separate file to avoid circular imports. """ +from homeassistant.components.frontend import EVENT_PANELS_UPDATED +from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED +from homeassistant.components.persistent_notification import ( + EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, +) from homeassistant.const import ( EVENT_COMPONENT_LOADED, + EVENT_CORE_CONFIG_UPDATE, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_THEMES_UPDATED, - EVENT_CORE_CONFIG_UPDATE, ) -from homeassistant.components.persistent_notification import ( - EVENT_PERSISTENT_NOTIFICATIONS_UPDATED, -) -from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED -from homeassistant.components.frontend import EVENT_PANELS_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index f8f1257aefc..4ae39787335 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -4,12 +4,11 @@ from homeassistant.core import callback from homeassistant.helpers.entity import Entity from .const import ( + DATA_CONNECTIONS, SIGNAL_WEBSOCKET_CONNECTED, SIGNAL_WEBSOCKET_DISCONNECTED, - DATA_CONNECTIONS, ) - # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/wemo/.translations/da.json b/homeassistant/components/wemo/.translations/da.json index c69547c66ab..1da4d407849 100644 --- a/homeassistant/components/wemo/.translations/da.json +++ b/homeassistant/components/wemo/.translations/da.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Ingen Wemo enheder fundet p\u00e5 netv\u00e6rket.", - "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Wemo." + "no_devices_found": "Der blev ikke fundet nogen Wemo-enheder p\u00e5 netv\u00e6rket.", + "single_instance_allowed": "Kun en enkelt konfiguration af Wemo er mulig." }, "step": { "confirm": { diff --git a/homeassistant/components/wemo/.translations/ko.json b/homeassistant/components/wemo/.translations/ko.json index 57515f2c970..cc3a70a0bc6 100644 --- a/homeassistant/components/wemo/.translations/ko.json +++ b/homeassistant/components/wemo/.translations/ko.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wemo \ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "description": "Wemo \ub97c \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Wemo" } }, diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index fe63f10aeba..9b1c4cd465f 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -7,10 +7,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.discovery import SERVICE_WEMO -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery - from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.helpers import config_validation as cv, discovery + from .const import DOMAIN # Mapping from Wemo model_name to component. diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index bc300fde571..6f7c9e7ee2b 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -63,7 +63,7 @@ class WemoBinarySensor(BinarySensorDevice): self.async_schedule_update_ha_state() async def async_added_to_hass(self): - """Wemo sensor added to HASS.""" + """Wemo sensor added to Home Assistant.""" # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index 21c911a66ce..9ad7dda10ba 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -2,8 +2,8 @@ import pywemo -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow from . import DOMAIN diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index 4a8be4fba81..ac1e202f38d 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -1,24 +1,24 @@ """Support for WeMo humidifier.""" import asyncio -import logging from datetime import timedelta +import logging import async_timeout from pywemo import discovery import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( - SUPPORT_SET_SPEED, - FanEntity, - SPEED_OFF, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, ) -from homeassistant.exceptions import PlatformNotReady from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from . import SUBSCRIPTION_REGISTRY from .const import DOMAIN, SERVICE_RESET_FILTER_LIFE, SERVICE_SET_HUMIDITY @@ -232,7 +232,7 @@ class WemoHumidifier(FanEntity): return SUPPORTED_FEATURES async def async_added_to_hass(self): - """Wemo humidifier added to HASS.""" + """Wemo humidifier added to Home Assistant.""" # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index dab96eb8c94..59b6d9e390e 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -1,7 +1,7 @@ """Support for Belkin WeMo lights.""" import asyncio -import logging from datetime import timedelta +import logging import async_timeout from pywemo import discovery @@ -9,15 +9,15 @@ import requests from homeassistant import util from homeassistant.components.light import ( - Light, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, + Light, ) from homeassistant.exceptions import PlatformNotReady import homeassistant.util.color as color_util @@ -96,7 +96,7 @@ class WemoLight(Light): self._unique_id = self.wemo.uniqueID async def async_added_to_hass(self): - """Wemo light added to HASS.""" + """Wemo light added to Home Assistant.""" # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() @@ -231,7 +231,7 @@ class WemoDimmer(Light): self.async_schedule_update_ha_state() async def async_added_to_hass(self): - """Wemo dimmer added to HASS.""" + """Wemo dimmer added to Home Assistant.""" # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index 3b43def230f..017d43f7aba 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -1,23 +1,17 @@ { "domain": "wemo", - "name": "Wemo", + "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": [ - "pywemo==0.4.34" - ], + "requirements": ["pywemo==0.4.34"], "ssdp": [ { "manufacturer": "Belkin International Inc." } ], "homekit": { - "models": [ - "Wemo" - ] + "models": ["Wemo"] }, "dependencies": [], - "codeowners": [ - "@sqldiablo" - ] + "codeowners": ["@sqldiablo"] } diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 1c0606b489d..531ac34ce92 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -1,16 +1,16 @@ """Support for WeMo switches.""" import asyncio -import logging from datetime import datetime, timedelta +import logging import async_timeout from pywemo import discovery import requests from homeassistant.components.switch import SwitchDevice +from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from homeassistant.exceptions import PlatformNotReady from homeassistant.util import convert -from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY, STATE_UNKNOWN from . import SUBSCRIPTION_REGISTRY from .const import DOMAIN @@ -196,7 +196,7 @@ class WemoSwitch(SwitchDevice): self.wemo.off() async def async_added_to_hass(self): - """Wemo switch added to HASS.""" + """Wemo switch added to Home Assistant.""" # Define inside async context so we know our event loop self._update_lock = asyncio.Lock() diff --git a/homeassistant/components/whois/manifest.json b/homeassistant/components/whois/manifest.json index 1566366362a..6e383ec1467 100644 --- a/homeassistant/components/whois/manifest.json +++ b/homeassistant/components/whois/manifest.json @@ -2,9 +2,7 @@ "domain": "whois", "name": "Whois", "documentation": "https://www.home-assistant.io/integrations/whois", - "requirements": [ - "python-whois==0.7.2" - ], + "requirements": ["python-whois==0.7.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 3c78d80ba92..dc9da1100f0 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "WHOIS lookup for %s didn't contain an expiration date", domain ) return - except whois.BaseException as ex: + except whois.BaseException as ex: # pylint: disable=broad-except _LOGGER.error("Exception %s occurred during WHOIS lookup for %s", ex, domain) return @@ -96,7 +96,7 @@ class WhoisSensor(Entity): """Get the current WHOIS data for the domain.""" try: response = self.whois(self._domain) - except whois.BaseException as ex: + except whois.BaseException as ex: # pylint: disable=broad-except _LOGGER.error("Exception %s occurred during WHOIS lookup", ex) self._empty_state_and_attributes() return diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index 3bda8b314f2..ceeb8b4cbc0 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -6,8 +6,8 @@ import os import time from aiohttp.web import Response -import pywink from pubnubsubhandler import PubNubSubscriptionHandler +import pywink import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -695,7 +695,7 @@ class WinkAuthCallbackView(HomeAssistantView): response_message = """Wink has been successfully authorized! You can close this window now! For the best results you should reboot - HomeAssistant""" + Home Assistant""" html_response = """Wink Auth

{}

""" @@ -740,7 +740,7 @@ class WinkDevice(Entity): try: if message is None: _LOGGER.error( - "Error on pubnub update for %s " "polling API for current state", + "Error on pubnub update for %s polling API for current state", self.name, ) self.schedule_update_ha_state(True) @@ -749,8 +749,7 @@ class WinkDevice(Entity): self.schedule_update_ha_state() except (ValueError, KeyError, AttributeError): _LOGGER.error( - "Error in pubnub JSON for %s " "polling API for current state", - self.name, + "Error in pubnub JSON for %s polling API for current state", self.name, ) self.schedule_update_ha_state(True) diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 6323fa7bbfe..85d477313f1 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -23,12 +23,12 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO, + PRESET_NONE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, - SUPPORT_PRESET_MODE, - PRESET_NONE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS from homeassistant.helpers.temperature import display_temp as show_temp diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index acf9c38e632..a1bae648292 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -2,10 +2,7 @@ "domain": "wink", "name": "Wink", "documentation": "https://www.home-assistant.io/integrations/wink", - "requirements": [ - "pubnubsub-handler==1.0.8", - "python-wink==1.10.5" - ], - "dependencies": ["configurator"], + "requirements": ["pubnubsub-handler==1.0.8", "python-wink==1.10.5"], + "dependencies": ["configurator", "http"], "codeowners": [] } diff --git a/homeassistant/components/wirelesstag/__init__.py b/homeassistant/components/wirelesstag/__init__.py index 1bc971f1372..c0a30a8867f 100644 --- a/homeassistant/components/wirelesstag/__init__.py +++ b/homeassistant/components/wirelesstag/__init__.py @@ -199,7 +199,7 @@ def setup(hass, config): except (ConnectTimeout, HTTPError, WirelessTagsException) as ex: _LOGGER.error("Unable to connect to wirelesstag.net service: %s", str(ex)) hass.components.persistent_notification.create( - "Error: {}
" "Please restart hass after fixing this." "".format(ex), + "Error: {}
Please restart hass after fixing this.".format(ex), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID, ) diff --git a/homeassistant/components/wirelesstag/manifest.json b/homeassistant/components/wirelesstag/manifest.json index 7472898b7ca..9320b9a9e73 100644 --- a/homeassistant/components/wirelesstag/manifest.json +++ b/homeassistant/components/wirelesstag/manifest.json @@ -1,10 +1,8 @@ { "domain": "wirelesstag", - "name": "Wirelesstag", + "name": "Wireless Sensor Tags", "documentation": "https://www.home-assistant.io/integrations/wirelesstag", - "requirements": [ - "wirelesstagpy==0.4.0" - ], + "requirements": ["wirelesstagpy==0.4.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json index d2dddbbd204..e4599fe8ec2 100644 --- a/homeassistant/components/withings/.translations/da.json +++ b/homeassistant/components/withings/.translations/da.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "no_flows": "Du skal konfigurere Withings, f\u00f8r du kan godkende med den. L\u00e6s venligst dokumentationen." + }, "create_entry": { "default": "Godkendt med Withings for den valgte profil." }, "step": { + "profile": { + "data": { + "profile": "Profile" + }, + "description": "Hvilken profil har du valgt p\u00e5 Withings hjemmeside? Det er vigtigt, at profilerne matcher, ellers vil data blive m\u00e6rket forkert.", + "title": "Brugerprofil." + }, "user": { "data": { "profile": "Profil" diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index 557518d97e8..ccd5f3f41fd 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -11,6 +11,7 @@ "data": { "profile": "Profil" }, + "description": "Welches Profil haben Sie auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.", "title": "Benutzerprofil" }, "user": { diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 482c4e96e5c..92c3f2ae155 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -7,11 +7,11 @@ import voluptuous as vol from withings_api import WithingsAuth from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from . import config_flow, const -from .common import _LOGGER, get_data_manager, NotAuthenticatedError +from .common import _LOGGER, NotAuthenticatedError, get_data_manager DOMAIN = const.DOMAIN diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 655776ae004..9cba055bac4 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -1,4 +1,5 @@ """Common code for Withings.""" +from asyncio import run_coroutine_threadsafe import datetime from functools import partial import logging @@ -6,15 +7,14 @@ import re import time from typing import Any, Dict -from asyncio import run_coroutine_threadsafe import requests from withings_api import ( AbstractWithingsApi, - SleepGetResponse, MeasureGetMeasResponse, + SleepGetResponse, SleepGetSummaryResponse, ) -from withings_api.common import UnauthorizedException, AuthFailedException +from withings_api.common import AuthFailedException, UnauthorizedException from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -51,7 +51,7 @@ class ThrottleData: """Throttle data.""" def __init__(self, interval: int, data: Any): - """Constructor.""" + """Initialize throttle data.""" self._time = int(time.time()) self._interval = interval self._data = data @@ -126,7 +126,7 @@ class WithingsDataManager: service_available = None def __init__(self, hass: HomeAssistant, profile: str, api: ConfigEntryWithingsApi): - """Constructor.""" + """Initialize data manager.""" self._hass = hass self._api = api self._profile = profile @@ -259,8 +259,8 @@ class WithingsDataManager: async def update_sleep(self) -> SleepGetResponse: """Update the sleep data.""" - end_date = int(time.time()) - start_date = end_date - (6 * 60 * 60) + end_date = dt.now() + start_date = end_date - datetime.timedelta(hours=2) def function(): return self._api.sleep_get(startdate=start_date, enddate=end_date) diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index 1f2169363e9..337a98ab404 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -3,15 +3,7 @@ "name": "Withings", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", - "requirements": [ - "withings-api==2.1.3" - ], - "dependencies": [ - "api", - "http", - "webhook" - ], - "codeowners": [ - "@vangorra" - ] -} \ No newline at end of file + "requirements": ["withings-api==2.1.3"], + "dependencies": ["api", "http", "webhook"], + "codeowners": ["@vangorra"] +} diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 17eae93ec0d..0bb7be16f8e 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -2,21 +2,21 @@ from typing import Callable, List, Union from withings_api.common import ( - MeasureType, GetSleepSummaryField, MeasureGetMeasResponse, + MeasureGroupAttribs, + MeasureType, SleepGetResponse, SleepGetSummaryResponse, - get_measure_value, - MeasureGroupAttribs, SleepState, + get_measure_value, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from homeassistant.helpers import config_entry_oauth2_flow from . import const from .common import _LOGGER, WithingsDataManager, get_data_manager @@ -55,7 +55,7 @@ class WithingsAttribute: unit_of_measurement: str, icon: str, ) -> None: - """Constructor.""" + """Initialize attribute.""" self.measurement = measurement self.measure_type = measure_type self.friendly_name = friendly_name @@ -73,7 +73,7 @@ class WithingsSleepStateAttribute(WithingsAttribute): def __init__( self, measurement: str, friendly_name: str, unit_of_measurement: str, icon: str ) -> None: - """Constructor.""" + """Initialize sleep state attribute.""" super().__init__(measurement, None, friendly_name, unit_of_measurement, icon) @@ -309,7 +309,7 @@ class WithingsHealthSensor(Entity): @property def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" + """Return a unique, Home Assistant friendly identifier for this entity.""" return "withings_{}_{}_{}".format( self._slug, self._user_id, slugify(self._attribute.measurement) ) @@ -382,7 +382,8 @@ class WithingsHealthSensor(Entity): self._state = None return - serie = data.series[len(data.series) - 1] + sorted_series = sorted(data.series, key=lambda serie: serie.startdate) + serie = sorted_series[len(sorted_series) - 1] state = None if serie.state == SleepState.AWAKE: state = const.STATE_AWAKE diff --git a/homeassistant/components/wled/.translations/da.json b/homeassistant/components/wled/.translations/da.json new file mode 100644 index 00000000000..0ab3a789b3a --- /dev/null +++ b/homeassistant/components/wled/.translations/da.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Denne WLED-enhed er allerede konfigureret.", + "connection_error": "Kunne ikke oprette forbindelse til WLED-enheden." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til WLED-enheden." + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "V\u00e6rt eller IP-adresse" + }, + "description": "Indstil din WLED til at integrere med Home Assistant.", + "title": "Forbind din WLED" + }, + "zeroconf_confirm": { + "description": "\u00d8nsker du at tilf\u00f8je WLED-enhed med navnet `{name}' til Home Assistant?", + "title": "Fandt WLED-enhed" + } + }, + "title": "WLED" + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/de.json b/homeassistant/components/wled/.translations/de.json index 2d1cc5ef97d..753d7868021 100644 --- a/homeassistant/components/wled/.translations/de.json +++ b/homeassistant/components/wled/.translations/de.json @@ -12,9 +12,12 @@ "user": { "data": { "host": "Hostname oder IP-Adresse" - } + }, + "description": "Richten Sie Ihre WLED f\u00fcr die Integration mit Home Assistant ein.", + "title": "Verkn\u00fcpfen Sie Ihr WLED" }, "zeroconf_confirm": { + "description": "M\u00f6chten Sie die WLED mit dem Namen \"{name}\" zu Home Assistant hinzuf\u00fcgen?", "title": "Gefundenes WLED-Ger\u00e4t" } }, diff --git a/homeassistant/components/wled/.translations/es.json b/homeassistant/components/wled/.translations/es.json index 7dd388d41af..b7f567698ea 100644 --- a/homeassistant/components/wled/.translations/es.json +++ b/homeassistant/components/wled/.translations/es.json @@ -7,7 +7,7 @@ "error": { "connection_error": "No se ha podido conectar al dispositivo WLED." }, - "flow_title": "WLED: {nombre}", + "flow_title": "WLED: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/.translations/ko.json b/homeassistant/components/wled/.translations/ko.json index bee9c2a6204..38496c01ee8 100644 --- a/homeassistant/components/wled/.translations/ko.json +++ b/homeassistant/components/wled/.translations/ko.json @@ -12,8 +12,15 @@ "user": { "data": { "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" - } + }, + "description": "Home Assistant \uc5d0 WLED \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4.", + "title": "WLED \uc5f0\uacb0" + }, + "zeroconf_confirm": { + "description": "Home Assistant \uc5d0 WLED `{name}` \uc744(\ub97c) \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c WLED \uae30\uae30" } - } + }, + "title": "WLED" } } \ No newline at end of file diff --git a/homeassistant/components/wled/.translations/nl.json b/homeassistant/components/wled/.translations/nl.json index 1bf70b7a095..266f74ce6c2 100644 --- a/homeassistant/components/wled/.translations/nl.json +++ b/homeassistant/components/wled/.translations/nl.json @@ -13,6 +13,7 @@ "data": { "host": "Hostnaam of IP-adres" }, + "description": "Stel uw WLED in op integratie met Home Assistant.", "title": "Koppel je WLED" }, "zeroconf_confirm": { diff --git a/homeassistant/components/wled/.translations/ru.json b/homeassistant/components/wled/.translations/ru.json index cd4c3c3b066..a884a20b337 100644 --- a/homeassistant/components/wled/.translations/ru.json +++ b/homeassistant/components/wled/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, "error": { diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index cd2c091bc10..e6adb460743 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -34,7 +34,7 @@ from .const import ( DOMAIN, ) -SCAN_INTERVAL = timedelta(seconds=5) +SCAN_INTERVAL = timedelta(seconds=10) WLED_COMPONENTS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create WLED instance for this entry session = async_get_clientsession(hass) - wled = WLED(entry.data[CONF_HOST], loop=hass.loop, session=session) + wled = WLED(entry.data[CONF_HOST], session=session) # Ensure we can connect and talk to it try: @@ -61,6 +61,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} + # For backwards compat, set unique ID + if entry.unique_id is None: + hass.config_entries.async_update_entry( + entry, unique_id=wled.device.info.mac_address + ) + # Set up all platforms for this device/entry. for component in WLED_COMPONENTS: hass.async_create_task( diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index c6b11fa1eb6..155cd022fd7 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -75,7 +75,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): errors = {} session = async_get_clientsession(self.hass) - wled = WLED(user_input[CONF_HOST], loop=self.hass.loop, session=session) + wled = WLED(user_input[CONF_HOST], session=session) try: device = await wled.update() @@ -87,10 +87,8 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): # Check if already configured mac_address = device.info.mac_address - for entry in self._async_current_entries(): - if entry.data[CONF_MAC] == mac_address: - # This mac address is already configured - return self.async_abort(reason="already_configured") + await self.async_set_unique_id(device.info.mac_address) + self._abort_if_unique_id_configured() title = user_input[CONF_HOST] if source == SOURCE_ZEROCONF: diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 5fc24d74d63..dcfdad963a7 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -3,7 +3,7 @@ # Integration domain DOMAIN = "wled" -# Hass data keys +# Home Assistant data keys DATA_WLED_CLIENT = "wled_client" DATA_WLED_TIMER = "wled_timer" DATA_WLED_UPDATED = "wled_updated" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 8bc1a56b205..f22282e5539 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -143,8 +143,14 @@ class WLEDLight(Light, WLEDDeviceEntity): async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" + data = {ATTR_ON: False, ATTR_SEGMENT_ID: self._segment} + + if ATTR_TRANSITION in kwargs: + # WLED uses 100ms per unit, so 10 = 1 second. + data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) + try: - await self.wled.light(on=False) + await self.wled.light(**data) self._state = False except WLEDError: _LOGGER.error("An error occurred while turning off WLED light.") @@ -168,7 +174,8 @@ class WLEDLight(Light, WLEDDeviceEntity): data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) if ATTR_TRANSITION in kwargs: - data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION] + # WLED uses 100ms per unit, so 10 = 1 second. + data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) if ATTR_BRIGHTNESS in kwargs: data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] @@ -186,6 +193,11 @@ class WLEDLight(Light, WLEDDeviceEntity): hue, sat = self._color data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) + # On a RGBW strip, when the color is pure white, disable the RGB LEDs in + # WLED by setting RGB to 0,0,0 + if data[ATTR_COLOR_PRIMARY] == (255, 255, 255): + data[ATTR_COLOR_PRIMARY] = (0, 0, 0) + # Add requested or last known white value if ATTR_WHITE_VALUE in kwargs: data[ATTR_COLOR_PRIMARY] += (kwargs[ATTR_WHITE_VALUE],) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 97e46998511..4b501d0c67c 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -3,8 +3,9 @@ "name": "WLED", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wled", - "requirements": ["wled==0.1.0"], + "requirements": ["wled==0.2.1"], "dependencies": [], "zeroconf": ["_wled._tcp.local."], - "codeowners": ["@frenck"] + "codeowners": ["@frenck"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index f95447c1e72..0aa1f5bfc42 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -1,122 +1,17 @@ """Sensor to indicate whether the current day is a workday.""" -import logging from datetime import datetime, timedelta +import logging +from typing import Any import holidays import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME, WEEKDAYS -from homeassistant.components.binary_sensor import BinarySensorDevice import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -# List of all countries currently supported by holidays -# Source: https://github.com/dr-prodigy/python-holidays#available-countries -# There seems to be no way to get the list out at runtime -ALL_COUNTRIES = [ - "Argentina", - "AR", - "Aruba", - "AW", - "Australia", - "AU", - "Austria", - "AT", - "Brazil", - "BR", - "Belarus", - "BY", - "Belgium", - "BE", - "Bulgaria", - "BG", - "Canada", - "CA", - "Colombia", - "CO", - "Croatia", - "HR", - "Czech", - "CZ", - "Denmark", - "DK", - "England", - "Estonia", - "EE", - "EuropeanCentralBank", - "ECB", - "TAR", - "Finland", - "FI", - "France", - "FRA", - "Germany", - "DE", - "Hungary", - "HU", - "Honduras", - "HND", - "Iceland", - "IS", - "India", - "IND", - "Ireland", - "IE", - "Isle of Man", - "Italy", - "IT", - "Japan", - "JP", - "Kenya", - "KE", - "Lithuania", - "LT", - "Luxembourg", - "LU", - "Mexico", - "MX", - "Netherlands", - "NL", - "NewZealand", - "NZ", - "Northern Ireland", - "Norway", - "NO", - "Peru", - "PE", - "Poland", - "Polish", - "PL", - "Portugal", - "PT", - "PortugalExt", - "PTE", - "Russia", - "RU", - "Scotland", - "Slovenia", - "SI", - "Slovakia", - "SK", - "South Africa", - "ZA", - "Spain", - "ES", - "Sweden", - "SE", - "Switzerland", - "CH", - "Ukraine", - "UA", - "UnitedKingdom", - "UK", - "UnitedStates", - "US", - "Wales", -] - ALLOWED_DAYS = WEEKDAYS + ["holiday"] CONF_COUNTRY = "country" @@ -133,9 +28,28 @@ DEFAULT_EXCLUDES = ["sat", "sun", "holiday"] DEFAULT_NAME = "Workday Sensor" DEFAULT_OFFSET = 0 + +def valid_country(value: Any) -> str: + """Validate that the given country is supported.""" + value = cv.string(value) + all_supported_countries = holidays.list_supported_countries() + + try: + raw_value = value.encode("utf-8") + except UnicodeError: + raise vol.Invalid( + "The country name or the abbreviation must be a valid UTF-8 string." + ) + if not raw_value: + raise vol.Invalid("Country name or the abbreviation must not be empty.") + if value not in all_supported_countries: + raise vol.Invalid("Country is not supported.") + return value + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_COUNTRY): vol.In(ALL_COUNTRIES), + vol.Required(CONF_COUNTRY): valid_country, vol.Optional(CONF_EXCLUDES, default=DEFAULT_EXCLUDES): vol.All( cv.ensure_list, [vol.In(ALLOWED_DAYS)] ), @@ -152,13 +66,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Workday sensor.""" - sensor_name = config.get(CONF_NAME) - country = config.get(CONF_COUNTRY) - province = config.get(CONF_PROVINCE) - workdays = config.get(CONF_WORKDAYS) - excludes = config.get(CONF_EXCLUDES) - days_offset = config.get(CONF_OFFSET) add_holidays = config.get(CONF_ADD_HOLIDAYS) + country = config[CONF_COUNTRY] + days_offset = config[CONF_OFFSET] + excludes = config[CONF_EXCLUDES] + province = config.get(CONF_PROVINCE) + sensor_name = config[CONF_NAME] + workdays = config[CONF_WORKDAYS] year = (get_date(datetime.today()) + timedelta(days=days_offset)).year obj_holidays = getattr(holidays, country)(years=year) @@ -260,7 +174,7 @@ class IsWorkdaySensor(BinarySensorDevice): # Default is no workday self._state = False - # Get iso day of the week (1 = Monday, 7 = Sunday) + # Get ISO day of the week (1 = Monday, 7 = Sunday) date = get_date(datetime.today()) + timedelta(days=self._days_offset) day = date.isoweekday() - 1 day_of_week = day_to_string(day) diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 4b407e95235..e888b0c5614 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -2,9 +2,8 @@ "domain": "workday", "name": "Workday", "documentation": "https://www.home-assistant.io/integrations/workday", - "requirements": [ - "holidays==0.9.11" - ], + "requirements": ["holidays==0.9.12"], "dependencies": [], - "codeowners": [] -} \ No newline at end of file + "codeowners": ["@fabaff"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/worldclock/manifest.json b/homeassistant/components/worldclock/manifest.json index 8f7b72491b8..9008fbc4855 100644 --- a/homeassistant/components/worldclock/manifest.json +++ b/homeassistant/components/worldclock/manifest.json @@ -4,7 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/worldclock", "requirements": [], "dependencies": [], - "codeowners": [ - "@fabaff" - ] + "codeowners": ["@fabaff"], + "quality_scale": "internal" } diff --git a/homeassistant/components/worldclock/sensor.py b/homeassistant/components/worldclock/sensor.py index 103c38819bb..1709ac7d23e 100644 --- a/homeassistant/components/worldclock/sensor.py +++ b/homeassistant/components/worldclock/sensor.py @@ -5,9 +5,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TIME_ZONE -import homeassistant.util.dt as dt_util -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/worldtidesinfo/manifest.json b/homeassistant/components/worldtidesinfo/manifest.json index 467a98a6660..56aa445bc08 100644 --- a/homeassistant/components/worldtidesinfo/manifest.json +++ b/homeassistant/components/worldtidesinfo/manifest.json @@ -1,6 +1,6 @@ { "domain": "worldtidesinfo", - "name": "Worldtidesinfo", + "name": "World Tides", "documentation": "https://www.home-assistant.io/integrations/worldtidesinfo", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/worxlandroid/manifest.json b/homeassistant/components/worxlandroid/manifest.json index b112bb7771c..479470f5a3a 100644 --- a/homeassistant/components/worxlandroid/manifest.json +++ b/homeassistant/components/worxlandroid/manifest.json @@ -1,6 +1,6 @@ { "domain": "worxlandroid", - "name": "Worxlandroid", + "name": "Worx Landroid", "documentation": "https://www.home-assistant.io/integrations/worxlandroid", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/worxlandroid/sensor.py b/homeassistant/components/worxlandroid/sensor.py index 4e9bf0a6a4a..fa2cae53f52 100644 --- a/homeassistant/components/worxlandroid/sensor.py +++ b/homeassistant/components/worxlandroid/sensor.py @@ -1,18 +1,16 @@ """Support for Worx Landroid mower.""" -import logging import asyncio +import logging import aiohttp import async_timeout - import voluptuous as vol -import homeassistant.helpers.config_validation as cv - -from homeassistant.helpers.entity import Entity -from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PIN, CONF_TIMEOUT from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -141,16 +139,9 @@ class WorxLandroidSensor(Entity): state = self.get_error(obj) if state is None: - state_obj = obj["settaggi"] + if obj["batteryChargerState"] == "charging": + return obj["batteryChargerState"] - if state_obj[14] == 1: - return "manual-stop" - if state_obj[5] == 1 and state_obj[13] == 0: - return "charging" - if state_obj[5] == 1 and state_obj[13] == 1: - return "charging-complete" - if state_obj[15] == 1: - return "going-home" - return "mowing" + return obj["state"] return state diff --git a/homeassistant/components/wsdot/manifest.json b/homeassistant/components/wsdot/manifest.json index 1c20b884223..73e6c2fe6a7 100644 --- a/homeassistant/components/wsdot/manifest.json +++ b/homeassistant/components/wsdot/manifest.json @@ -1,6 +1,6 @@ { "domain": "wsdot", - "name": "Wsdot", + "name": "Washington State Department of Transportation (WSDOT)", "documentation": "https://www.home-assistant.io/integrations/wsdot", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/wsdot/sensor.py b/homeassistant/components/wsdot/sensor.py index 2b6c0b73563..5afa3a3efcf 100644 --- a/homeassistant/components/wsdot/sensor.py +++ b/homeassistant/components/wsdot/sensor.py @@ -1,21 +1,21 @@ """Support for Washington State Department of Transportation (WSDOT) data.""" +from datetime import datetime, timedelta, timezone import logging import re -from datetime import datetime, timezone, timedelta import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, ATTR_ATTRIBUTION, - CONF_ID, ATTR_NAME, + CONF_API_KEY, + CONF_ID, + CONF_NAME, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wunderground/manifest.json b/homeassistant/components/wunderground/manifest.json index 8309f03bca4..b429aad3a78 100644 --- a/homeassistant/components/wunderground/manifest.json +++ b/homeassistant/components/wunderground/manifest.json @@ -1,6 +1,6 @@ { "domain": "wunderground", - "name": "Wunderground", + "name": "Weather Underground (WUnderground)", "documentation": "https://www.home-assistant.io/integrations/wunderground", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 5272b33ccb5..44eb10c0e7d 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -9,27 +9,27 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.helpers.typing import HomeAssistantType, ConfigType from homeassistant.components import sensor from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, - TEMP_FAHRENHEIT, - TEMP_CELSIUS, + CONF_MONITORED_CONDITIONS, + LENGTH_FEET, LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, - LENGTH_FEET, - ATTR_ATTRIBUTION, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.entity import Entity from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.util import Throttle _RESOURCE = "http://api.wunderground.com/api/{}/{}/{}/q/" _LOGGER = logging.getLogger(__name__) @@ -66,7 +66,7 @@ class WUSensorConfig: device_state_attributes=None, device_class=None, ): - """Constructor. + """Initialize sensor configuration. :param friendly_name: Friendly name :param feature: WU feature. See: @@ -98,7 +98,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): unit_of_measurement: Optional[str] = None, device_class=None, ): - """Constructor. + """Initialize current conditions sensor configuration. :param friendly_name: Friendly name of sensor :field: Field name in the "current_observation" dictionary. @@ -127,7 +127,7 @@ class WUDailyTextForecastSensorConfig(WUSensorConfig): def __init__( self, period: int, field: str, unit_of_measurement: Optional[str] = None ): - """Constructor. + """Initialize daily text forecast sensor configuration. :param period: forecast period number :param field: field name to use as value @@ -164,14 +164,14 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): icon=None, device_class=None, ): - """Constructor. + """Initialize daily simple forecast sensor configuration. :param friendly_name: friendly_name of the sensor :param period: forecast period number :param field: field name to use as value :param wu_unit: "fahrenheit", "celsius", "degrees" etc. see the example json at: https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1 - :param ha_unit: corresponding unit in home assistant + :param ha_unit: corresponding unit in Home Assistant """ super().__init__( friendly_name=friendly_name, @@ -207,7 +207,7 @@ class WUHourlyForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for hourly text forecasts.""" def __init__(self, period: int, field: int): - """Constructor. + """Initialize hourly forecast sensor configuration. :param period: forecast period number :param field: field name to use as value @@ -274,7 +274,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): icon: str, device_class=None, ): - """Constructor. + """Initialize almanac sensor configuration. :param friendly_name: Friendly name :param field: value name returned in 'almanac' dict as returned by the WU API @@ -297,7 +297,7 @@ class WUAlertsSensorConfig(WUSensorConfig): """Helper for defining field configuration for alerts.""" def __init__(self, friendly_name: Union[str, Callable]): - """Constructor. + """Initialiize alerts sensor configuration. :param friendly_name: Friendly name """ @@ -998,7 +998,7 @@ class WUndergroundSensor(Entity): self.rest.request_feature(SENSOR_TYPES[condition].feature) # 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.entity_id = sensor.ENTITY_ID_FORMAT.format(f"pws_{condition}") self._unique_id = f"{unique_id_base},{condition}" self._device_class = self._cfg_expand("device_class") @@ -1012,7 +1012,7 @@ class WUndergroundSensor(Entity): val = val(self.rest) except (KeyError, IndexError, TypeError, ValueError) as err: _LOGGER.warning( - "Failed to expand cfg from WU API." " Condition: %s Attr: %s Error: %s", + "Failed to expand cfg from WU API. Condition: %s Attr: %s Error: %s", self._condition, what, repr(err), @@ -1122,11 +1122,11 @@ class WUndergroundData: self._api_key, "/".join(sorted(self._features)), self._lang ) if self._pws_id: - url = url + f"pws:{self._pws_id}" + url = f"{url}pws:{self._pws_id}" else: - url = url + f"{self._latitude},{self._longitude}" + url = f"{url}{self._latitude},{self._longitude}" - return url + ".json" + return f"{url}.json" @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index 122d09feaa4..4d9ff6e2235 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -4,8 +4,8 @@ import logging import voluptuous as vol from wunderpy2 import WunderApi +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_NAME, CONF_ACCESS_TOKEN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/wunderlist/manifest.json b/homeassistant/components/wunderlist/manifest.json index 90a55ad48e8..0502d8efd35 100644 --- a/homeassistant/components/wunderlist/manifest.json +++ b/homeassistant/components/wunderlist/manifest.json @@ -2,9 +2,7 @@ "domain": "wunderlist", "name": "Wunderlist", "documentation": "https://www.home-assistant.io/integrations/wunderlist", - "requirements": [ - "wunderpy2==0.1.6" - ], + "requirements": ["wunderpy2==0.1.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/wwlln/.translations/da.json b/homeassistant/components/wwlln/.translations/da.json index 7d9a676e163..5d4f4c40b5d 100644 --- a/homeassistant/components/wwlln/.translations/da.json +++ b/homeassistant/components/wwlln/.translations/da.json @@ -1,16 +1,16 @@ { "config": { "error": { - "identifier_exists": "Placering er allerede registreret" + "identifier_exists": "Lokalitet er allerede registreret" }, "step": { "user": { "data": { "latitude": "Breddegrad", "longitude": "L\u00e6ngdegrad", - "radius": "Radius (ved hj\u00e6lp af dit basis enhedssystem)" + "radius": "Radius (ved hj\u00e6lp af dit basisenhedssystem)" }, - "title": "Udfyld dine placeringsoplysninger." + "title": "Udfyld dine lokalitetsoplysninger." } }, "title": "World Wide Lightning Location Network (WWLLN)" diff --git a/homeassistant/components/wwlln/config_flow.py b/homeassistant/components/wwlln/config_flow.py index 3e43ba93278..f9cd022f255 100644 --- a/homeassistant/components/wwlln/config_flow.py +++ b/homeassistant/components/wwlln/config_flow.py @@ -2,7 +2,6 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.helpers import config_validation as cv from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, @@ -12,6 +11,7 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM_METRIC, ) from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv from .const import CONF_WINDOW, DEFAULT_RADIUS, DEFAULT_WINDOW, DOMAIN diff --git a/homeassistant/components/wwlln/manifest.json b/homeassistant/components/wwlln/manifest.json index a7bf14454da..343b8b8a8ce 100644 --- a/homeassistant/components/wwlln/manifest.json +++ b/homeassistant/components/wwlln/manifest.json @@ -1,13 +1,9 @@ { "domain": "wwlln", - "name": "World Wide Lightning Location Network", + "name": "World Wide Lightning Location Network (WWLLN)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wwlln", - "requirements": [ - "aiowwlln==2.0.2" - ], + "requirements": ["aiowwlln==2.0.2"], "dependencies": [], - "codeowners": [ - "@bachya" - ] + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/x10/light.py b/homeassistant/components/x10/light.py index bb2d2d89456..7be2f12d949 100644 --- a/homeassistant/components/x10/light.py +++ b/homeassistant/components/x10/light.py @@ -1,16 +1,16 @@ """Support for X10 lights.""" import logging -from subprocess import check_output, CalledProcessError, STDOUT +from subprocess import STDOUT, CalledProcessError, check_output import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_ID, CONF_DEVICES from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def x10_command(command): def get_unit_status(code): """Get on/off status for given unit.""" - output = check_output("heyu onstate " + code, shell=True) + output = check_output(f"heyu onstate {code}", shell=True) return int(output.decode("utf-8")[0]) @@ -84,18 +84,18 @@ class X10Light(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" if self._is_cm11a: - x10_command("on " + self._id) + x10_command(f"on {self._id}") else: - x10_command("fon " + self._id) + x10_command(f"fon {self._id}") self._brightness = kwargs.get(ATTR_BRIGHTNESS, 255) self._state = True def turn_off(self, **kwargs): """Instruct the light to turn off.""" if self._is_cm11a: - x10_command("off " + self._id) + x10_command(f"off {self._id}") else: - x10_command("foff " + self._id) + x10_command(f"foff {self._id}") self._state = False def update(self): diff --git a/homeassistant/components/x10/manifest.json b/homeassistant/components/x10/manifest.json index bae5247ffbc..b994e50a7ac 100644 --- a/homeassistant/components/x10/manifest.json +++ b/homeassistant/components/x10/manifest.json @@ -1,6 +1,6 @@ { "domain": "x10", - "name": "X10", + "name": "Heyu X10", "documentation": "https://www.home-assistant.io/integrations/x10", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/xbox_live/manifest.json b/homeassistant/components/xbox_live/manifest.json index 79f4ce6c87f..4bf8e3faf62 100644 --- a/homeassistant/components/xbox_live/manifest.json +++ b/homeassistant/components/xbox_live/manifest.json @@ -1,12 +1,8 @@ { "domain": "xbox_live", - "name": "Xbox live", + "name": "Xbox Live", "documentation": "https://www.home-assistant.io/integrations/xbox_live", - "requirements": [ - "xboxapi==0.1.1" - ], + "requirements": ["xboxapi==0.1.1"], "dependencies": [], - "codeowners": [ - "@MartinHjelmare" - ] + "codeowners": ["@MartinHjelmare"] } diff --git a/homeassistant/components/xbox_live/sensor.py b/homeassistant/components/xbox_live/sensor.py index e837cc4bbbc..41ebf69126a 100644 --- a/homeassistant/components/xbox_live/sensor.py +++ b/homeassistant/components/xbox_live/sensor.py @@ -1,6 +1,6 @@ """Sensor for Xbox Live account status.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from xboxapi import xbox_api diff --git a/homeassistant/components/xeoma/camera.py b/homeassistant/components/xeoma/camera.py index bb5febe6bd7..d6f313c0382 100644 --- a/homeassistant/components/xeoma/camera.py +++ b/homeassistant/components/xeoma/camera.py @@ -1,8 +1,8 @@ """Support for Xeoma Cameras.""" import logging -import voluptuous as vol from pyxeoma.xeoma import Xeoma, XeomaError +import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME diff --git a/homeassistant/components/xeoma/manifest.json b/homeassistant/components/xeoma/manifest.json index 7cf061018b9..f5fd27b5063 100644 --- a/homeassistant/components/xeoma/manifest.json +++ b/homeassistant/components/xeoma/manifest.json @@ -2,9 +2,7 @@ "domain": "xeoma", "name": "Xeoma", "documentation": "https://www.home-assistant.io/integrations/xeoma", - "requirements": [ - "pyxeoma==1.4.1" - ], + "requirements": ["pyxeoma==1.4.1"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/xfinity/device_tracker.py b/homeassistant/components/xfinity/device_tracker.py index 712d31d46db..20e13682979 100644 --- a/homeassistant/components/xfinity/device_tracker.py +++ b/homeassistant/components/xfinity/device_tracker.py @@ -5,13 +5,13 @@ from requests.exceptions import RequestException import voluptuous as vol from xfinity_gateway import XfinityGateway -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -32,7 +32,7 @@ def get_scanner(hass, config): scanner = XfinityDeviceScanner(gateway) except (RequestException, ValueError): _LOGGER.error( - "Error communicating with Xfinity Gateway. " "Check host: %s", gateway.host + "Error communicating with Xfinity Gateway. Check host: %s", gateway.host ) return scanner @@ -51,7 +51,7 @@ class XfinityDeviceScanner(DeviceScanner): try: connected_devices = self.gateway.scan_devices() except (RequestException, ValueError): - _LOGGER.error("Unable to scan devices. " "Check connection to gateway") + _LOGGER.error("Unable to scan devices. Check connection to gateway") return connected_devices def get_device_name(self, device): diff --git a/homeassistant/components/xfinity/manifest.json b/homeassistant/components/xfinity/manifest.json index 9e800dc2e4a..861a05da4c6 100644 --- a/homeassistant/components/xfinity/manifest.json +++ b/homeassistant/components/xfinity/manifest.json @@ -1,12 +1,8 @@ { "domain": "xfinity", - "name": "Xfinity", + "name": "Xfinity Gateway", "documentation": "https://www.home-assistant.io/integrations/xfinity", - "requirements": [ - "xfinity-gateway==0.0.4" - ], + "requirements": ["xfinity-gateway==0.0.4"], "dependencies": [], - "codeowners": [ - "@cisasteelersfan" - ] + "codeowners": ["@cisasteelersfan"] } diff --git a/homeassistant/components/xiaomi/manifest.json b/homeassistant/components/xiaomi/manifest.json index a607cda511e..4c5b2c19dd9 100644 --- a/homeassistant/components/xiaomi/manifest.json +++ b/homeassistant/components/xiaomi/manifest.json @@ -3,8 +3,6 @@ "name": "Xiaomi", "documentation": "https://www.home-assistant.io/integrations/xiaomi", "requirements": [], - "dependencies": [ - "ffmpeg" - ], + "dependencies": ["ffmpeg"], "codeowners": [] } diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 9eeddd357f6..4568f67dbf5 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -1,13 +1,8 @@ { "domain": "xiaomi_aqara", - "name": "Xiaomi aqara", + "name": "Xiaomi Gateway (Aqara)", "documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara", - "requirements": [ - "PyXiaomiGateway==0.12.4" - ], + "requirements": ["PyXiaomiGateway==0.12.4"], "dependencies": [], - "codeowners": [ - "@danielhiversen", - "@syssi" - ] + "codeowners": ["@danielhiversen", "@syssi"] } diff --git a/homeassistant/components/xiaomi_miio/air_quality.py b/homeassistant/components/xiaomi_miio/air_quality.py index 3824c5b88cd..f5e7e476ac5 100644 --- a/homeassistant/components/xiaomi_miio/air_quality.py +++ b/homeassistant/components/xiaomi_miio/air_quality.py @@ -3,9 +3,9 @@ from miio import AirQualityMonitor, Device, DeviceException import voluptuous as vol from homeassistant.components.air_quality import ( - AirQualityEntity, - PLATFORM_SCHEMA, _LOGGER, + PLATFORM_SCHEMA, + AirQualityEntity, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN from homeassistant.exceptions import PlatformNotReady diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 91b18aaf364..7cb45296506 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -4,7 +4,6 @@ from enum import Enum from functools import partial import logging -import voluptuous as vol from miio import ( # pylint: disable=import-error AirFresh, AirHumidifier, @@ -12,19 +11,19 @@ from miio import ( # pylint: disable=import-error Device, DeviceException, ) - -from miio.airfresh import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airfresh import ( # pylint: disable=import-error, import-error LedBrightness as AirfreshLedBrightness, OperationMode as AirfreshOperationMode, ) -from miio.airhumidifier import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airhumidifier import ( # pylint: disable=import-error, import-error LedBrightness as AirhumidifierLedBrightness, OperationMode as AirhumidifierOperationMode, ) -from miio.airpurifier import ( # pylint: disable=import-error; pylint: disable=import-error +from miio.airpurifier import ( # pylint: disable=import-error, import-error LedBrightness as AirpurifierLedBrightness, OperationMode as AirpurifierOperationMode, ) +import voluptuous as vol from homeassistant.components.fan import PLATFORM_SCHEMA, SUPPORT_SET_SPEED, FanEntity from homeassistant.const import ( @@ -39,24 +38,24 @@ import homeassistant.helpers.config_validation as cv from .const import ( DOMAIN, - SERVICE_SET_BUZZER_ON, - SERVICE_SET_BUZZER_OFF, - SERVICE_SET_LED_ON, - SERVICE_SET_LED_OFF, - SERVICE_SET_CHILD_LOCK_ON, - SERVICE_SET_CHILD_LOCK_OFF, - SERVICE_SET_LED_BRIGHTNESS, - SERVICE_SET_FAVORITE_LEVEL, - SERVICE_SET_AUTO_DETECT_ON, - SERVICE_SET_AUTO_DETECT_OFF, - SERVICE_SET_LEARN_MODE_ON, - SERVICE_SET_LEARN_MODE_OFF, - SERVICE_SET_VOLUME, SERVICE_RESET_FILTER, - SERVICE_SET_EXTRA_FEATURES, - SERVICE_SET_TARGET_HUMIDITY, - SERVICE_SET_DRY_ON, + SERVICE_SET_AUTO_DETECT_OFF, + SERVICE_SET_AUTO_DETECT_ON, + SERVICE_SET_BUZZER_OFF, + SERVICE_SET_BUZZER_ON, + SERVICE_SET_CHILD_LOCK_OFF, + SERVICE_SET_CHILD_LOCK_ON, SERVICE_SET_DRY_OFF, + SERVICE_SET_DRY_ON, + SERVICE_SET_EXTRA_FEATURES, + SERVICE_SET_FAVORITE_LEVEL, + SERVICE_SET_LEARN_MODE_OFF, + SERVICE_SET_LEARN_MODE_ON, + SERVICE_SET_LED_BRIGHTNESS, + SERVICE_SET_LED_OFF, + SERVICE_SET_LED_ON, + SERVICE_SET_TARGET_HUMIDITY, + SERVICE_SET_VOLUME, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 2343a6787c2..bcc83bae454 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -34,14 +34,14 @@ from homeassistant.util import color, dt from .const import ( DOMAIN, - SERVICE_SET_SCENE, - SERVICE_SET_DELAYED_TURN_OFF, - SERVICE_REMINDER_ON, - SERVICE_REMINDER_OFF, - SERVICE_NIGHT_LIGHT_MODE_ON, - SERVICE_NIGHT_LIGHT_MODE_OFF, - SERVICE_EYECARE_MODE_ON, SERVICE_EYECARE_MODE_OFF, + SERVICE_EYECARE_MODE_ON, + SERVICE_NIGHT_LIGHT_MODE_OFF, + SERVICE_NIGHT_LIGHT_MODE_ON, + SERVICE_REMINDER_OFF, + SERVICE_REMINDER_ON, + SERVICE_SET_DELAYED_TURN_OFF, + SERVICE_SET_SCENE, ) _LOGGER = logging.getLogger(__name__) @@ -467,7 +467,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): ) result = await self._try_command( - "Setting brightness and color temperature failed: " "%s bri, %s cct", + "Setting brightness and color temperature failed: %s bri, %s cct", self._light.set_brightness_and_color_temperature, percent_brightness, percent_color_temp, @@ -479,7 +479,7 @@ class XiaomiPhilipsBulb(XiaomiPhilipsGenericLight): elif ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( - "Setting color temperature: " "%s mireds, %s%% cct", + "Setting color temperature: %s mireds, %s%% cct", color_temp, percent_color_temp, ) @@ -825,14 +825,14 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): if ATTR_BRIGHTNESS in kwargs and ATTR_HS_COLOR in kwargs: _LOGGER.debug( - "Setting brightness and color: " "%s %s%%, %s", + "Setting brightness and color: %s %s%%, %s", brightness, percent_brightness, rgb, ) result = await self._try_command( - "Setting brightness and color failed: " "%s bri, %s color", + "Setting brightness and color failed: %s bri, %s color", self._light.set_brightness_and_rgb, percent_brightness, rgb, @@ -853,7 +853,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): ) result = await self._try_command( - "Setting brightness and color temperature failed: " "%s bri, %s cct", + "Setting brightness and color temperature failed: %s bri, %s cct", self._light.set_brightness_and_color_temperature, percent_brightness, percent_color_temp, @@ -875,7 +875,7 @@ class XiaomiPhilipsMoonlightLamp(XiaomiPhilipsBulb): elif ATTR_COLOR_TEMP in kwargs: _LOGGER.debug( - "Setting color temperature: " "%s mireds, %s%% cct", + "Setting color temperature: %s mireds, %s%% cct", color_temp, percent_color_temp, ) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 849e4573bbf..3d179c63adb 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -2,13 +2,7 @@ "domain": "xiaomi_miio", "name": "Xiaomi miio", "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", - "requirements": [ - "construct==2.9.45", - "python-miio==0.4.7" - ], + "requirements": ["construct==2.9.45", "python-miio==0.4.8"], "dependencies": [], - "codeowners": [ - "@rytilahti", - "@syssi" - ] + "codeowners": ["@rytilahti", "@syssi"] } diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index f9a06924b5c..63229b851d0 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -26,10 +26,10 @@ import homeassistant.helpers.config_validation as cv from .const import ( DOMAIN, - SERVICE_SET_WIFI_LED_ON, - SERVICE_SET_WIFI_LED_OFF, SERVICE_SET_POWER_MODE, SERVICE_SET_POWER_PRICE, + SERVICE_SET_WIFI_LED_OFF, + SERVICE_SET_WIFI_LED_ON, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index f1845f534bb..4ef34e8ff56 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -39,11 +39,11 @@ import homeassistant.helpers.config_validation as cv from .const import ( DOMAIN, + SERVICE_CLEAN_ZONE, SERVICE_MOVE_REMOTE_CONTROL, SERVICE_MOVE_REMOTE_CONTROL_STEP, SERVICE_START_REMOTE_CONTROL, SERVICE_STOP_REMOTE_CONTROL, - SERVICE_CLEAN_ZONE, ) _LOGGER = logging.getLogger(__name__) @@ -379,7 +379,7 @@ class MiroboVacuum(StateVacuumDevice): fan_speed = int(fan_speed) except ValueError as exc: _LOGGER.error( - "Fan speed step not recognized (%s). " "Valid speeds are: %s", + "Fan speed step not recognized (%s). Valid speeds are: %s", exc, self.fan_speed_list, ) diff --git a/homeassistant/components/xiaomi_tv/manifest.json b/homeassistant/components/xiaomi_tv/manifest.json index 740eaf3ea1c..13843c88ecc 100644 --- a/homeassistant/components/xiaomi_tv/manifest.json +++ b/homeassistant/components/xiaomi_tv/manifest.json @@ -1,12 +1,8 @@ { "domain": "xiaomi_tv", - "name": "Xiaomi tv", + "name": "Xiaomi TV", "documentation": "https://www.home-assistant.io/integrations/xiaomi_tv", - "requirements": [ - "pymitv==1.4.3" - ], + "requirements": ["pymitv==1.4.3"], "dependencies": [], - "codeowners": [ - "@simse" - ] + "codeowners": ["@simse"] } diff --git a/homeassistant/components/xiaomi_tv/media_player.py b/homeassistant/components/xiaomi_tv/media_player.py index c34448ba63b..c82708852c2 100644 --- a/homeassistant/components/xiaomi_tv/media_player.py +++ b/homeassistant/components/xiaomi_tv/media_player.py @@ -1,10 +1,10 @@ """Add support for the Xiaomi TVs.""" import logging -import voluptuous as vol import pymitv +import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_TURN_OFF, SUPPORT_TURN_ON, diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 21255497503..26d2362a192 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -1,13 +1,8 @@ { "domain": "xmpp", - "name": "Xmpp", + "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": [ - "slixmpp==1.4.2" - ], + "requirements": ["slixmpp==1.4.2"], "dependencies": [], - "codeowners": [ - "@fabaff", - "@flowolf" - ] + "codeowners": ["@fabaff", "@flowolf"] } diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 338b0b85c03..28d42698657 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -9,14 +9,20 @@ import string import requests import slixmpp from slixmpp.exceptions import IqError, IqTimeout, XMPPError -from slixmpp.xmlstream.xmlstream import NotConnectedError from slixmpp.plugins.xep_0363.http_upload import ( FileTooBig, FileUploadError, UploadServiceNotFound, ) +from slixmpp.xmlstream.xmlstream import NotConnectedError import voluptuous as vol +from homeassistant.components.notify import ( + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) from homeassistant.const import ( CONF_PASSWORD, CONF_RECIPIENT, @@ -27,13 +33,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper -from homeassistant.components.notify import ( - ATTR_TITLE, - ATTR_TITLE_DEFAULT, - PLATFORM_SCHEMA, - BaseNotificationService, -) - _LOGGER = logging.getLogger(__name__) ATTR_DATA = "data" @@ -202,7 +201,7 @@ async def async_send_message( except FileTooBig as ex: _LOGGER.error("File too big for server, could not upload file %s", ex) except UploadServiceNotFound as ex: - _LOGGER.error("UploadServiceNotFound: " " could not upload file %s", ex) + _LOGGER.error("UploadServiceNotFound, could not upload file %s", ex) except FileUploadError as ex: _LOGGER.error("FileUploadError, could not upload file %s", ex) except requests.exceptions.SSLError as ex: diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py index aeb6204265b..1fbcb49d0c9 100644 --- a/homeassistant/components/xs1/__init__.py +++ b/homeassistant/components/xs1/__init__.py @@ -63,8 +63,7 @@ def setup(hass, config): ) except ConnectionError as error: _LOGGER.error( - "Failed to create XS1 API client " "because of a connection error: %s", - error, + "Failed to create XS1 API client because of a connection error: %s", error, ) return False diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py index 95ad10539bd..33c778c0d3d 100644 --- a/homeassistant/components/xs1/climate.py +++ b/homeassistant/components/xs1/climate.py @@ -5,8 +5,8 @@ from xs1_api_client.api_constants import ActuatorType from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - SUPPORT_TARGET_TEMPERATURE, HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE diff --git a/homeassistant/components/xs1/manifest.json b/homeassistant/components/xs1/manifest.json index 290c552309b..480da6df351 100644 --- a/homeassistant/components/xs1/manifest.json +++ b/homeassistant/components/xs1/manifest.json @@ -1,10 +1,8 @@ { "domain": "xs1", - "name": "Xs1", + "name": "EZcontrol XS1", "documentation": "https://www.home-assistant.io/integrations/xs1", - "requirements": [ - "xs1-api-client==2.3.5" - ], + "requirements": ["xs1-api-client==2.3.5"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index 7f2cbc2a33d..05823a511dd 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -3,11 +3,11 @@ import logging import voluptuous as vol from yalesmartalarmclient.client import ( - YaleSmartAlarmClient, - AuthenticationError, - YALE_STATE_DISARM, - YALE_STATE_ARM_PARTIAL, YALE_STATE_ARM_FULL, + YALE_STATE_ARM_PARTIAL, + YALE_STATE_DISARM, + AuthenticationError, + YaleSmartAlarmClient, ) from homeassistant.components.alarm_control_panel import ( diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index 05e979ffb0a..a937e5e5d5b 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -1,10 +1,8 @@ { "domain": "yale_smart_alarm", - "name": "Yale smart alarm", + "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", - "requirements": [ - "yalesmartalarmclient==0.1.6" - ], + "requirements": ["yalesmartalarmclient==0.1.6"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/yamaha/manifest.json b/homeassistant/components/yamaha/manifest.json index bacb9fc3305..d7aa9dbfae0 100644 --- a/homeassistant/components/yamaha/manifest.json +++ b/homeassistant/components/yamaha/manifest.json @@ -1,10 +1,8 @@ { "domain": "yamaha", - "name": "Yamaha", + "name": "Yamaha Network Receivers", "documentation": "https://www.home-assistant.io/integrations/yamaha", - "requirements": [ - "rxv==0.6.0" - ], + "requirements": ["rxv==0.6.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index fa2c68dce88..7ab7d5b3a47 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -5,7 +5,7 @@ import requests import rxv import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -13,15 +13,14 @@ from homeassistant.components.media_player.const import ( SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_STOP, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOUND_MODE, ) - from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, diff --git a/homeassistant/components/yamaha_musiccast/manifest.json b/homeassistant/components/yamaha_musiccast/manifest.json index ea36c4921c5..8734f870966 100644 --- a/homeassistant/components/yamaha_musiccast/manifest.json +++ b/homeassistant/components/yamaha_musiccast/manifest.json @@ -1,12 +1,8 @@ { "domain": "yamaha_musiccast", - "name": "Yamaha musiccast", + "name": "Yamaha MusicCast", "documentation": "https://www.home-assistant.io/integrations/yamaha_musiccast", - "requirements": [ - "pymusiccast==0.1.6" - ], + "requirements": ["pymusiccast==0.1.6"], "dependencies": [], - "codeowners": [ - "@jalmeroth" - ] + "codeowners": ["@jalmeroth"] } diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 18b80cc4085..f239e07a1dc 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,11 +1,11 @@ """Support for Yamaha MusicCast Receivers.""" import logging - import socket + import pymusiccast import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -232,7 +232,7 @@ class YamahaDevice(MediaPlayerDevice): self._zone.update_status() def update_hass(self): - """Push updates to HASS.""" + """Push updates to Home Assistant.""" if self.entity_id: _LOGGER.debug("update_hass: pushing updates") self.schedule_update_ha_state() diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 44dcf5b100c..6ba0886d2db 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -2,11 +2,7 @@ "domain": "yandex_transport", "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", - "requirements": [ - "ya_ma==0.3.8" - ], + "requirements": ["ya_ma==0.3.8"], "dependencies": [], - "codeowners": [ - "@rishatik92" - ] + "codeowners": ["@rishatik92"] } diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 4bf634a61f4..54db4882e74 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -1,16 +1,16 @@ """Service for obtaining information about closer bus from Transport Yandex Service.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from ya_ma import YandexMapsRequester -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_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yandextts/manifest.json b/homeassistant/components/yandextts/manifest.json index 66a546abdb4..99f074fa758 100644 --- a/homeassistant/components/yandextts/manifest.json +++ b/homeassistant/components/yandextts/manifest.json @@ -1,6 +1,6 @@ { "domain": "yandextts", - "name": "Yandextts", + "name": "Yandex TTS", "documentation": "https://www.home-assistant.io/integrations/yandextts", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index c899c811a47..b947f6b448c 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -1,24 +1,25 @@ """Support for Xiaomi Yeelight WiFi color bulb.""" -import logging from datetime import timedelta +import logging import voluptuous as vol from yeelight import Bulb, BulbException + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.discovery import SERVICE_YEELIGHT +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DEVICES, + CONF_HOST, CONF_NAME, CONF_SCAN_INTERVAL, - CONF_HOST, - ATTR_ENTITY_ID, ) -from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.helpers import discovery -from homeassistant.helpers.discovery import load_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import dispatcher_send, dispatcher_connect +from homeassistant.helpers.discovery import load_platform +from homeassistant.helpers.dispatcher import dispatcher_connect, dispatcher_send from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -48,6 +49,7 @@ ACTION_STAY = "stay" ACTION_OFF = "off" ACTIVE_MODE_NIGHTLIGHT = "1" +ACTIVE_COLOR_FLOWING = "1" NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" @@ -123,6 +125,7 @@ UPDATE_REQUEST_PROPERTIES = [ "hue", "sat", "color_mode", + "flowing", "bg_power", "bg_lmode", "bg_flowing", @@ -250,10 +253,19 @@ class YeelightDevice: return self._active_mode is not None + @property + def is_color_flow_enabled(self) -> bool: + """Return true / false if color flow is currently running.""" + return self._color_flow == ACTIVE_COLOR_FLOWING + @property def _active_mode(self): return self.bulb.last_properties.get("active_mode") + @property + def _color_flow(self): + return self.bulb.last_properties.get("flowing") + @property def type(self): """Return bulb type.""" diff --git a/homeassistant/components/yeelight/binary_sensor.py b/homeassistant/components/yeelight/binary_sensor.py index da39152e9ca..29e24b510e5 100644 --- a/homeassistant/components/yeelight/binary_sensor.py +++ b/homeassistant/components/yeelight/binary_sensor.py @@ -4,7 +4,8 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DATA_YEELIGHT, DATA_UPDATED + +from . import DATA_UPDATED, DATA_YEELIGHT _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 772fb00977b..61de12eafbf 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -4,60 +4,61 @@ import logging import voluptuous as vol import yeelight from yeelight import ( + BulbException, + Flow, RGBTransition, SleepTransition, - Flow, - BulbException, transitions as yee_transitions, ) -from yeelight.enums import PowerMode, LightType, BulbType, SceneClass +from yeelight.enums import BulbType, LightType, PowerMode, 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, -) -from homeassistant.const import CONF_HOST, ATTR_ENTITY_ID, ATTR_MODE, CONF_NAME -from homeassistant.core import callback from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_TRANSITION, ATTR_COLOR_TEMP, - ATTR_FLASH, - FLASH_SHORT, - FLASH_LONG, ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_KELVIN, + ATTR_RGB_COLOR, + ATTR_TRANSITION, + FLASH_LONG, + FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, - SUPPORT_TRANSITION, SUPPORT_COLOR_TEMP, - SUPPORT_FLASH, SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, Light, - ATTR_RGB_COLOR, - ATTR_KELVIN, ) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE, CONF_HOST, CONF_NAME +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.service import extract_entity_ids import homeassistant.util.color as color_util +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired as kelvin_to_mired, + color_temperature_mired_to_kelvin as mired_to_kelvin, +) + from . import ( - CONF_TRANSITION, - DATA_YEELIGHT, - CONF_MODE_MUSIC, - CONF_SAVE_ON_CHANGE, - CONF_CUSTOM_EFFECTS, - DATA_UPDATED, - YEELIGHT_SERVICE_SCHEMA, - DOMAIN, - ATTR_TRANSITIONS, - YEELIGHT_FLOW_TRANSITION_SCHEMA, ACTION_RECOVER, - CONF_FLOW_PARAMS, ATTR_ACTION, ATTR_COUNT, - NIGHTLIGHT_SWITCH_TYPE_LIGHT, + ATTR_TRANSITIONS, + CONF_CUSTOM_EFFECTS, + CONF_FLOW_PARAMS, + CONF_MODE_MUSIC, CONF_NIGHTLIGHT_SWITCH_TYPE, + CONF_SAVE_ON_CHANGE, + CONF_TRANSITION, + DATA_UPDATED, + DATA_YEELIGHT, + DOMAIN, + NIGHTLIGHT_SWITCH_TYPE_LIGHT, + YEELIGHT_FLOW_TRANSITION_SCHEMA, + YEELIGHT_SERVICE_SCHEMA, ) _LOGGER = logging.getLogger(__name__) @@ -141,6 +142,21 @@ MODEL_TO_DEVICE_TYPE = { "ceiling4": BulbType.WhiteTempMood, } +EFFECTS_MAP = { + EFFECT_DISCO: yee_transitions.disco, + EFFECT_TEMP: yee_transitions.temp, + EFFECT_STROBE: yee_transitions.strobe, + EFFECT_STROBE_COLOR: yee_transitions.strobe_color, + EFFECT_ALARM: yee_transitions.alarm, + EFFECT_POLICE: yee_transitions.police, + EFFECT_POLICE2: yee_transitions.police2, + EFFECT_CHRISTMAS: yee_transitions.christmas, + EFFECT_RGB: yee_transitions.rgb, + EFFECT_RANDOM_LOOP: yee_transitions.randomloop, + EFFECT_LSD: yee_transitions.lsd, + EFFECT_SLOWDOWN: yee_transitions.slowdown, +} + VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Range(min=1, max=100)) SERVICE_SCHEMA_SET_MODE = YEELIGHT_SERVICE_SCHEMA.extend( @@ -282,7 +298,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: _lights_setup_helper(YeelightGenericLight) _LOGGER.warning( - "Cannot determine device type for %s, %s. " "Falling back to white only", + "Cannot determine device type for %s, %s. Falling back to white only", device.ipaddr, device.name, ) @@ -415,6 +431,7 @@ class YeelightGenericLight(Light): self._brightness = None self._color_temp = None self._hs = None + self._effect = None model_specs = self._bulb.get_model_specs() self._min_mireds = kelvin_to_mired(model_specs["color_temp"]["max"]) @@ -515,6 +532,11 @@ class YeelightGenericLight(Light): """Return the color property.""" return self._hs + @property + def effect(self): + """Return the current effect.""" + return self._effect + # F821: https://github.com/PyCQA/pyflakes/issues/373 @property def _bulb(self) -> "Bulb": # noqa: F821 @@ -545,6 +567,16 @@ class YeelightGenericLight(Light): def _predefined_effects(self): return YEELIGHT_MONO_EFFECT_LIST + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + + attributes = {"flowing": self.device.is_color_flow_enabled} + if self.device.is_nightlight_supported: + attributes["night_light"] = self.device.is_nightlight_enabled + + return attributes + @property def device(self): """Return yeelight device.""" @@ -553,6 +585,8 @@ class YeelightGenericLight(Light): def update(self): """Update light properties.""" self._hs = self._get_hs_from_properties() + if not self.device.is_color_flow_enabled: + self._effect = None def _get_hs_from_properties(self): rgb = self._get_property("rgb") @@ -657,45 +691,33 @@ class YeelightGenericLight(Light): @_cmd def set_effect(self, effect) -> None: """Activate effect.""" - if effect: - if effect == EFFECT_STOP: - self._bulb.stop_flow(light_type=self.light_type) - return + if not effect: + return - effects_map = { - EFFECT_DISCO: yee_transitions.disco, - EFFECT_TEMP: yee_transitions.temp, - EFFECT_STROBE: yee_transitions.strobe, - EFFECT_STROBE_COLOR: yee_transitions.strobe_color, - EFFECT_ALARM: yee_transitions.alarm, - EFFECT_POLICE: yee_transitions.police, - EFFECT_POLICE2: yee_transitions.police2, - EFFECT_CHRISTMAS: yee_transitions.christmas, - EFFECT_RGB: yee_transitions.rgb, - EFFECT_RANDOM_LOOP: yee_transitions.randomloop, - EFFECT_LSD: yee_transitions.lsd, - EFFECT_SLOWDOWN: yee_transitions.slowdown, - } + if effect == EFFECT_STOP: + self._bulb.stop_flow(light_type=self.light_type) + return - if effect in self.custom_effects_names: - flow = Flow(**self.custom_effects[effect]) - elif effect in effects_map: - flow = Flow(count=0, transitions=effects_map[effect]()) - elif effect == EFFECT_FAST_RANDOM_LOOP: - flow = Flow( - count=0, transitions=yee_transitions.randomloop(duration=250) - ) - elif effect == EFFECT_WHATSAPP: - flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) - elif effect == EFFECT_FACEBOOK: - flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) - elif effect == EFFECT_TWITTER: - flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) + if effect in self.custom_effects_names: + flow = Flow(**self.custom_effects[effect]) + elif effect in EFFECTS_MAP: + flow = Flow(count=0, transitions=EFFECTS_MAP[effect]()) + elif effect == EFFECT_FAST_RANDOM_LOOP: + flow = Flow(count=0, transitions=yee_transitions.randomloop(duration=250)) + elif effect == EFFECT_WHATSAPP: + flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) + elif effect == EFFECT_FACEBOOK: + flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) + elif effect == EFFECT_TWITTER: + flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) + else: + return - try: - self._bulb.start_flow(flow, light_type=self.light_type) - except BulbException as ex: - _LOGGER.error("Unable to set effect: %s", ex) + try: + self._bulb.start_flow(flow, light_type=self.light_type) + self._effect = effect + except BulbException as ex: + _LOGGER.error("Unable to set effect: %s", ex) def turn_on(self, **kwargs) -> None: """Turn the bulb on.""" @@ -721,7 +743,7 @@ class YeelightGenericLight(Light): self.set_music_mode(self.config[CONF_MODE_MUSIC]) except BulbException as ex: _LOGGER.error( - "Unable to turn on music mode," "consider disabling it: %s", ex + "Unable to turn on music mode, consider disabling it: %s", ex ) try: @@ -913,6 +935,6 @@ class YeelightAmbientLight(YeelightColorLight): bg_prop = self.PROPERTIES_MAPPING.get(prop) if not bg_prop: - bg_prop = "bg_" + prop + bg_prop = f"bg_{prop}" return super()._get_property(bg_prop, default) diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 3d27d5bd393..35c2a8ddfac 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,12 +2,7 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": [ - "yeelight==0.5.0" - ], + "requirements": ["yeelight==0.5.0"], "dependencies": [], - "codeowners": [ - "@rytilahti", - "@zewelor" - ] + "codeowners": ["@rytilahti", "@zewelor"] } diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index 3424014e8f4..c49c874dc21 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -1,19 +1,19 @@ """Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).""" import logging -import yeelightsunflower import voluptuous as vol +import yeelightsunflower -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( - Light, - ATTR_HS_COLOR, - SUPPORT_COLOR, ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, + ATTR_HS_COLOR, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + Light, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/yeelightsunflower/manifest.json b/homeassistant/components/yeelightsunflower/manifest.json index 390ff742724..6c1a44e8cb1 100644 --- a/homeassistant/components/yeelightsunflower/manifest.json +++ b/homeassistant/components/yeelightsunflower/manifest.json @@ -1,12 +1,8 @@ { "domain": "yeelightsunflower", - "name": "Yeelightsunflower", + "name": "Yeelight Sunflower", "documentation": "https://www.home-assistant.io/integrations/yeelightsunflower", - "requirements": [ - "yeelightsunflower==0.0.10" - ], + "requirements": ["yeelightsunflower==0.0.10"], "dependencies": [], - "codeowners": [ - "@lindsaymarkward" - ] + "codeowners": ["@lindsaymarkward"] } diff --git a/homeassistant/components/yessssms/manifest.json b/homeassistant/components/yessssms/manifest.json index b68649525c2..0dc6f213bce 100644 --- a/homeassistant/components/yessssms/manifest.json +++ b/homeassistant/components/yessssms/manifest.json @@ -1,12 +1,8 @@ { "domain": "yessssms", - "name": "Yessssms", + "name": "yesss! SMS", "documentation": "https://www.home-assistant.io/integrations/yessssms", - "requirements": [ - "YesssSMS==0.4.1" - ], + "requirements": ["YesssSMS==0.4.1"], "dependencies": [], - "codeowners": [ - "@flowolf" - ] + "codeowners": ["@flowolf"] } diff --git a/homeassistant/components/yessssms/notify.py b/homeassistant/components/yessssms/notify.py index 1c1eed0e89d..fbc6b50e8d6 100644 --- a/homeassistant/components/yessssms/notify.py +++ b/homeassistant/components/yessssms/notify.py @@ -1,16 +1,13 @@ """Support for the YesssSMS platform.""" import logging +from YesssSMS import YesssSMS import voluptuous as vol -from YesssSMS import YesssSMS - +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_PASSWORD, CONF_RECIPIENT, CONF_USERNAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - - from .const import CONF_PROVIDER _LOGGER = logging.getLogger(__name__) @@ -70,7 +67,7 @@ class YesssSMSNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a SMS message via Yesss.at's website.""" if self.yesss.account_is_suspended(): - # only retry to login after HASS was restarted with (hopefully) + # only retry to login after Home Assistant was restarted with (hopefully) # new login data. _LOGGER.error( "Account is suspended, cannot send SMS. " diff --git a/homeassistant/components/yi/manifest.json b/homeassistant/components/yi/manifest.json index 461f3e24330..f14d7ad742b 100644 --- a/homeassistant/components/yi/manifest.json +++ b/homeassistant/components/yi/manifest.json @@ -1,14 +1,8 @@ { "domain": "yi", - "name": "Yi", + "name": "Yi Home Cameras", "documentation": "https://www.home-assistant.io/integrations/yi", - "requirements": [ - "aioftp==0.12.0" - ], - "dependencies": [ - "ffmpeg" - ], - "codeowners": [ - "@bachya" - ] + "requirements": ["aioftp==0.12.0"], + "dependencies": ["ffmpeg"], + "codeowners": ["@bachya"] } diff --git a/homeassistant/components/yr/manifest.json b/homeassistant/components/yr/manifest.json index d49004cc0e3..10b274b8dd3 100644 --- a/homeassistant/components/yr/manifest.json +++ b/homeassistant/components/yr/manifest.json @@ -2,11 +2,7 @@ "domain": "yr", "name": "Yr", "documentation": "https://www.home-assistant.io/integrations/yr", - "requirements": [ - "xmltodict==0.12.0" - ], + "requirements": ["xmltodict==0.12.0"], "dependencies": [], - "codeowners": [ - "@danielhiversen" - ] + "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index fc754c9a257..c9392561fc8 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -1,23 +1,21 @@ """Support for Yr.no weather service.""" import asyncio import logging - from random import randrange from xml.parsers.expat import ExpatError import aiohttp import async_timeout -import xmltodict import voluptuous as vol +import xmltodict -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ELEVATION, CONF_MONITORED_CONDITIONS, - ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, @@ -26,8 +24,9 @@ from homeassistant.const import ( TEMP_CELSIUS, ) 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_utc_time_change, async_call_later +from homeassistant.helpers.event import async_call_later, async_track_utc_time_change from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -161,7 +160,7 @@ class YrData: def __init__(self, hass, coordinates, forecast, devices): """Initialize the data object.""" self._url = ( - "https://aa015h6buqvih86i1.api.met.no/" "weatherapi/locationforecast/1.9/" + "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/" ) self._urlparams = coordinates self._forecast = forecast diff --git a/homeassistant/components/yweather/manifest.json b/homeassistant/components/yweather/manifest.json index 482951e156f..9d9c76f67e4 100644 --- a/homeassistant/components/yweather/manifest.json +++ b/homeassistant/components/yweather/manifest.json @@ -1,10 +1,8 @@ { "domain": "yweather", - "name": "Yweather", + "name": "Yahoo Weather", "documentation": "https://www.home-assistant.io/integrations/yweather", - "requirements": [ - "yahooweather==0.10" - ], + "requirements": ["yahooweather==0.10"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/zabbix/manifest.json b/homeassistant/components/zabbix/manifest.json index 5959fba5fa2..5cf4adf5804 100644 --- a/homeassistant/components/zabbix/manifest.json +++ b/homeassistant/components/zabbix/manifest.json @@ -2,9 +2,7 @@ "domain": "zabbix", "name": "Zabbix", "documentation": "https://www.home-assistant.io/integrations/zabbix", - "requirements": [ - "pyzabbix==0.7.4" - ], + "requirements": ["pyzabbix==0.7.4"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/zamg/manifest.json b/homeassistant/components/zamg/manifest.json index 2b95a9ec0c7..ff131767b33 100644 --- a/homeassistant/components/zamg/manifest.json +++ b/homeassistant/components/zamg/manifest.json @@ -1,6 +1,6 @@ { "domain": "zamg", - "name": "Zamg", + "name": "Zentralanstalt für Meteorologie und Geodynamik (ZAMG)", "documentation": "https://www.home-assistant.io/integrations/zamg", "requirements": [], "dependencies": [], diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 9eea1f6612c..44c216eb1be 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -11,19 +11,12 @@ import pytz import requests import voluptuous as vol -from homeassistant.components.weather import ( - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_WIND_SPEED, - ATTR_WEATHER_ATTRIBUTION, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_WIND_BEARING, -) from homeassistant.const import ( - CONF_NAME, + ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, + CONF_NAME, __version__, ) import homeassistant.helpers.config_validation as cv @@ -43,15 +36,15 @@ DEFAULT_NAME = "zamg" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) SENSOR_TYPES = { - ATTR_WEATHER_PRESSURE: ("Pressure", "hPa", "LDstat hPa", float), + "pressure": ("Pressure", "hPa", "LDstat hPa", float), "pressure_sealevel": ("Pressure at Sea Level", "hPa", "LDred hPa", float), - ATTR_WEATHER_HUMIDITY: ("Humidity", "%", "RF %", int), - ATTR_WEATHER_WIND_SPEED: ("Wind Speed", "km/h", "WG km/h", float), - ATTR_WEATHER_WIND_BEARING: ("Wind Bearing", "°", "WR °", int), + "humidity": ("Humidity", "%", "RF %", int), + "wind_speed": ("Wind Speed", "km/h", "WG km/h", float), + "wind_bearing": ("Wind Bearing", "°", "WR °", int), "wind_max_speed": ("Top Wind Speed", "km/h", "WSG km/h", float), "wind_max_bearing": ("Top Wind Bearing", "°", "WSR °", int), "sun_last_hour": ("Sun Last Hour", "%", "SO %", int), - ATTR_WEATHER_TEMPERATURE: ("Temperature", "°C", "T °C", float), + "temperature": ("Temperature", "°C", "T °C", float), "precipitation": ("Precipitation", "l/m²", "N l/m²", float), "dewpoint": ("Dew Point", "°C", "TP °C", float), # The following probably not useful for general consumption, @@ -140,7 +133,7 @@ class ZamgSensor(Entity): def device_state_attributes(self): """Return the state attributes.""" return { - ATTR_WEATHER_ATTRIBUTION: ATTRIBUTION, + ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_STATION: self.probe.get_data("station_name"), ATTR_UPDATED: self.probe.last_update.isoformat(), } diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index d890b193d72..42746c6bad3 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -1,20 +1,20 @@ """Support for Zengge lights.""" import logging -from zengge import zengge import voluptuous as vol +from zengge import zengge -from homeassistant.const import CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, ATTR_WHITE_VALUE, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_WHITE_VALUE, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util diff --git a/homeassistant/components/zengge/manifest.json b/homeassistant/components/zengge/manifest.json index e24e4537837..1890088f291 100644 --- a/homeassistant/components/zengge/manifest.json +++ b/homeassistant/components/zengge/manifest.json @@ -2,9 +2,7 @@ "domain": "zengge", "name": "Zengge", "documentation": "https://www.home-assistant.io/integrations/zengge", - "requirements": [ - "zengge==0.2" - ], + "requirements": ["zengge==0.2"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 9f27a5fafc9..d6be4cdf6a0 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,26 +1,25 @@ """Support for exposing Home Assistant via Zeroconf.""" - +import ipaddress import logging import socket -import ipaddress import voluptuous as vol - from zeroconf import ( + NonUniqueNameException, ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf, - NonUniqueNameException, ) from homeassistant import util from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, + ATTR_NAME, EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, __version__, ) -from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT +from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF _LOGGER = logging.getLogger(__name__) @@ -30,7 +29,6 @@ ATTR_HOST = "host" ATTR_PORT = "port" ATTR_HOSTNAME = "hostname" ATTR_TYPE = "type" -ATTR_NAME = "name" ATTR_PROPERTIES = "properties" ZEROCONF_TYPE = "_home-assistant._tcp.local." diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index ba764300dae..e038b9c0da1 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -1,15 +1,9 @@ { "domain": "zeroconf", - "name": "Zeroconf", + "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": [ - "zeroconf==0.24.0" - ], - "dependencies": [ - "api" - ], - "codeowners": [ - "@robbiet480", - "@Kane610" - ] + "requirements": ["zeroconf==0.24.4"], + "dependencies": ["api"], + "codeowners": ["@robbiet480", "@Kane610"], + "quality_scale": "internal" } diff --git a/homeassistant/components/zestimate/manifest.json b/homeassistant/components/zestimate/manifest.json index 79c89406fac..c9443bc1ad5 100644 --- a/homeassistant/components/zestimate/manifest.json +++ b/homeassistant/components/zestimate/manifest.json @@ -2,9 +2,7 @@ "domain": "zestimate", "name": "Zestimate", "documentation": "https://www.home-assistant.io/integrations/zestimate", - "requirements": [ - "xmltodict==0.12.0" - ], + "requirements": ["xmltodict==0.12.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 4b8bdf5fa2e..cdf7e6304ad 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -3,11 +3,11 @@ from datetime import timedelta import logging import requests -import xmltodict import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 39f254ac9af..908d8113b2e 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -9,8 +9,8 @@ "step": { "user": { "data": { - "radio_type": "Radio type", - "usb_path": "Sti til USB enhed" + "radio_type": "Radio-type", + "usb_path": "Sti til USB-enhed" }, "title": "ZHA" } @@ -33,12 +33,35 @@ "close": "Luk", "dim_down": "D\u00e6mp ned", "dim_up": "D\u00e6mp op", + "face_1": "med ansigt 1 aktiveret", + "face_2": "med ansigt 2 aktiveret", + "face_3": "med ansigt 3 aktiveret", + "face_4": "med ansigt 4 aktiveret", + "face_5": "med ansigt 5 aktiveret", + "face_6": "med ansigt 6 aktiveret", + "face_any": "Med ethvert/specificeret ansigt(er) aktiveret", "left": "Venstre", "open": "\u00c5ben", - "right": "H\u00f8jre" + "right": "H\u00f8jre", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" }, "trigger_type": { - "device_shaken": "Enhed rystet" + "device_dropped": "Enhed faldt", + "device_flipped": "Enheden blev vendt \"{subtype}\"", + "device_knocked": "Enhed banket med \"{subtype}\"", + "device_rotated": "Enhed roteret \"{subtype}\"", + "device_shaken": "Enhed rystet", + "device_slid": "Enheden gled \"{subtype}\"", + "device_tilted": "Enheden vippes", + "remote_button_double_press": "\"{subtype}\"-knappen er dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen trykket p\u00e5 konstant", + "remote_button_long_release": "\"{subtype}\"-knappen frigivet efter langt tryk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen firedobbelt-klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt-klikket", + "remote_button_short_press": "\"{subtype}\"-knappen trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knappen frigivet", + "remote_button_triple_press": "\"{subtype}\"-knappen tredobbeltklikkes" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/es.json b/homeassistant/components/zha/.translations/es.json index b8529ce9047..fb2271b260a 100644 --- a/homeassistant/components/zha/.translations/es.json +++ b/homeassistant/components/zha/.translations/es.json @@ -54,14 +54,14 @@ "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \" {subtype} \"", "device_tilted": "Dispositivo inclinado", - "remote_button_double_press": "\"{subtipo}\" bot\u00f3n de doble clic", - "remote_button_long_press": "Bot\u00f3n \"{subtipo}\" pulsado continuamente", - "remote_button_long_release": "Bot\u00f3n \"{subtipo}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", - "remote_button_quadruple_press": "\"{subtipo}\" bot\u00f3n cu\u00e1druple pulsado", - "remote_button_quintuple_press": "\"{subtipo}\" bot\u00f3n qu\u00edntuple pulsado", - "remote_button_short_press": "Bot\u00f3n \"{subtipo}\" pulsado", - "remote_button_short_release": "Bot\u00f3n \"{subtipo}\" liberado", - "remote_button_triple_press": "\"{subtipo}\" bot\u00f3n de triple clic" + "remote_button_double_press": "\"{subtype}\" bot\u00f3n de doble clic", + "remote_button_long_press": "Bot\u00f3n \"{subtype}\" pulsado continuamente", + "remote_button_long_release": "Bot\u00f3n \"{subtype}\" liberado despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "\"{subtype}\" bot\u00f3n cu\u00e1druple pulsado", + "remote_button_quintuple_press": "\"{subtype}\" bot\u00f3n qu\u00edntuple pulsado", + "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", + "remote_button_triple_press": "\"{subtype}\" bot\u00f3n de triple clic" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 9b1ba025d7c..5d8bdfa82eb 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -59,7 +59,7 @@ "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clics", "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clics", - "remote_button_short_press": "bouton \" {subtype} \" enfonc\u00e9", + "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" \u00e0 trois clics" } diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 3a62f5d7ebe..69b8f9ad9a4 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -47,21 +47,21 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "device_dropped": "\uae30\uae30\ub97c \ub5a8\uad7c", - "device_flipped": "\"{subtype}\" \uae30\uae30\ub97c \ub4a4\uc9d1\uc74c", - "device_knocked": "\"{subtype}\" \uae30\uae30\ub97c \ub450\ub4dc\ub9bc", - "device_rotated": "\"{subtype}\" \uae30\uae30\ub97c \ud68c\uc804", - "device_shaken": "\uae30\uae30\ub97c \ud754\ub4e6", - "device_slid": "\"{subtype}\" \uae30\uae30\ub97c \uc2ac\ub77c\uc774\ub4dc", - "device_tilted": "\uae30\uae30\ub97c \uae30\uc6b8\uc784", - "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", - "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", - "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", - "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", - "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", - "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", - "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", - "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984" + "device_dropped": "\uae30\uae30\uac00 \ub5a8\uc5b4\uc84c\uc744 \ub54c", + "device_flipped": "\"{subtype}\" \uae30\uae30\uac00 \ub4a4\uc9d1\uc5b4\uc9c8 \ub54c", + "device_knocked": "\"{subtype}\" \uae30\uae30\uac00 \ub450\ub4dc\ub824\uc9c8 \ub54c", + "device_rotated": "\"{subtype}\" \uae30\uae30\uac00 \ud68c\uc804\ub420 \ub54c", + "device_shaken": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c", + "device_slid": "\"{subtype}\" \uae30\uae30\uac00 \ubbf8\ub044\ub7ec\uc9c8 \ub54c", + "device_tilted": "\uae30\uae30\uac00 \uae30\uc6b8\uc5b4\uc9c8 \ub54c", + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index c0bc7c176a2..8850fdfc07a 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -19,7 +19,7 @@ }, "device_automation": { "action_type": { - "squawk": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u0438\u0440\u0435\u043d\u0443", + "squawk": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440", "warn": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435" }, "trigger_subtype": { @@ -49,7 +49,7 @@ "trigger_type": { "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u043b\u0438", "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", - "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\" ", + "device_knocked": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\"", "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", "device_slid": "\u0421\u0434\u0432\u0438\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \"{subtype}\"", diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index ecd27c48839..377c77bf601 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,4 +1,5 @@ """Support for Zigbee Home Automation devices.""" + import logging import voluptuous as vol @@ -7,8 +8,6 @@ from homeassistant import config_entries, const as ha_const import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE -# Loading the config flow file will register the flow -from . import config_flow # noqa: F401 pylint: disable=unused-import from . import api from .core import ZHAGateway from .core.const import ( @@ -28,7 +27,6 @@ from .core.const import ( DOMAIN, RadioType, ) -from .core.registries import establish_device_mappings DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(ha_const.CONF_TYPE): cv.string}) @@ -88,7 +86,6 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ - establish_device_mappings() for component in COMPONENTS: hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {}) @@ -100,7 +97,7 @@ async def async_setup_entry(hass, config_entry): if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported - import zhaquirks # noqa: F401 pylint: disable=unused-import + import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel, import-error zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() @@ -144,5 +141,4 @@ async def async_unload_entry(hass, config_entry): for component in COMPONENTS: await hass.config_entries.async_forward_entry_unload(config_entry, component) - del hass.data[DATA_ZHA] return True diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 438b93244cf..7c732b6906e 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -1,7 +1,9 @@ """Web socket API for Zigbee Home Automation devices.""" import asyncio +import collections import logging +from typing import Any import voluptuous as vol from zigpy.types.named import EUI64 @@ -23,6 +25,7 @@ from .core.const import ( ATTR_ENDPOINT_ID, ATTR_LEVEL, ATTR_MANUFACTURER, + ATTR_MEMBERS, ATTR_NAME, ATTR_VALUE, ATTR_WARNING_DEVICE_DURATION, @@ -30,6 +33,7 @@ from .core.const import ( ATTR_WARNING_DEVICE_STROBE, ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, ATTR_WARNING_DEVICE_STROBE_INTENSITY, + BINDINGS, CHANNEL_IAS_WD, CLUSTER_COMMAND_SERVER, CLUSTER_COMMANDS_CLIENT, @@ -39,6 +43,9 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, + GROUP_ID, + GROUP_IDS, + GROUP_NAME, MFG_CLUSTER_ID_START, WARNING_DEVICE_MODE_EMERGENCY, WARNING_DEVICE_SOUND_HIGH, @@ -46,7 +53,11 @@ from .core.const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .core.helpers import async_is_bindable_target, get_matched_clusters +from .core.helpers import ( + async_get_device_info, + async_is_bindable_target, + get_matched_clusters, +) _LOGGER = logging.getLogger(__name__) @@ -62,8 +73,6 @@ ATTR_IEEE_ADDRESS = "ieee_address" ATTR_IEEE = "ieee" ATTR_SOURCE_IEEE = "source_ieee" ATTR_TARGET_IEEE = "target_ieee" -BIND_REQUEST = 0x0021 -UNBIND_REQUEST = 0x0022 SERVICE_PERMIT = "permit" SERVICE_REMOVE = "remove" @@ -157,6 +166,8 @@ SERVICE_SCHEMAS = { ), } +ClusterBinding = collections.namedtuple("ClusterBinding", "id endpoint_id type name") + @websocket_api.require_admin @websocket_api.async_response @@ -211,6 +222,34 @@ 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/devices/groupable"}) +async def websocket_get_groupable_devices(hass, connection, msg): + """Get ZHA devices that can be grouped.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + + devices = [] + for device in zha_gateway.devices.values(): + if device.is_groupable: + devices.append( + async_get_device_info( + hass, device, ha_device_registry=ha_device_registry + ) + ) + connection.send_result(msg[ID], devices) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(TYPE): "zha/groups"}) +async def websocket_get_groups(hass, connection, msg): + """Get ZHA groups.""" + groups = await get_groups(hass) + connection.send_result(msg[ID], groups) + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( @@ -236,29 +275,219 @@ async def websocket_get_device(hass, connection, msg): connection.send_result(msg[ID], device) -@callback -def async_get_device_info(hass, device, ha_device_registry=None): - """Get ZHA device.""" +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + {vol.Required(TYPE): "zha/group", vol.Required(GROUP_ID): cv.positive_int} +) +async def websocket_get_group(hass, connection, msg): + """Get ZHA group.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - ret_device = {} - ret_device.update(device.device_info) - ret_device["entities"] = [ - { - "entity_id": entity_ref.reference_id, - ATTR_NAME: entity_ref.device_info[ATTR_NAME], - } - for entity_ref in zha_gateway.device_registry[device.ieee] - ] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + group = None - if ha_device_registry is not None: - reg_device = ha_device_registry.async_get_device( - {(DOMAIN, str(device.ieee))}, set() + if group_id in zha_gateway.application_controller.groups: + group = async_get_group_info( + hass, + zha_gateway, + zha_gateway.application_controller.groups[group_id], + ha_device_registry, ) - if reg_device is not None: - ret_device["user_given_name"] = reg_device.name_by_user - ret_device["device_reg_id"] = reg_device.id - ret_device["area_id"] = reg_device.area_id - return ret_device + if not group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + connection.send_result(msg[ID], group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/add", + vol.Required(GROUP_NAME): cv.string, + vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_add_group(hass, connection, msg): + """Add a new ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = len(zha_gateway.application_controller.groups) + 1 + group_name = msg[GROUP_NAME] + zigpy_group = async_get_group_by_name(zha_gateway, group_name) + ret_group = None + members = msg.get(ATTR_MEMBERS) + + # guard against group already existing + if zigpy_group is None: + zigpy_group = zha_gateway.application_controller.groups.add_group( + group_id, group_name + ) + if members is not None: + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id)) + await asyncio.gather(*tasks) + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/remove", + vol.Required(GROUP_IDS): vol.All(cv.ensure_list, [cv.positive_int]), + } +) +async def websocket_remove_groups(hass, connection, msg): + """Remove the specified ZHA groups.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + groups = zha_gateway.application_controller.groups + group_ids = msg[GROUP_IDS] + + if len(group_ids) > 1: + tasks = [] + for group_id in group_ids: + tasks.append(remove_group(groups[group_id], zha_gateway)) + await asyncio.gather(*tasks) + else: + await remove_group(groups[group_ids[0]], zha_gateway) + ret_groups = await get_groups(hass) + connection.send_result(msg[ID], ret_groups) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/members/add", + vol.Required(GROUP_ID): cv.positive_int, + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_add_group_members(hass, connection, msg): + """Add members to a ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + members = msg[ATTR_MEMBERS] + zigpy_group = None + + if group_id in zha_gateway.application_controller.groups: + zigpy_group = zha_gateway.application_controller.groups[group_id] + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_add_to_group(group_id)) + await asyncio.gather(*tasks) + if not zigpy_group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/group/members/remove", + vol.Required(GROUP_ID): cv.positive_int, + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + } +) +async def websocket_remove_group_members(hass, connection, msg): + """Remove members from a ZHA group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + group_id = msg[GROUP_ID] + members = msg[ATTR_MEMBERS] + zigpy_group = None + + if group_id in zha_gateway.application_controller.groups: + zigpy_group = zha_gateway.application_controller.groups[group_id] + tasks = [] + for ieee in members: + tasks.append(zha_gateway.devices[ieee].async_remove_from_group(group_id)) + await asyncio.gather(*tasks) + if not zigpy_group: + connection.send_message( + websocket_api.error_message( + msg[ID], websocket_api.const.ERR_NOT_FOUND, "ZHA Group not found" + ) + ) + return + ret_group = async_get_group_info(hass, zha_gateway, zigpy_group, ha_device_registry) + connection.send_result(msg[ID], ret_group) + + +async def get_groups(hass,): + """Get ZHA Groups.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ha_device_registry = await async_get_registry(hass) + + groups = [] + for group in zha_gateway.application_controller.groups.values(): + groups.append( + async_get_group_info(hass, zha_gateway, group, ha_device_registry) + ) + return groups + + +async def remove_group(group, zha_gateway): + """Remove ZHA Group.""" + if group.members: + tasks = [] + for member_ieee in group.members.keys(): + if member_ieee[0] in zha_gateway.devices: + tasks.append( + zha_gateway.devices[member_ieee[0]].async_remove_from_group( + group.group_id + ) + ) + if tasks: + await asyncio.gather(*tasks) + else: + # we have members but none are tracked by ZHA for whatever reason + zha_gateway.application_controller.groups.pop(group.group_id) + else: + zha_gateway.application_controller.groups.pop(group.group_id) + + +@callback +def async_get_group_info(hass, zha_gateway, group, ha_device_registry): + """Get ZHA group.""" + ret_group = {} + ret_group["group_id"] = group.group_id + ret_group["name"] = group.name + ret_group["members"] = [ + async_get_device_info( + hass, + zha_gateway.get_device(member_ieee[0]), + ha_device_registry=ha_device_registry, + ) + for member_ieee in group.members.keys() + if member_ieee[0] in zha_gateway.devices + ] + return ret_group + + +@callback +def async_get_group_by_name(zha_gateway, group_name): + """Get ZHA group by name.""" + for group in zha_gateway.application_controller.groups.values(): + if group.name == group_name: + return group + return None @websocket_api.require_admin @@ -345,11 +574,15 @@ async def websocket_device_cluster_attributes(hass, connection, msg): {ID: attr_id, ATTR_NAME: attributes[attr_id][0]} ) _LOGGER.debug( - "Requested attributes for: %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{RESPONSE}: [{cluster_attributes}]", + "Requested attributes for: %s: %s, %s: '%s', %s: %s, %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + RESPONSE, + cluster_attributes, ) connection.send_result(msg[ID], cluster_attributes) @@ -399,11 +632,15 @@ async def websocket_device_cluster_commands(hass, connection, msg): } ) _LOGGER.debug( - "Requested commands for: %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{RESPONSE}: [{cluster_commands}]", + "Requested commands for: %s: %s, %s: '%s', %s: %s, %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + RESPONSE, + cluster_commands, ) connection.send_result(msg[ID], cluster_commands) @@ -443,14 +680,21 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): [attribute], allow_cache=False, only_cache=False, manufacturer=manufacturer ) _LOGGER.debug( - "Read attribute for: %s %s %s %s %s %s %s", - 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), + "Read attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s],", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_ATTRIBUTE, + attribute, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + str(success.get(attribute)), + "failure", + failure, ) connection.send_result(msg[ID], str(success.get(attribute))) @@ -473,9 +717,11 @@ async def websocket_get_bindable_devices(hass, connection, msg): ] _LOGGER.debug( - "Get bindable devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - "{}: [{}]".format("bindable devices:", devices), + "Get bindable devices: %s: [%s], %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + "bindable devices", + devices, ) connection.send_message(websocket_api.result_message(msg[ID], devices)) @@ -495,11 +741,15 @@ async def websocket_bind_devices(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] target_ieee = msg[ATTR_TARGET_IEEE] - await async_binding_operation(zha_gateway, source_ieee, target_ieee, BIND_REQUEST) + await async_binding_operation( + zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Bind_req + ) _LOGGER.info( - "Issue bind devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", + "Devices bound: %s: [%s] %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + ATTR_TARGET_IEEE, + target_ieee, ) @@ -517,12 +767,74 @@ async def websocket_unbind_devices(hass, connection, msg): zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] source_ieee = msg[ATTR_SOURCE_IEEE] target_ieee = msg[ATTR_TARGET_IEEE] - await async_binding_operation(zha_gateway, source_ieee, target_ieee, UNBIND_REQUEST) - _LOGGER.info( - "Issue unbind devices: %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", + await async_binding_operation( + zha_gateway, source_ieee, target_ieee, zdo_types.ZDOCmd.Unbind_req ) + _LOGGER.info( + "Devices un-bound: %s: [%s] %s: [%s]", + ATTR_SOURCE_IEEE, + source_ieee, + ATTR_TARGET_IEEE, + target_ieee, + ) + + +def is_cluster_binding(value: Any) -> ClusterBinding: + """Validate and transform a cluster binding.""" + if not isinstance(value, collections.Mapping): + raise vol.Invalid("Not a cluster binding") + try: + cluster_binding = ClusterBinding( + name=value["name"], + type=value["type"], + id=value["id"], + endpoint_id=value["endpoint_id"], + ) + except KeyError: + raise vol.Invalid("Not a cluster binding") + + return cluster_binding + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/groups/bind", + vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(GROUP_ID): cv.positive_int, + vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), + } +) +async def websocket_bind_group(hass, connection, msg): + """Directly bind a device to a group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee = msg[ATTR_SOURCE_IEEE] + group_id = msg[GROUP_ID] + bindings = msg[BINDINGS] + source_device = zha_gateway.get_device(source_ieee) + + await source_device.async_bind_to_group(group_id, bindings) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zha/groups/unbind", + vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(GROUP_ID): cv.positive_int, + vol.Required(BINDINGS): vol.All(cv.ensure_list, [is_cluster_binding]), + } +) +async def websocket_unbind_group(hass, connection, msg): + """Unbind a device from a group.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + source_ieee = msg[ATTR_SOURCE_IEEE] + group_id = msg[GROUP_ID] + bindings = msg[BINDINGS] + source_device = zha_gateway.get_device(source_ieee) + await source_device.async_unbind_from_group(group_id, bindings) async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation): @@ -542,22 +854,34 @@ async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operati zdo = cluster_pair.source_cluster.endpoint.device.zdo - _LOGGER.debug( - "processing binding operation for: %s %s %s", - f"{ATTR_SOURCE_IEEE}: [{source_ieee}]", - f"{ATTR_TARGET_IEEE}: [{target_ieee}]", - "{}: {}".format("cluster", cluster_pair.source_cluster.cluster_id), + op_msg = "cluster: %s %s --> [%s]" + op_params = ( + cluster_pair.source_cluster.cluster_id, + operation.name, + target_ieee, ) + zdo.debug(f"processing {op_msg}", *op_params) + bind_tasks.append( - zdo.request( - operation, - source_device.ieee, - cluster_pair.source_cluster.endpoint.endpoint_id, - cluster_pair.source_cluster.cluster_id, - destination_address, + ( + zdo.request( + operation, + source_device.ieee, + cluster_pair.source_cluster.endpoint.endpoint_id, + cluster_pair.source_cluster.cluster_id, + destination_address, + ), + op_msg, + op_params, ) ) - await asyncio.gather(*bind_tasks) + res = await asyncio.gather(*(t[0] for t in bind_tasks), return_exceptions=True) + for outcome, log_msg in zip(res, bind_tasks): + if isinstance(outcome, Exception): + fmt = log_msg[1] + " failed: %s" + else: + fmt = log_msg[1] + " completed: %s" + zdo.debug(fmt, *(log_msg[2] + (outcome,))) def async_load_api(hass): @@ -612,14 +936,21 @@ def async_load_api(hass): manufacturer=manufacturer, ) _LOGGER.debug( - "Set attribute for: %s %s %s %s %s %s %s", - 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}]", + "Set attribute for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_ATTRIBUTE, + attribute, + ATTR_VALUE, + value, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, ) hass.helpers.service.async_register_admin_service( @@ -654,15 +985,23 @@ def async_load_api(hass): manufacturer=manufacturer, ) _LOGGER.debug( - "Issue command for: %s %s %s %s %s %s %s %s", - 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}]", + "Issued command for: %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_CLUSTER_TYPE, + cluster_type, + ATTR_ENDPOINT_ID, + endpoint_id, + ATTR_COMMAND, + command, + ATTR_COMMAND_TYPE, + command_type, + ATTR_ARGS, + args, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, ) hass.helpers.service.async_register_admin_service( @@ -689,12 +1028,17 @@ def async_load_api(hass): command, *args, manufacturer=manufacturer, expect_reply=True ) _LOGGER.debug( - "Issue group command for: %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_COMMAND}: [{command}]", - f"{ATTR_ARGS}: [{args}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - f"{RESPONSE}: [{response}]", + "Issued group command for: %s: [%s] %s: [%s] %s: %s %s: [%s] %s: %s", + ATTR_CLUSTER_ID, + cluster_id, + ATTR_COMMAND, + command, + ATTR_ARGS, + args, + ATTR_MANUFACTURER, + manufacturer, + RESPONSE, + response, ) hass.helpers.service.async_register_admin_service( @@ -718,20 +1062,24 @@ def async_load_api(hass): await channel.squawk(mode, strobe, level) else: _LOGGER.error( - "Squawking IASWD: %s is missing the required IASWD channel!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Squawking IASWD: %s: [%s] is missing the required IASWD channel!", + ATTR_IEEE, + str(ieee), ) else: _LOGGER.error( - "Squawking IASWD: %s could not be found!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Squawking IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee) ) _LOGGER.debug( - "Squawking IASWD: %s %s %s %s", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), - "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), - "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), - "{}: [{}]".format(ATTR_LEVEL, level), + "Squawking IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_IEEE, + str(ieee), + ATTR_WARNING_DEVICE_MODE, + mode, + ATTR_WARNING_DEVICE_STROBE, + strobe, + ATTR_LEVEL, + level, ) hass.helpers.service.async_register_admin_service( @@ -760,20 +1108,24 @@ def async_load_api(hass): ) else: _LOGGER.error( - "Warning IASWD: %s is missing the required IASWD channel!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Warning IASWD: %s: [%s] is missing the required IASWD channel!", + ATTR_IEEE, + str(ieee), ) else: _LOGGER.error( - "Warning IASWD: %s could not be found!", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), + "Warning IASWD: %s: [%s] could not be found!", ATTR_IEEE, str(ieee) ) _LOGGER.debug( - "Warning IASWD: %s %s %s %s", - "{}: [{}]".format(ATTR_IEEE, str(ieee)), - "{}: [{}]".format(ATTR_WARNING_DEVICE_MODE, mode), - "{}: [{}]".format(ATTR_WARNING_DEVICE_STROBE, strobe), - "{}: [{}]".format(ATTR_LEVEL, level), + "Warning IASWD: %s: [%s] %s: [%s] %s: [%s] %s: [%s]", + ATTR_IEEE, + str(ieee), + ATTR_WARNING_DEVICE_MODE, + mode, + ATTR_WARNING_DEVICE_STROBE, + strobe, + ATTR_LEVEL, + level, ) hass.helpers.service.async_register_admin_service( @@ -785,7 +1137,16 @@ 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_groupable_devices) + websocket_api.async_register_command(hass, websocket_get_groups) websocket_api.async_register_command(hass, websocket_get_device) + websocket_api.async_register_command(hass, websocket_get_group) + websocket_api.async_register_command(hass, websocket_add_group) + websocket_api.async_register_command(hass, websocket_remove_groups) + websocket_api.async_register_command(hass, websocket_add_group_members) + websocket_api.async_register_command(hass, websocket_remove_group_members) + websocket_api.async_register_command(hass, websocket_bind_group) + websocket_api.async_register_command(hass, websocket_unbind_group) 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/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 24c2b92e739..d8bc1187be8 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -1,4 +1,5 @@ """Binary sensors on Zigbee Home Automation networks.""" +import functools import logging from homeassistant.components.binary_sensor import ( @@ -18,20 +19,16 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .core.const import ( - CHANNEL_ATTRIBUTE, + CHANNEL_ACCELEROMETER, + CHANNEL_OCCUPANCY, CHANNEL_ON_OFF, CHANNEL_ZONE, DATA_ZHA, DATA_ZHA_DISPATCHERS, - SENSOR_ACCELERATION, - SENSOR_OCCUPANCY, - SENSOR_OPENING, - SENSOR_TYPE, SIGNAL_ATTR_UPDATED, - UNKNOWN, ZHA_DISCOVERY_NEW, - ZONE, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -46,20 +43,7 @@ CLASS_MAPPING = { 0x002D: DEVICE_CLASS_VIBRATION, } - -async def get_ias_device_class(channel): - """Get the HA device class from the channel.""" - zone_type = await channel.get_attribute_value("zone_type") - return CLASS_MAPPING.get(zone_type) - - -DEVICE_CLASS_REGISTRY = { - UNKNOWN: None, - SENSOR_OPENING: DEVICE_CLASS_OPENING, - ZONE: get_ias_device_class, - SENSOR_OCCUPANCY: DEVICE_CLASS_OCCUPANCY, - SENSOR_ACCELERATION: DEVICE_CLASS_MOVING, -} +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -94,52 +78,39 @@ async def _async_setup_entities( """Set up the ZHA binary sensors.""" entities = [] for discovery_info in discovery_infos: - entities.append(BinarySensor(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, BinarySensor) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) class BinarySensor(ZhaEntity, BinarySensorDevice): """ZHA BinarySensor.""" - _domain = DOMAIN - _device_class = None + DEVICE_CLASS = None - def __init__(self, **kwargs): + def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA binary sensor.""" - super().__init__(**kwargs) - self._device_state_attributes = {} - self._zone_channel = self.cluster_channels.get(CHANNEL_ZONE) - self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) - self._attr_channel = self.cluster_channels.get(CHANNEL_ATTRIBUTE) - self._zha_sensor_type = kwargs[SENSOR_TYPE] + super().__init__(unique_id, zha_device, channels, **kwargs) + self._channel = channels[0] + self._device_class = self.DEVICE_CLASS - async def _determine_device_class(self): - """Determine the device class for this binary sensor.""" - device_class_supplier = DEVICE_CLASS_REGISTRY.get(self._zha_sensor_type) - if callable(device_class_supplier): - channel = self.cluster_channels.get(self._zha_sensor_type) - if channel is None: - return None - return await device_class_supplier(channel) - return device_class_supplier + async def get_device_class(self): + """Get the HA device class from the channel.""" + pass async def async_added_to_hass(self): """Run when about to be added to hass.""" - self._device_class = await self._determine_device_class() await super().async_added_to_hass() - if self._on_off_channel: - await self.async_accept_signal( - self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - if self._zone_channel: - await self.async_accept_signal( - self._zone_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - if self._attr_channel: - await self.async_accept_signal( - self._attr_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) + await self.get_device_class() + await self.async_accept_signal( + self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) @callback def async_restore_last_state(self, last_state): @@ -149,7 +120,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): @property def is_on(self) -> bool: - """Return if the switch is on based on the statemachine.""" + """Return True if the switch is on based on the state machine.""" if self._state is None: return False return self._state @@ -167,13 +138,43 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): async def async_update(self): """Attempt to retrieve on off state from the binary sensor.""" await super().async_update() - if self._on_off_channel: - self._state = await self._on_off_channel.get_attribute_value("on_off") - if self._zone_channel: - value = await self._zone_channel.get_attribute_value("zone_status") - if value is not None: - self._state = value & 3 - if self._attr_channel: - self._state = await self._attr_channel.get_attribute_value( - self._attr_channel.value_attribute - ) + attribute = getattr(self._channel, "value_attribute", "on_off") + self._state = await self._channel.get_attribute_value(attribute) + + +@STRICT_MATCH(channel_names=CHANNEL_ACCELEROMETER) +class Accelerometer(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_MOVING + + +@STRICT_MATCH(channel_names=CHANNEL_OCCUPANCY) +class Occupancy(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY + + +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) +class Opening(BinarySensor): + """ZHA BinarySensor.""" + + DEVICE_CLASS = DEVICE_CLASS_OPENING + + +@STRICT_MATCH(channel_names=CHANNEL_ZONE) +class IASZone(BinarySensor): + """ZHA IAS BinarySensor.""" + + async def get_device_class(self) -> None: + """Get the HA device class from the channel.""" + zone_type = await self._channel.get_attribute_value("zone_type") + self._device_class = CLASS_MAPPING.get(zone_type) + + async def async_update(self): + """Attempt to retrieve on off state from the binary sensor.""" + await super().async_update() + value = await self._channel.get_attribute_value("zone_status") + if value is not None: + self._state = value & 3 diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 474cb15b41a..5ee0d0ee9bb 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -1,4 +1,5 @@ """Config flow for ZHA.""" +import asyncio from collections import OrderedDict import os @@ -9,11 +10,14 @@ from homeassistant import config_entries from .core.const import ( CONF_RADIO_TYPE, CONF_USB_PATH, + CONTROLLER, + DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DOMAIN, + ZHA_GW_RADIO, RadioType, ) -from .core.helpers import check_zigpy_connection +from .core.registries import RADIO_TYPES @config_entries.HANDLERS.register(DOMAIN) @@ -57,3 +61,20 @@ class ZhaFlowHandler(config_entries.ConfigFlow): return self.async_create_entry( title=import_info[CONF_USB_PATH], data=import_info ) + + +async def check_zigpy_connection(usb_path, radio_type, database_path): + """Test zigpy radio connection.""" + try: + radio = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() + controller_application = RADIO_TYPES[radio_type][CONTROLLER] + except KeyError: + return False + try: + await radio.connect(usb_path, DEFAULT_BAUDRATE) + controller = controller_application(radio, database_path) + await asyncio.wait_for(controller.startup(auto_form=True), timeout=30) + await controller.shutdown() + except Exception: # pylint: disable=broad-except + return False + return True diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 29cecb7784e..a5ecf21e0c3 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -17,7 +17,6 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from ..const import ( - CHANNEL_ATTRIBUTE, CHANNEL_EVENT_RELAY, CHANNEL_ZDO, REPORT_CONFIG_DEFAULT, @@ -90,19 +89,19 @@ class ZigbeeChannel(LogMixin): self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._cluster = cluster self._zha_device = device - self._unique_id = "{}:{}:0x{:04x}".format( - str(device.ieee), cluster.endpoint.endpoint_id, cluster.cluster_id - ) - # this keeps logs consistent with zigpy logging - self._log_id = "0x{:04x}:{}:0x{:04x}".format( - device.nwk, cluster.endpoint.endpoint_id, cluster.cluster_id - ) + self._id = f"{cluster.endpoint.endpoint_id}:0x{cluster.cluster_id:04x}" + self._unique_id = f"{str(device.ieee)}:{self._id}" self._report_config = CLUSTER_REPORT_CONFIGS.get( self._cluster.cluster_id, self.REPORT_CONFIG ) self._status = ChannelStatus.CREATED self._cluster.add_listener(self) + @property + def id(self) -> str: + """Return channel id unique for this device only.""" + return self._id + @property def generic_id(self): """Return the generic id for this channel.""" @@ -264,8 +263,8 @@ class ZigbeeChannel(LogMixin): def log(self, level, msg, *args): """Log a message.""" - msg = "[%s]: " + msg - args = (self._log_id,) + args + msg = f"[%s:%s]: {msg}" + args = (self.device.nwk, self._id,) + args _LOGGER.log(level, msg, *args) def __getattr__(self, name): @@ -280,7 +279,6 @@ class ZigbeeChannel(LogMixin): class AttributeListeningChannel(ZigbeeChannel): """Channel for attribute reports from the cluster.""" - CHANNEL_NAME = CHANNEL_ATTRIBUTE REPORT_CONFIG = [{"attr": 0, "config": REPORT_CONFIG_DEFAULT}] def __init__(self, cluster, device): @@ -359,7 +357,7 @@ class ZDOChannel(LogMixin): def log(self, level, msg, *args): """Log a message.""" - msg = "[%s:ZDO](%s): " + msg + msg = f"[%s:ZDO](%s): {msg}" args = (self._zha_device.nwk, self._zha_device.model) + args _LOGGER.log(level, msg, *args) @@ -394,15 +392,17 @@ class EventRelayChannel(ZigbeeChannel): ) -# pylint: disable=wrong-import-position -from . import closures # noqa: F401 -from . import general # noqa: F401 -from . import homeautomation # noqa: F401 -from . import hvac # noqa: F401 -from . import lighting # noqa: F401 -from . import lightlink # noqa: F401 -from . import manufacturerspecific # noqa: F401 -from . import measurement # noqa: F401 -from . import protocol # noqa: F401 -from . import security # noqa: F401 -from . import smartenergy # noqa: F401 +# pylint: disable=wrong-import-position, import-outside-toplevel +from . import ( # noqa: F401 isort:skip + closures, + general, + homeautomation, + hvac, + lighting, + lightlink, + manufacturerspecific, + measurement, + protocol, + security, + smartenergy, +) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index dda6c1f4c13..d9d8f57eaaf 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -5,6 +5,7 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ import logging +from typing import Optional import zigpy.zcl.clusters.homeautomation as homeautomation @@ -65,6 +66,12 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) + def __init__(self, cluster, device): + """Initialize Metering.""" + super().__init__(cluster, device) + self._divisor = None + self._multiplier = None + async def async_update(self): """Retrieve latest state.""" self.debug("async_update") @@ -78,8 +85,39 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): async def async_initialize(self, from_cache): """Initialize channel.""" await self.get_attribute_value("active_power", from_cache=from_cache) + await self.fetch_config(from_cache) await super().async_initialize(from_cache) + async def fetch_config(self, from_cache): + """Fetch config from device and updates format specifier.""" + divisor = await self.get_attribute_value( + "ac_power_divisor", from_cache=from_cache + ) + if divisor is None: + divisor = await self.get_attribute_value( + "power_divisor", from_cache=from_cache + ) + self._divisor = divisor + + mult = await self.get_attribute_value( + "ac_power_multiplier", from_cache=from_cache + ) + if mult is None: + mult = await self.get_attribute_value( + "power_multiplier", from_cache=from_cache + ) + self._multiplier = mult + + @property + def divisor(self) -> Optional[int]: + """Return active power divisor.""" + return self._divisor or 1 + + @property + def multiplier(self) -> Optional[int]: + """Return active power divisor.""" + return self._multiplier or 1 + @registries.ZIGBEE_CHANNEL_REGISTRY.register( homeautomation.MeterIdentification.cluster_id diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 14d982ab1e8..db4745d51c3 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -6,6 +6,7 @@ https://home-assistant.io/integrations/zha/ """ import logging +from zigpy.exceptions import DeliveryError import zigpy.zcl.clusters.hvac as hvac from homeassistant.core import callback @@ -35,7 +36,6 @@ class FanChannel(ZigbeeChannel): async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" - from zigpy.exceptions import DeliveryError try: await self.cluster.write_attributes({"fan_mode": value}) diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index 31dd5cd63d1..39f45f6c4a2 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -18,7 +18,6 @@ from ..const import ( SIGNAL_ATTR_UPDATED, ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index e4840dae86d..69e4ea1a27a 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -6,6 +6,7 @@ https://home-assistant.io/integrations/zha/ """ import logging +from zigpy.exceptions import DeliveryError import zigpy.zcl.clusters.security as security from homeassistant.core import callback @@ -149,7 +150,6 @@ class IASZoneChannel(ZigbeeChannel): if self._zha_device.manufacturer == "LUMI": self.debug("finished IASZoneChannel configuration") return - from zigpy.exceptions import DeliveryError self.debug("started IASZoneChannel configuration") diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index ac83c2cdcd8..61be496fa1c 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -24,6 +24,7 @@ ATTR_LEVEL = "level" ATTR_LQI = "lqi" ATTR_MANUFACTURER = "manufacturer" ATTR_MANUFACTURER_CODE = "manufacturer_code" +ATTR_MEMBERS = "members" ATTR_MODEL = "model" ATTR_NAME = "name" ATTR_NWK = "nwk" @@ -41,7 +42,9 @@ ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE = "duty_cycle" ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity" BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] +BINDINGS = "bindings" +CHANNEL_ACCELEROMETER = "accelerometer" CHANNEL_ATTRIBUTE = "attribute" CHANNEL_BASIC = "basic" CHANNEL_COLOR = "light_color" @@ -49,10 +52,16 @@ CHANNEL_DOORLOCK = "door_lock" CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CHANNEL_EVENT_RELAY = "event_relay" CHANNEL_FAN = "fan" +CHANNEL_HUMIDITY = "humidity" CHANNEL_IAS_WD = "ias_wd" +CHANNEL_ILLUMINANCE = "illuminance" CHANNEL_LEVEL = ATTR_LEVEL +CHANNEL_OCCUPANCY = "occupancy" CHANNEL_ON_OFF = "on_off" CHANNEL_POWER_CONFIGURATION = "power" +CHANNEL_PRESSURE = "pressure" +CHANNEL_SMARTENERGY_METERING = "smartenergy_metering" +CHANNEL_TEMPERATURE = "temperature" CHANNEL_ZDO = "zdo" CHANNEL_ZONE = ZONE = "ias_zone" @@ -105,6 +114,10 @@ DISCOVERY_KEY = "zha_discovery_info" DOMAIN = "zha" +GROUP_ID = "group_id" +GROUP_IDS = "group_ids" +GROUP_NAME = "group_name" + MFG_CLUSTER_ID_START = 0xFC00 POWER_MAINS_POWERED = "Mains" @@ -161,15 +174,15 @@ REPORT_CONFIG_OP = ( SENSOR_ACCELERATION = "acceleration" SENSOR_BATTERY = "battery" -SENSOR_ELECTRICAL_MEASUREMENT = "electrical_measurement" +SENSOR_ELECTRICAL_MEASUREMENT = CHANNEL_ELECTRICAL_MEASUREMENT SENSOR_GENERIC = "generic" -SENSOR_HUMIDITY = "humidity" -SENSOR_ILLUMINANCE = "illuminance" +SENSOR_HUMIDITY = CHANNEL_HUMIDITY +SENSOR_ILLUMINANCE = CHANNEL_ILLUMINANCE SENSOR_METERING = "metering" -SENSOR_OCCUPANCY = "occupancy" +SENSOR_OCCUPANCY = CHANNEL_OCCUPANCY SENSOR_OPENING = "opening" -SENSOR_PRESSURE = "pressure" -SENSOR_TEMPERATURE = "temperature" +SENSOR_PRESSURE = CHANNEL_PRESSURE +SENSOR_TEMPERATURE = CHANNEL_TEMPERATURE SENSOR_TYPE = "sensor_type" SIGNAL_ATTR_UPDATED = "attribute_updated" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e5d1678ad6f..634a06f7f58 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -10,9 +10,12 @@ from enum import Enum import logging import time +from zigpy import types import zigpy.exceptions -import zigpy.quirks from zigpy.profiles import zha, zll +import zigpy.quirks +from zigpy.zcl.clusters.general import Groups +import zigpy.zdo.types as zdo_types from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( @@ -179,6 +182,17 @@ class ZHADevice(LogMixin): """Return true if this device is an end device.""" return self._zigpy_device.node_desc.is_end_device + @property + def is_groupable(self): + """Return true if this device has a group cluster.""" + if not self.available: + return False + clusters = self.async_get_clusters() + for cluster_map in clusters.values(): + for clusters in cluster_map.values(): + if Groups.cluster_id in clusters: + return True + @property def gateway(self): """Return the gateway for this device.""" @@ -506,8 +520,80 @@ class ZHADevice(LogMixin): ) return response + async def async_add_to_group(self, group_id): + """Add this device to the provided zigbee group.""" + await self._zigpy_device.add_to_group(group_id) + + async def async_remove_from_group(self, group_id): + """Remove this device from the provided zigbee group.""" + await self._zigpy_device.remove_from_group(group_id) + + async def async_bind_to_group(self, group_id, cluster_bindings): + """Directly bind this device to a group for the given clusters.""" + await self._async_group_binding_operation( + group_id, zdo_types.ZDOCmd.Bind_req, cluster_bindings + ) + + async def async_unbind_from_group(self, group_id, cluster_bindings): + """Unbind this device from a group for the given clusters.""" + await self._async_group_binding_operation( + group_id, zdo_types.ZDOCmd.Unbind_req, cluster_bindings + ) + + async def _async_group_binding_operation( + self, group_id, operation, cluster_bindings + ): + """Create or remove a direct zigbee binding between a device and a group.""" + + zdo = self._zigpy_device.zdo + op_msg = "0x%04x: %s %s, ep: %s, cluster: %s to group: 0x%04x" + destination_address = zdo_types.MultiAddress() + destination_address.addrmode = types.uint8_t(1) + destination_address.nwk = types.uint16_t(group_id) + + tasks = [] + + for cluster_binding in cluster_bindings: + if cluster_binding.endpoint_id == 0: + continue + if ( + cluster_binding.id + in self._zigpy_device.endpoints[ + cluster_binding.endpoint_id + ].out_clusters + ): + op_params = ( + self.nwk, + operation.name, + str(self.ieee), + cluster_binding.endpoint_id, + cluster_binding.id, + group_id, + ) + zdo.debug("processing " + op_msg, *op_params) + tasks.append( + ( + zdo.request( + operation, + self.ieee, + cluster_binding.endpoint_id, + cluster_binding.id, + destination_address, + ), + op_msg, + op_params, + ) + ) + res = await asyncio.gather(*(t[0] for t in tasks), return_exceptions=True) + for outcome, log_msg in zip(res, tasks): + if isinstance(outcome, Exception): + fmt = log_msg[1] + " failed: %s" + else: + fmt = log_msg[1] + " completed: %s" + zdo.debug(fmt, *(log_msg[2] + (outcome,))) + def log(self, level, msg, *args): """Log a message.""" - msg = "[%s](%s): " + msg + msg = f"[%s](%s): {msg}" args = (self.nwk, self.model) + args _LOGGER.log(level, msg, *args) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index e23862a7d3e..d128ed274c0 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -11,30 +11,18 @@ import zigpy.profiles from zigpy.zcl.clusters.general import OnOff, PowerConfiguration from homeassistant import const as ha_const -from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR -from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from .channels import AttributeListeningChannel, EventRelayChannel, ZDOChannel -from .const import ( - COMPONENTS, - CONF_DEVICE_CONFIG, - DATA_ZHA, - SENSOR_GENERIC, - SENSOR_TYPE, - UNKNOWN, - ZHA_DISCOVERY_NEW, -) +from .const import COMPONENTS, CONF_DEVICE_CONFIG, DATA_ZHA, ZHA_DISCOVERY_NEW from .registries import ( - BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, COMPONENT_CLUSTERS, DEVICE_CLASS, EVENT_RELAY_CLUSTERS, OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES, - SENSOR_TYPES, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, ZIGBEE_CHANNEL_REGISTRY, @@ -163,15 +151,6 @@ def _async_handle_profile_match( "component": component, } - if component == BINARY_SENSOR: - discovery_info.update({SENSOR_TYPE: UNKNOWN}) - for cluster_id in profile_clusters: - if cluster_id in BINARY_SENSOR_TYPES: - discovery_info.update( - {SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster_id, UNKNOWN)} - ) - break - return discovery_info @@ -291,13 +270,4 @@ def _async_handle_single_cluster_match( "component": component, } - if component == SENSOR: - discovery_info.update( - {SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, SENSOR_GENERIC)} - ) - if component == BINARY_SENSOR: - discovery_info.update( - {SENSOR_TYPE: BINARY_SENSOR_TYPES.get(cluster.cluster_id, UNKNOWN)} - ) - return discovery_info diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index ef81705ce47..72931c665ee 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -20,7 +20,6 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_send -from ..api import async_get_device_info from .const import ( ATTR_IEEE, ATTR_MANUFACTURER, @@ -65,6 +64,7 @@ from .const import ( ) from .device import DeviceStatus, ZHADevice from .discovery import async_dispatch_discovery_info, async_process_endpoint +from .helpers import async_get_device_info from .patches import apply_application_controller_patch from .registries import RADIO_TYPES from .store import async_get_registry diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index d3f06090dae..981a03fe7b5 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -4,29 +4,20 @@ Helpers for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ -import asyncio import collections import logging -import bellows.ezsp -import bellows.zigbee.application import zigpy.types -import zigpy_deconz.api -import zigpy_deconz.zigbee.application -import zigpy_xbee.api -import zigpy_xbee.zigbee.application -import zigpy_zigate.api -import zigpy_zigate.zigbee.application from homeassistant.core import callback from .const import ( + ATTR_NAME, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY, - DEFAULT_BAUDRATE, - RadioType, + DOMAIN, ) from .registries import BINDABLE_CLUSTERS @@ -56,30 +47,6 @@ async def safe_read( return {} -async def check_zigpy_connection(usb_path, radio_type, database_path): - """Test zigpy radio connection.""" - if radio_type == RadioType.ezsp.name: - radio = bellows.ezsp.EZSP() - ControllerApplication = bellows.zigbee.application.ControllerApplication - elif radio_type == RadioType.xbee.name: - radio = zigpy_xbee.api.XBee() - ControllerApplication = zigpy_xbee.zigbee.application.ControllerApplication - elif radio_type == RadioType.deconz.name: - radio = zigpy_deconz.api.Deconz() - ControllerApplication = zigpy_deconz.zigbee.application.ControllerApplication - elif radio_type == RadioType.zigate.name: - radio = zigpy_zigate.api.ZiGate() - ControllerApplication = zigpy_zigate.zigbee.application.ControllerApplication - try: - await radio.connect(usb_path, DEFAULT_BAUDRATE) - controller = ControllerApplication(radio, database_path) - await asyncio.wait_for(controller.startup(auto_form=True), timeout=30) - await controller.shutdown() - except Exception: # pylint: disable=broad-except - return False - return True - - def get_attr_id_by_name(cluster, attr_name): """Get the attribute id for a cluster attribute by its name.""" return next( @@ -164,3 +131,28 @@ class LogMixin: def error(self, msg, *args): """Error level log.""" return self.log(logging.ERROR, msg, *args) + + +@callback +def async_get_device_info(hass, device, ha_device_registry=None): + """Get ZHA device.""" + zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] + ret_device = {} + ret_device.update(device.device_info) + ret_device["entities"] = [ + { + "entity_id": entity_ref.reference_id, + ATTR_NAME: entity_ref.device_info[ATTR_NAME], + } + for entity_ref in zha_gateway.device_registry[device.ieee] + ] + + if ha_device_registry is not None: + reg_device = ha_device_registry.async_get_device( + {(DOMAIN, str(device.ieee))}, set() + ) + if reg_device is not None: + ret_device["user_given_name"] = reg_device.name_by_user + ret_device["device_reg_id"] = reg_device.id + ret_device["area_id"] = reg_device.area_id + return ret_device diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 13688a6c420..37acffd39d0 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -5,7 +5,9 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/zha/ """ import collections +from typing import Callable, Set, Union +import attr import bellows.ezsp import bellows.zigbee.application import zigpy.profiles.zha @@ -28,45 +30,120 @@ from homeassistant.components.switch import DOMAIN as SWITCH # importing channels updates registries from . import channels # noqa: F401 pylint: disable=unused-import -from .const import ( - CONTROLLER, - SENSOR_ACCELERATION, - SENSOR_BATTERY, - SENSOR_ELECTRICAL_MEASUREMENT, - SENSOR_HUMIDITY, - SENSOR_ILLUMINANCE, - SENSOR_METERING, - SENSOR_OCCUPANCY, - SENSOR_OPENING, - SENSOR_PRESSURE, - SENSOR_TEMPERATURE, - ZHA_GW_RADIO, - ZHA_GW_RADIO_DESCRIPTION, - ZONE, - RadioType, -) -from .decorators import DictRegistry, SetRegistry +from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType +from .decorators import CALLABLE_T, DictRegistry, SetRegistry + +SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 +SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 +SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 + +REMOTE_DEVICE_TYPES = { + zigpy.profiles.zha.PROFILE_ID: [ + zigpy.profiles.zha.DeviceType.COLOR_CONTROLLER, + zigpy.profiles.zha.DeviceType.COLOR_DIMMER_SWITCH, + zigpy.profiles.zha.DeviceType.COLOR_SCENE_CONTROLLER, + zigpy.profiles.zha.DeviceType.DIMMER_SWITCH, + zigpy.profiles.zha.DeviceType.NON_COLOR_CONTROLLER, + zigpy.profiles.zha.DeviceType.NON_COLOR_SCENE_CONTROLLER, + zigpy.profiles.zha.DeviceType.REMOTE_CONTROL, + zigpy.profiles.zha.DeviceType.SCENE_SELECTOR, + ], + zigpy.profiles.zll.PROFILE_ID: [ + zigpy.profiles.zll.DeviceType.COLOR_CONTROLLER, + zigpy.profiles.zll.DeviceType.COLOR_SCENE_CONTROLLER, + zigpy.profiles.zll.DeviceType.CONTROL_BRIDGE, + zigpy.profiles.zll.DeviceType.CONTROLLER, + zigpy.profiles.zll.DeviceType.SCENE_CONTROLLER, + ], +} + +SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { + # this works for now but if we hit conflicts we can break it out to + # a different dict that is keyed by manufacturer + SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, + SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, + zcl.clusters.closures.DoorLock: LOCK, + zcl.clusters.general.AnalogInput.cluster_id: SENSOR, + zcl.clusters.general.MultistateInput.cluster_id: SENSOR, + zcl.clusters.general.OnOff: SWITCH, + zcl.clusters.general.PowerConfiguration: SENSOR, + zcl.clusters.homeautomation.ElectricalMeasurement: SENSOR, + zcl.clusters.hvac.Fan: FAN, + zcl.clusters.measurement.IlluminanceMeasurement: SENSOR, + zcl.clusters.measurement.OccupancySensing: BINARY_SENSOR, + zcl.clusters.measurement.PressureMeasurement: SENSOR, + zcl.clusters.measurement.RelativeHumidity: SENSOR, + zcl.clusters.measurement.TemperatureMeasurement: SENSOR, + zcl.clusters.security.IasZone: BINARY_SENSOR, + zcl.clusters.smartenergy.Metering: SENSOR, +} + +SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {zcl.clusters.general.OnOff: BINARY_SENSOR} + +SWITCH_CLUSTERS = SetRegistry() BINARY_SENSOR_CLUSTERS = SetRegistry() -BINARY_SENSOR_TYPES = {} +BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) + BINDABLE_CLUSTERS = SetRegistry() CHANNEL_ONLY_CLUSTERS = SetRegistry() CLUSTER_REPORT_CONFIGS = {} CUSTOM_CLUSTER_MAPPINGS = {} -DEVICE_CLASS = collections.defaultdict(dict) + +DEVICE_CLASS = { + zigpy.profiles.zha.PROFILE_ID: { + SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, + zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, + zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, + zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, + }, + zigpy.profiles.zll.PROFILE_ID: { + zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, + zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, + }, +} + DEVICE_TRACKER_CLUSTERS = SetRegistry() EVENT_RELAY_CLUSTERS = SetRegistry() LIGHT_CLUSTERS = SetRegistry() OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() -RADIO_TYPES = {} -REMOTE_DEVICE_TYPES = collections.defaultdict(list) -SENSOR_TYPES = {} -SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} -SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} -SWITCH_CLUSTERS = SetRegistry() -SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 -SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 -SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 + +RADIO_TYPES = { + RadioType.ezsp.name: { + ZHA_GW_RADIO: bellows.ezsp.EZSP, + CONTROLLER: bellows.zigbee.application.ControllerApplication, + ZHA_GW_RADIO_DESCRIPTION: "EZSP", + }, + RadioType.deconz.name: { + ZHA_GW_RADIO: zigpy_deconz.api.Deconz, + CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, + ZHA_GW_RADIO_DESCRIPTION: "Deconz", + }, + RadioType.xbee.name: { + ZHA_GW_RADIO: zigpy_xbee.api.XBee, + CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication, + ZHA_GW_RADIO_DESCRIPTION: "XBee", + }, + RadioType.zigate.name: { + ZHA_GW_RADIO: zigpy_zigate.api.ZiGate, + CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication, + ZHA_GW_RADIO_DESCRIPTION: "ZiGate", + }, +} COMPONENT_CLUSTERS = { BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, @@ -78,132 +155,133 @@ COMPONENT_CLUSTERS = { ZIGBEE_CHANNEL_REGISTRY = DictRegistry() -def establish_device_mappings(): - """Establish mappings between ZCL objects and HA ZHA objects. +def set_or_callable(value): + """Convert single str or None to a set. Pass through callables and sets.""" + if value is None: + return frozenset() + if callable(value): + return value + if isinstance(value, (frozenset, set, list)): + return frozenset(value) + return frozenset([str(value)]) - These cannot be module level, as importing bellows must be done in a - in a function. - """ - RADIO_TYPES[RadioType.ezsp.name] = { - ZHA_GW_RADIO: bellows.ezsp.EZSP, - CONTROLLER: bellows.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "EZSP", - } - RADIO_TYPES[RadioType.deconz.name] = { - ZHA_GW_RADIO: zigpy_deconz.api.Deconz, - CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "Deconz", - } +@attr.s(frozen=True) +class MatchRule: + """Match a ZHA Entity to a channel name or generic id.""" - RADIO_TYPES[RadioType.xbee.name] = { - ZHA_GW_RADIO: zigpy_xbee.api.XBee, - CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "XBee", - } - - RADIO_TYPES[RadioType.zigate.name] = { - ZHA_GW_RADIO: zigpy_zigate.api.ZiGate, - CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication, - ZHA_GW_RADIO_DESCRIPTION: "ZiGate", - } - - BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) - - BINARY_SENSOR_TYPES.update( - { - SMARTTHINGS_ACCELERATION_CLUSTER: SENSOR_ACCELERATION, - zcl.clusters.general.OnOff.cluster_id: SENSOR_OPENING, - zcl.clusters.measurement.OccupancySensing.cluster_id: SENSOR_OCCUPANCY, - zcl.clusters.security.IasZone.cluster_id: ZONE, - } + channel_names: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + generic_ids: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + manufacturers: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable + ) + models: Union[Callable, Set[str], str] = attr.ib( + factory=frozenset, converter=set_or_callable ) - DEVICE_CLASS[zigpy.profiles.zha.PROFILE_ID].update( - { - SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, - zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, - zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, - zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, - zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, - zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, - } - ) - DEVICE_CLASS[zigpy.profiles.zll.PROFILE_ID].update( - { - zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, - zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT, - zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, - } - ) +class ZHAEntityRegistry: + """Channel to ZHA Entity mapping.""" - SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update( - { - # this works for now but if we hit conflicts we can break it out to - # a different dict that is keyed by manufacturer - SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, - SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, - zcl.clusters.closures.DoorLock: LOCK, - zcl.clusters.general.AnalogInput.cluster_id: SENSOR, - zcl.clusters.general.MultistateInput.cluster_id: SENSOR, - zcl.clusters.general.OnOff: SWITCH, - zcl.clusters.general.PowerConfiguration: SENSOR, - zcl.clusters.homeautomation.ElectricalMeasurement: SENSOR, - zcl.clusters.hvac.Fan: FAN, - zcl.clusters.measurement.IlluminanceMeasurement: SENSOR, - zcl.clusters.measurement.OccupancySensing: BINARY_SENSOR, - zcl.clusters.measurement.PressureMeasurement: SENSOR, - zcl.clusters.measurement.RelativeHumidity: SENSOR, - zcl.clusters.measurement.TemperatureMeasurement: SENSOR, - zcl.clusters.security.IasZone: BINARY_SENSOR, - zcl.clusters.smartenergy.Metering: SENSOR, - } - ) + def __init__(self): + """Initialize Registry instance.""" + self._strict_registry = collections.defaultdict(dict) + self._loose_registry = collections.defaultdict(dict) - SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update( - {zcl.clusters.general.OnOff: BINARY_SENSOR} - ) + def get_entity( + self, component: str, zha_device, chnls: dict, default: CALLABLE_T = None + ) -> CALLABLE_T: + """Match a ZHA Channels to a ZHA Entity class.""" + for match in self._strict_registry[component]: + if self._strict_matched(zha_device, chnls, match): + return self._strict_registry[component][match] - SENSOR_TYPES.update( - { - SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR_HUMIDITY, - zcl.clusters.general.PowerConfiguration.cluster_id: SENSOR_BATTERY, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: SENSOR_ELECTRICAL_MEASUREMENT, - zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: SENSOR_ILLUMINANCE, - zcl.clusters.measurement.PressureMeasurement.cluster_id: SENSOR_PRESSURE, - zcl.clusters.measurement.RelativeHumidity.cluster_id: SENSOR_HUMIDITY, - zcl.clusters.measurement.TemperatureMeasurement.cluster_id: SENSOR_TEMPERATURE, - zcl.clusters.smartenergy.Metering.cluster_id: SENSOR_METERING, - } - ) + return default - zha = zigpy.profiles.zha - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.NON_COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append( - zha.DeviceType.NON_COLOR_SCENE_CONTROLLER - ) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.REMOTE_CONTROL) - REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.SCENE_SELECTOR) + def strict_match( + self, + component: str, + channel_names: Union[Callable, Set[str], str] = None, + generic_ids: Union[Callable, Set[str], str] = None, + manufacturers: Union[Callable, Set[str], str] = None, + models: Union[Callable, Set[str], str] = None, + ) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Decorate a strict match rule.""" - zll = zigpy.profiles.zll - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROL_BRIDGE) - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROLLER) - REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER) + rule = MatchRule(channel_names, generic_ids, manufacturers, models) + + def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T: + """Register a strict match rule. + + All non empty fields of a match rule must match. + """ + self._strict_registry[component][rule] = zha_ent + return zha_ent + + return decorator + + def loose_match( + self, + component: str, + channel_names: Union[Callable, Set[str], str] = None, + generic_ids: Union[Callable, Set[str], str] = None, + manufacturers: Union[Callable, Set[str], str] = None, + models: Union[Callable, Set[str], str] = None, + ) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Decorate a loose match rule.""" + + rule = MatchRule(channel_names, generic_ids, manufacturers, models) + + def decorator(zha_entity: CALLABLE_T) -> CALLABLE_T: + """Register a loose match rule. + + All non empty fields of a match rule must match. + """ + self._loose_registry[component][rule] = zha_entity + return zha_entity + + return decorator + + def _strict_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool: + """Return True if this device matches the criteria.""" + return all(self._matched(zha_device, chnls, rule)) + + def _loose_matched(self, zha_device, chnls: dict, rule: MatchRule) -> bool: + """Return True if this device matches the criteria.""" + return any(self._matched(zha_device, chnls, rule)) + + @staticmethod + def _matched(zha_device, chnls: dict, rule: MatchRule) -> list: + """Return a list of field matches.""" + if not any(attr.asdict(rule).values()): + return [False] + + matches = [] + if rule.channel_names: + channel_names = {ch.name for ch in chnls} + matches.append(rule.channel_names.issubset(channel_names)) + + if rule.generic_ids: + all_generic_ids = {ch.generic_id for ch in chnls} + matches.append(rule.generic_ids.issubset(all_generic_ids)) + + if rule.manufacturers: + if callable(rule.manufacturers): + matches.append(rule.manufacturers(zha_device.manufacturer)) + else: + matches.append(zha_device.manufacturer in rule.manufacturers) + + if rule.models: + if callable(rule.models): + matches.append(rule.models(zha_device.model)) + else: + matches.append(zha_device.model in rule.models) + + return matches + + +ZHA_ENTITIES = ZHAEntityRegistry() diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index bcc9b2a42d4..46fef76b656 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -2,8 +2,7 @@ # pylint: disable=unused-import from collections import OrderedDict import logging -from typing import MutableMapping -from typing import cast +from typing import MutableMapping, cast import attr diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 60a1f6c3c40..76548935814 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -1,4 +1,5 @@ """Support for the ZHA platform.""" +import functools import logging import time @@ -14,9 +15,11 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity -from .sensor import battery_percentage_remaining_formatter +from .sensor import Battery +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -47,11 +50,20 @@ async def _async_setup_entities( """Set up the ZHA device trackers.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZHADeviceScannerEntity(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity( + DOMAIN, zha_dev, channels, ZHADeviceScannerEntity + ) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): """Represent a tracked device.""" @@ -100,7 +112,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): """Handle tracking.""" self.debug("battery_percentage_remaining updated: %s", value) self._connected = True - self._battery_level = battery_percentage_remaining_formatter(value) + self._battery_level = Battery.formatter(value) self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index cdd62b11d1e..b7c46e5a40a 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -2,11 +2,11 @@ import voluptuous as vol import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE -from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from . import DOMAIN from .core.helpers import async_get_zha_device diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 108d8e27a9f..0b001bdedbc 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -30,8 +30,6 @@ RESTART_GRACE_PERIOD = 7200 # 2 hours class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): """A base class for ZHA entities.""" - _domain = None # Must be overridden by subclasses - def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwargs): """Init ZHA entity.""" self._force_update = False @@ -187,6 +185,6 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): def log(self, level, msg, *args): """Log a message.""" - msg = "%s: " + msg + msg = f"%s: {msg}" args = (self.entity_id,) + args _LOGGER.log(level, msg, *args) diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 43ad2291cb7..f489447e530 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,4 +1,5 @@ """Fans on Zigbee Home Automation networks.""" +import functools import logging from homeassistant.components.fan import ( @@ -20,6 +21,7 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -45,6 +47,7 @@ SPEED_LIST = [ VALUE_TO_SPEED = dict(enumerate(SPEED_LIST)) SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -79,16 +82,21 @@ async def _async_setup_entities( """Set up the ZHA fans.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZhaFan(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, ZhaFan) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_FAN) class ZhaFan(ZhaEntity, FanEntity): """Representation of a ZHA fan.""" - _domain = DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index fb388afac0f..eb7d3297b43 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,5 +1,6 @@ """Lights on Zigbee Home Automation networks.""" from datetime import timedelta +import functools import logging from zigpy.zcl.foundation import Status @@ -21,6 +22,7 @@ from .core.const import ( SIGNAL_SET_LEVEL, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -36,6 +38,7 @@ UPDATE_COLORLOOP_HUE = 0x8 UNSUPPORTED_ATTRIBUTE = 0x86 SCAN_INTERVAL = timedelta(minutes=60) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN) PARALLEL_UPDATES = 5 @@ -71,17 +74,21 @@ async def _async_setup_entities( """Set up the ZHA lights.""" entities = [] for discovery_info in discovery_infos: - zha_light = Light(**discovery_info) - entities.append(zha_light) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(light.DOMAIN, zha_dev, channels, Light) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) class Light(ZhaEntity, light.Light): """Representation of a ZHA or ZLL light.""" - _domain = light.DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index a2151b4bdcb..bf82252246c 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -1,4 +1,5 @@ """Locks on Zigbee Home Automation networks.""" +import functools import logging from zigpy.zcl.foundation import Status @@ -19,6 +20,7 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -26,6 +28,7 @@ _LOGGER = logging.getLogger(__name__) """ The first state is Zigbee 'Not fully locked' """ STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) VALUE_TO_STATE = dict(enumerate(STATE_LIST)) @@ -62,16 +65,21 @@ async def _async_setup_entities( """Set up the ZHA locks.""" entities = [] for discovery_info in discovery_infos: - entities.append(ZhaDoorLock(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, ZhaDoorLock) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_DOORLOCK) class ZhaDoorLock(ZhaEntity, LockDevice): """Representation of a ZHA lock.""" - _domain = DOMAIN - def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 8781625d326..e3d0eda3e02 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,11 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.11.0", - "zha-quirks==0.0.28", + "bellows-homeassistant==0.12.0", + "zha-quirks==0.0.31", "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.11.0", - "zigpy-xbee-homeassistant==0.7.0", + "zigpy-homeassistant==0.12.0", + "zigpy-xbee-homeassistant==0.8.0", "zigpy-zigate==0.5.0" ], "dependencies": [], diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index b260dfc5459..3b73a9793c9 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -1,4 +1,5 @@ """Sensors on Zigbee Home Automation networks.""" +import functools import logging import numbers @@ -11,30 +12,31 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_TEMPERATURE, DOMAIN, ) -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.const import ( + ATTR_UNIT_OF_MEASUREMENT, + POWER_WATT, + STATE_UNKNOWN, + TEMP_CELSIUS, +) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util.temperature import fahrenheit_to_celsius from .core.const import ( - CHANNEL_ATTRIBUTE, CHANNEL_ELECTRICAL_MEASUREMENT, + CHANNEL_HUMIDITY, + CHANNEL_ILLUMINANCE, CHANNEL_POWER_CONFIGURATION, + CHANNEL_PRESSURE, + CHANNEL_SMARTENERGY_METERING, + CHANNEL_TEMPERATURE, DATA_ZHA, DATA_ZHA_DISPATCHERS, - SENSOR_BATTERY, - SENSOR_ELECTRICAL_MEASUREMENT, - SENSOR_GENERIC, - SENSOR_HUMIDITY, - SENSOR_ILLUMINANCE, - SENSOR_METERING, - SENSOR_PRESSURE, - SENSOR_TEMPERATURE, - SENSOR_TYPE, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, - UNKNOWN, ZHA_DISCOVERY_NEW, ) +from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES from .entity import ZhaEntity PARALLEL_UPDATES = 5 @@ -56,115 +58,8 @@ BATTERY_SIZES = { 255: "Unknown", } - -# Formatter functions -def pass_through_formatter(value): - """No op update function.""" - return value - - -def illuminance_formatter(value): - """Convert Illimination data.""" - if value is None: - return None - return round(pow(10, ((value - 1) / 10000)), 1) - - -def temperature_formatter(value): - """Convert temperature data.""" - if value is None: - return None - return round(value / 100, 1) - - -def humidity_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - return round(float(value) / 100, 1) - - -def active_power_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - return round(float(value) / 10, 1) - - -def pressure_formatter(value): - """Return the state of the entity.""" - if value is None: - return None - - return round(float(value)) - - -def battery_percentage_remaining_formatter(value): - """Return the state of the entity.""" - # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ - if not isinstance(value, numbers.Number) or value == -1: - return value - value = value / 2 - value = int(round(value)) - return value - - -async def async_battery_device_state_attr_provider(channel): - """Return device statr attrs for battery sensors.""" - state_attrs = {} - battery_size = await channel.get_attribute_value("battery_size") - if battery_size is not None: - state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") - battery_quantity = await channel.get_attribute_value("battery_quantity") - if battery_quantity is not None: - state_attrs["battery_quantity"] = battery_quantity - return state_attrs - - -FORMATTER_FUNC_REGISTRY = { - SENSOR_HUMIDITY: humidity_formatter, - SENSOR_TEMPERATURE: temperature_formatter, - SENSOR_PRESSURE: pressure_formatter, - SENSOR_ELECTRICAL_MEASUREMENT: active_power_formatter, - SENSOR_ILLUMINANCE: illuminance_formatter, - SENSOR_GENERIC: pass_through_formatter, - SENSOR_BATTERY: battery_percentage_remaining_formatter, -} - -UNIT_REGISTRY = { - SENSOR_HUMIDITY: "%", - SENSOR_TEMPERATURE: TEMP_CELSIUS, - SENSOR_PRESSURE: "hPa", - SENSOR_ILLUMINANCE: "lx", - SENSOR_ELECTRICAL_MEASUREMENT: POWER_WATT, - SENSOR_GENERIC: None, - SENSOR_BATTERY: "%", -} - -CHANNEL_REGISTRY = { - SENSOR_ELECTRICAL_MEASUREMENT: CHANNEL_ELECTRICAL_MEASUREMENT, - SENSOR_BATTERY: CHANNEL_POWER_CONFIGURATION, -} - -POLLING_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: True} - -FORCE_UPDATE_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: False} - -DEVICE_CLASS_REGISTRY = { - UNKNOWN: None, - SENSOR_HUMIDITY: DEVICE_CLASS_HUMIDITY, - SENSOR_TEMPERATURE: DEVICE_CLASS_TEMPERATURE, - SENSOR_PRESSURE: DEVICE_CLASS_PRESSURE, - SENSOR_ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE, - SENSOR_METERING: DEVICE_CLASS_POWER, - SENSOR_ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER, - SENSOR_BATTERY: DEVICE_CLASS_BATTERY, -} - - -DEVICE_STATE_ATTR_PROVIDER_REGISTRY = { - SENSOR_BATTERY: async_battery_device_state_attr_provider -} +CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}" +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -201,48 +96,39 @@ async def _async_setup_entities( for discovery_info in discovery_infos: entities.append(await make_sensor(discovery_info)) - async_add_entities(entities, update_before_add=True) + if entities: + async_add_entities(entities, update_before_add=True) async def make_sensor(discovery_info): """Create ZHA sensors factory.""" - return Sensor(**discovery_info) + + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] + + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, Sensor) + return entity(**discovery_info) class Sensor(ZhaEntity): """Base ZHA sensor.""" - _domain = DOMAIN + _decimals = 1 + _device_class = None + _divisor = 1 + _multiplier = 1 + _unit = None def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._sensor_type = kwargs.get(SENSOR_TYPE, SENSOR_GENERIC) - 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 - ) + self._channel = channels[0] async def async_added_to_hass(self): """Run when about to be added to hass.""" await super().async_added_to_hass() - if self.state_attr_provider is not None: - self._device_state_attributes = await self.state_attr_provider( - self._channel - ) + self._device_state_attributes = await self.async_state_attr_provider() + await self.async_accept_signal( self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state ) @@ -271,18 +157,139 @@ class Sensor(ZhaEntity): def async_set_state(self, state): """Handle state update from channel.""" - # 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 - 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) + if state is not None: + state = self.formatter(state) + self._state = state self.async_schedule_update_ha_state() @callback def async_restore_last_state(self, last_state): """Restore previous state.""" self._state = last_state.state - self._unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + @callback + async def async_state_attr_provider(self): + """Initialize device state attributes.""" + return {} + + def formatter(self, value): + """Numeric pass-through formatter.""" + if self._decimals > 0: + return round( + float(value * self._multiplier) / self._divisor, self._decimals + ) + return round(float(value * self._multiplier) / self._divisor) + + +@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) +class Battery(Sensor): + """Battery sensor of power configuration cluster.""" + + _device_class = DEVICE_CLASS_BATTERY + _unit = "%" + + @staticmethod + def formatter(value): + """Return the state of the entity.""" + # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ + if not isinstance(value, numbers.Number) or value == -1: + return value + value = round(value / 2) + return value + + async def async_state_attr_provider(self): + """Return device state attrs for battery sensors.""" + state_attrs = {} + battery_size = await self._channel.get_attribute_value("battery_size") + if battery_size is not None: + state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") + battery_quantity = await self._channel.get_attribute_value("battery_quantity") + if battery_quantity is not None: + state_attrs["battery_quantity"] = battery_quantity + return state_attrs + + +@STRICT_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) +class ElectricalMeasurement(Sensor): + """Active power measurement.""" + + _device_class = DEVICE_CLASS_POWER + _divisor = 10 + _unit = POWER_WATT + + @property + def should_poll(self) -> bool: + """Return True if HA needs to poll for state changes.""" + return True + + def formatter(self, value) -> int: + """Return 'normalized' value.""" + return round(value * self._channel.multiplier / self._channel.divisor) + + +@STRICT_MATCH(generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER) +@STRICT_MATCH(channel_names=CHANNEL_HUMIDITY) +class Humidity(Sensor): + """Humidity sensor.""" + + _device_class = DEVICE_CLASS_HUMIDITY + _divisor = 100 + _unit = "%" + + +@STRICT_MATCH(channel_names=CHANNEL_ILLUMINANCE) +class Illuminance(Sensor): + """Illuminance Sensor.""" + + _device_class = DEVICE_CLASS_ILLUMINANCE + _unit = "lx" + + @staticmethod + def formatter(value): + """Convert illumination data.""" + return round(pow(10, ((value - 1) / 10000)), 1) + + +@STRICT_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) +class SmartEnergyMetering(Sensor): + """Metering sensor.""" + + _device_class = DEVICE_CLASS_POWER + + def formatter(self, value): + """Pass through channel formatter.""" + return self._channel.formatter_function(value) + + @property + def unit_of_measurement(self): + """Return Unit of measurement.""" + return self._channel.unit_of_measurement + + +@STRICT_MATCH(channel_names=CHANNEL_PRESSURE) +class Pressure(Sensor): + """Pressure sensor.""" + + _device_class = DEVICE_CLASS_PRESSURE + _decimals = 0 + _unit = "hPa" + + +@STRICT_MATCH(channel_names=CHANNEL_TEMPERATURE) +class Temperature(Sensor): + """Temperature Sensor.""" + + _device_class = DEVICE_CLASS_TEMPERATURE + _divisor = 100 + _unit = TEMP_CELSIUS + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + if last_state.state == STATE_UNKNOWN: + return + if last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) != TEMP_CELSIUS: + ftemp = float(last_state.state) + self._state = round(fahrenheit_to_celsius(ftemp), 1) + return + self._state = last_state.state diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index ab8e9c19580..3e38d6982f0 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -1,7 +1,7 @@ # Describes the format for available zha services permit: - description: Allow nodes to join the ZigBee network. + description: Allow nodes to join the Zigbee network. fields: duration: description: Time to permit joins, in seconds @@ -11,7 +11,7 @@ permit: example: "00:0d:6f:00:05:7d:2d:34" remove: - description: Remove a node from the ZigBee network. + description: Remove a node from the Zigbee network. fields: ieee_address: description: IEEE address of the node to remove diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index bfe816d614a..cbd29925f62 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -1,4 +1,5 @@ """Switches on Zigbee Home Automation networks.""" +import functools import logging from zigpy.zcl.foundation import Status @@ -15,9 +16,11 @@ from .core.const import ( SIGNAL_ATTR_UPDATED, ZHA_DISCOVERY_NEW, ) +from .core.registries import ZHA_ENTITIES from .entity import ZhaEntity _LOGGER = logging.getLogger(__name__) +STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -52,16 +55,21 @@ async def _async_setup_entities( """Set up the ZHA switches.""" entities = [] for discovery_info in discovery_infos: - entities.append(Switch(**discovery_info)) + zha_dev = discovery_info["zha_device"] + channels = discovery_info["channels"] - async_add_entities(entities, update_before_add=True) + entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, Switch) + if entity: + entities.append(entity(**discovery_info)) + + if entities: + async_add_entities(entities, update_before_add=True) +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) class Switch(ZhaEntity, SwitchDevice): """ZHA switch.""" - _domain = DOMAIN - def __init__(self, **kwargs): """Initialize the ZHA switch.""" super().__init__(**kwargs) diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index b94e19d6dbd..203131afdb1 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -161,9 +161,7 @@ class ZhongHongClimate(ClimateDevice): @property def unique_id(self): """Return the unique ID of the HVAC.""" - return "zhong_hong_hvac_{}_{}".format( - self._device.addr_out, self._device.addr_in - ) + return f"zhong_hong_hvac_{self._device.addr_out}_{self._device.addr_in}" @property def supported_features(self): diff --git a/homeassistant/components/zhong_hong/manifest.json b/homeassistant/components/zhong_hong/manifest.json index 32ee6964321..13a65ad1646 100644 --- a/homeassistant/components/zhong_hong/manifest.json +++ b/homeassistant/components/zhong_hong/manifest.json @@ -1,10 +1,8 @@ { "domain": "zhong_hong", - "name": "Zhong hong", + "name": "ZhongHong", "documentation": "https://www.home-assistant.io/integrations/zhong_hong", - "requirements": [ - "zhong_hong_hvac==1.0.9" - ], + "requirements": ["zhong_hong_hvac==1.0.9"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index e74726a70f9..3a4fa9029bd 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -1,24 +1,24 @@ """Support for Zigbee devices.""" -import logging from binascii import hexlify, unhexlify +import logging -import xbee_helper.const as xb_const -from xbee_helper import ZigBee -from xbee_helper.device import convert_adc -from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure from serial import Serial, SerialException import voluptuous as vol +from xbee_helper import ZigBee +import xbee_helper.const as xb_const +from xbee_helper.device import convert_adc +from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure from homeassistant.const import ( - EVENT_HOMEASSISTANT_STOP, + CONF_ADDRESS, CONF_DEVICE, CONF_NAME, CONF_PIN, - CONF_ADDRESS, + EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -325,7 +325,7 @@ class ZigBeeDigitalIn(Entity): except ZIGBEE_TX_FAILURE: _LOGGER.warning( "Transmission failure when attempting to get sample from " - "ZigBee device at address: %s", + "Zigbee device at address: %s", hexlify(self._config.address), ) return @@ -335,7 +335,7 @@ class ZigBeeDigitalIn(Entity): pin_name = DIGITAL_PINS[self._config.pin] if pin_name not in sample: _LOGGER.warning( - "Pin %s (%s) was not in the sample provided by Zigbee device " "%s.", + "Pin %s (%s) was not in the sample provided by Zigbee device %s.", self._config.pin, pin_name, hexlify(self._config.address), @@ -348,7 +348,7 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): """Representation of a GPIO pin configured as a digital input.""" def _set_state(self, state): - """Initialize the ZigBee digital out device.""" + """Initialize the Zigbee digital out device.""" try: DEVICE.set_gpio_pin( self._config.pin, self._config.bool2state[state], self._config.address @@ -356,12 +356,12 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): except ZIGBEE_TX_FAILURE: _LOGGER.warning( "Transmission failure when attempting to set output pin on " - "ZigBee device at address: %s", + "Zigbee device at address: %s", hexlify(self._config.address), ) return except ZIGBEE_EXCEPTION as exc: - _LOGGER.exception("Unable to set digital pin on ZigBee device: %s", exc) + _LOGGER.exception("Unable to set digital pin on Zigbee device: %s", exc) return self._state = state if not self.should_poll: @@ -376,19 +376,19 @@ class ZigBeeDigitalOut(ZigBeeDigitalIn): self._set_state(False) def update(self): - """Ask the ZigBee device what its output is set to.""" + """Ask the Zigbee device what its output is set to.""" try: pin_state = DEVICE.get_gpio_pin(self._config.pin, self._config.address) except ZIGBEE_TX_FAILURE: _LOGGER.warning( "Transmission failure when attempting to get output pin status" - " from ZigBee device at address: %s", + " from Zigbee device at address: %s", hexlify(self._config.address), ) return except ZIGBEE_EXCEPTION as exc: _LOGGER.exception( - "Unable to get output pin status from ZigBee device: %s", exc + "Unable to get output pin status from Zigbee device: %s", exc ) return self._state = self._config.state2bool[pin_state] @@ -462,8 +462,8 @@ class ZigBeeAnalogIn(Entity): except ZIGBEE_TX_FAILURE: _LOGGER.warning( "Transmission failure when attempting to get sample from " - "ZigBee device at address: %s", + "Zigbee device at address: %s", hexlify(self._config.address), ) except ZIGBEE_EXCEPTION as exc: - _LOGGER.exception("Unable to get sample from ZigBee device: %s", exc) + _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) diff --git a/homeassistant/components/zigbee/manifest.json b/homeassistant/components/zigbee/manifest.json index 3968b0e294f..d6c0d76f3c0 100644 --- a/homeassistant/components/zigbee/manifest.json +++ b/homeassistant/components/zigbee/manifest.json @@ -2,9 +2,7 @@ "domain": "zigbee", "name": "Zigbee", "documentation": "https://www.home-assistant.io/integrations/zigbee", - "requirements": [ - "xbee-helper==0.0.7" - ], + "requirements": ["xbee-helper==0.0.7"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/zigbee/sensor.py b/homeassistant/components/zigbee/sensor.py index 3909aad4093..648b3bd5a2f 100644 --- a/homeassistant/components/zigbee/sensor.py +++ b/homeassistant/components/zigbee/sensor.py @@ -27,9 +27,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the ZigBee platform. + """Set up the Zigbee platform. - Uses the 'type' config value to work out which type of ZigBee sensor we're + Uses the 'type' config value to work out which type of Zigbee sensor we're dealing with and instantiates the relevant classes to handle it. """ typ = config.get(CONF_TYPE) @@ -37,7 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: sensor_class, config_class = TYPE_CLASSES[typ] except KeyError: - _LOGGER.exception("Unknown ZigBee sensor type: %s", typ) + _LOGGER.exception("Unknown Zigbee sensor type: %s", typ) return add_entities([sensor_class(hass, config_class(config))], True) @@ -73,11 +73,11 @@ class ZigBeeTemperatureSensor(Entity): except zigbee.ZIGBEE_TX_FAILURE: _LOGGER.warning( "Transmission failure when attempting to get sample from " - "ZigBee device at address: %s", + "Zigbee device at address: %s", hexlify(self._config.address), ) except zigbee.ZIGBEE_EXCEPTION as exc: - _LOGGER.exception("Unable to get sample from ZigBee device: %s", exc) + _LOGGER.exception("Unable to get sample from Zigbee device: %s", exc) # This must be below the classes to which it refers. diff --git a/homeassistant/components/zigbee/switch.py b/homeassistant/components/zigbee/switch.py index 16af6ec7e3a..4e8d21f438a 100644 --- a/homeassistant/components/zigbee/switch.py +++ b/homeassistant/components/zigbee/switch.py @@ -5,7 +5,6 @@ from homeassistant.components.switch import SwitchDevice from . import PLATFORM_SCHEMA, ZigBeeDigitalOut, ZigBeeDigitalOutConfig - CONF_ON_STATE = "on_state" DEFAULT_ON_STATE = "high" diff --git a/homeassistant/components/ziggo_mediabox_xl/manifest.json b/homeassistant/components/ziggo_mediabox_xl/manifest.json index ff9e64ae78e..84a384e5168 100644 --- a/homeassistant/components/ziggo_mediabox_xl/manifest.json +++ b/homeassistant/components/ziggo_mediabox_xl/manifest.json @@ -1,10 +1,8 @@ { "domain": "ziggo_mediabox_xl", - "name": "Ziggo mediabox xl", + "name": "Ziggo Mediabox XL", "documentation": "https://www.home-assistant.io/integrations/ziggo_mediabox_xl", - "requirements": [ - "ziggo-mediabox-xl==1.1.0" - ], + "requirements": ["ziggo-mediabox-xl==1.1.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 6ae62be3eb9..e88993beee8 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -4,29 +4,28 @@ from typing import Set, cast import voluptuous as vol -from homeassistant.core import callback, State -from homeassistant.loader import bind_hass -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( - CONF_NAME, + ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_ICON, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ICON, + CONF_NAME, CONF_RADIUS, EVENT_CORE_CONFIG_UPDATE, ) +from homeassistant.core import State, callback from homeassistant.helpers import config_per_platform +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.loader import bind_hass from homeassistant.util import slugify -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE from homeassistant.util.location import distance - from .config_flow import configured_zones -from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE, ATTR_PASSIVE, ATTR_RADIUS +from .const import ATTR_PASSIVE, ATTR_RADIUS, CONF_PASSIVE, DOMAIN, HOME_ZONE from .zone import Zone - # mypy: allow-untyped-calls, allow-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -97,7 +96,7 @@ def async_active_zone(hass, latitude, longitude, radius=0): async def async_setup(hass, config): - """Set up configured zones as well as home assistant zone if necessary.""" + """Set up configured zones as well as Home Assistant zone if necessary.""" hass.data[DOMAIN] = {} entities: Set[str] = set() zone_entries = configured_zones(hass) diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index 39633754772..4531ff7b834 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -4,22 +4,21 @@ from typing import Set import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import ( - CONF_NAME, + CONF_ICON, CONF_LATITUDE, CONF_LONGITUDE, - CONF_ICON, + CONF_NAME, CONF_RADIUS, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE - # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/components/zone/manifest.json b/homeassistant/components/zone/manifest.json index 7a8cdcf6c6c..8efed9ba7a6 100644 --- a/homeassistant/components/zone/manifest.json +++ b/homeassistant/components/zone/manifest.json @@ -5,7 +5,6 @@ "documentation": "https://www.home-assistant.io/integrations/zone", "requirements": [], "dependencies": [], - "codeowners": [ - "@home-assistant/core" - ] + "codeowners": ["@home-assistant/core"], + "quality_scale": "internal" } diff --git a/homeassistant/components/zoneminder/manifest.json b/homeassistant/components/zoneminder/manifest.json index 5e834e2fa4f..2f6fe831eb3 100644 --- a/homeassistant/components/zoneminder/manifest.json +++ b/homeassistant/components/zoneminder/manifest.json @@ -1,12 +1,8 @@ { "domain": "zoneminder", - "name": "Zoneminder", + "name": "ZoneMinder", "documentation": "https://www.home-assistant.io/integrations/zoneminder", - "requirements": [ - "zm-py==0.4.0" - ], + "requirements": ["zm-py==0.4.0"], "dependencies": [], - "codeowners": [ - "@rohankapoorcom" - ] + "codeowners": ["@rohankapoorcom"] } diff --git a/homeassistant/components/zwave/.translations/da.json b/homeassistant/components/zwave/.translations/da.json index e9049026a4f..25eee9b3d91 100644 --- a/homeassistant/components/zwave/.translations/da.json +++ b/homeassistant/components/zwave/.translations/da.json @@ -2,16 +2,16 @@ "config": { "abort": { "already_configured": "Z-Wave er allerede konfigureret", - "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n Z-Wave forekomst" + "one_instance_only": "Komponenten underst\u00f8tter kun \u00e9n Z-Wave-instans" }, "error": { - "option_error": "Z-Wave validering mislykkedes. Er stien til USB enhed korrekt?" + "option_error": "Z-Wave-validering mislykkedes. Er stien til USB-enhed korrekt?" }, "step": { "user": { "data": { "network_key": "Netv\u00e6rksn\u00f8gle (efterlad blank for autogenerering)", - "usb_path": "Sti til USB enhed" + "usb_path": "Sti til USB-enhed" }, "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler", "title": "Ops\u00e6t Z-Wave" diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index 293cc45273f..9b9236de1c2 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -8,62 +8,60 @@ from pprint import pprint import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import callback, CoreState +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + EVENT_HOMEASSISTANT_STOP, +) +from homeassistant.core import CoreState, callback from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from homeassistant.helpers.device_registry import ( - async_get_registry as async_get_device_registry, -) -from homeassistant.const import ( - ATTR_ENTITY_ID, - EVENT_HOMEASSISTANT_START, - EVENT_HOMEASSISTANT_STOP, -) from homeassistant.helpers.entity_values import EntityValues from homeassistant.helpers.event import async_track_time_change from homeassistant.util import convert import homeassistant.util.dt as dt_util -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from . import const from . import config_flow # noqa: F401 pylint: disable=unused-import -from . import websocket_api as wsapi +from . import const, websocket_api as wsapi, workaround from .const import ( CONF_AUTOHEAL, + CONF_CONFIG_PATH, CONF_DEBUG, + CONF_NETWORK_KEY, CONF_POLLING_INTERVAL, CONF_USB_STICK_PATH, - CONF_CONFIG_PATH, - CONF_NETWORK_KEY, + DATA_DEVICES, + DATA_ENTITY_VALUES, + DATA_NETWORK, + DATA_ZWAVE_CONFIG, DEFAULT_CONF_AUTOHEAL, DEFAULT_CONF_USB_STICK_PATH, - DEFAULT_POLLING_INTERVAL, DEFAULT_DEBUG, + DEFAULT_POLLING_INTERVAL, DOMAIN, - DATA_DEVICES, - DATA_NETWORK, - DATA_ENTITY_VALUES, - DATA_ZWAVE_CONFIG, ) -from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity -from . import workaround from .discovery_schemas import DISCOVERY_SCHEMAS +from .node_entity import ZWaveBaseEntity, ZWaveNodeEntity from .util import ( + check_has_unique_id, check_node_schema, check_value_schema, - node_name, - check_has_unique_id, is_node_parsed, node_device_id_and_name, + node_name, ) _LOGGER = logging.getLogger(__name__) @@ -272,7 +270,7 @@ def nice_print_node(node): value_id: _obj_to_dict(value) for value_id, value in node.values.items() } - _LOGGER.info("FOUND NODE %s \n" "%s", node.product_name, node_dict) + _LOGGER.info("FOUND NODE %s \n%s", node.product_name, node_dict) def get_config_value(node, value_index, tries=5): @@ -437,7 +435,6 @@ async def async_setup_entry(hass, config_entry): platform=None, scan_interval=DEFAULT_SCAN_INTERVAL, entity_namespace=None, - async_entities_added_callback=lambda: None, ) platform.config_entry = config_entry @@ -473,7 +470,7 @@ async def async_setup_entry(hass, config_entry): @callback def _on_timeout(sec): _LOGGER.warning( - "Z-Wave node %d not ready after %d seconds, " "continuing anyway", + "Z-Wave node %d not ready after %d seconds, continuing anyway", entity.node_id, sec, ) @@ -523,7 +520,7 @@ async def async_setup_entry(hass, config_entry): def network_complete(): """Handle the querying of all nodes on network.""" _LOGGER.info( - "Z-Wave network is complete. All nodes on the network " "have been queried" + "Z-Wave network is complete. All nodes on the network have been queried" ) hass.bus.fire(const.EVENT_NETWORK_COMPLETE) @@ -681,7 +678,7 @@ async def async_setup_entry(hass, config_entry): if value.type == const.TYPE_BOOL: value.data = int(selection == "True") _LOGGER.info( - "Setting config parameter %s on Node %s " "with bool selection %s", + "Setting config parameter %s on Node %s with bool selection %s", param, node_id, str(selection), @@ -690,7 +687,7 @@ async def async_setup_entry(hass, config_entry): if value.type == const.TYPE_LIST: value.data = str(selection) _LOGGER.info( - "Setting config parameter %s on Node %s " "with list selection %s", + "Setting config parameter %s on Node %s with list selection %s", param, node_id, str(selection), @@ -709,7 +706,7 @@ async def async_setup_entry(hass, config_entry): return value.data = int(selection) _LOGGER.info( - "Setting config parameter %s on Node %s " "with selection %s", + "Setting config parameter %s on Node %s with selection %s", param, node_id, selection, @@ -717,7 +714,7 @@ async def async_setup_entry(hass, config_entry): return node.set_config_param(param, selection, size) _LOGGER.info( - "Setting unknown config parameter %s on Node %s " "with selection %s", + "Setting unknown config parameter %s on Node %s with selection %s", param, node_id, selection, @@ -828,7 +825,7 @@ async def async_setup_entry(hass, config_entry): ) return _LOGGER.info( - "Node %s on instance %s does not have resettable " "meters.", + "Node %s on instance %s does not have resettable meters.", node_id, instance, ) diff --git a/homeassistant/components/zwave/binary_sensor.py b/homeassistant/components/zwave/binary_sensor.py index a28f53f93d4..68df3313de3 100644 --- a/homeassistant/components/zwave/binary_sensor.py +++ b/homeassistant/components/zwave/binary_sensor.py @@ -1,12 +1,14 @@ """Support for Z-Wave binary sensors.""" -import logging import datetime -import homeassistant.util.dt as dt_util +import logging + +from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import track_point_in_time -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice -from . import workaround, ZWaveDeviceEntity +import homeassistant.util.dt as dt_util + +from . import ZWaveDeviceEntity, workaround from .const import COMMAND_CLASS_SENSOR_BINARY _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index a264cdea7da..b570e31c128 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -7,8 +7,8 @@ import voluptuous as vol from homeassistant import config_entries from .const import ( - CONF_USB_STICK_PATH, CONF_NETWORK_KEY, + CONF_USB_STICK_PATH, DEFAULT_CONF_USB_STICK_PATH, DOMAIN, ) diff --git a/homeassistant/components/zwave/cover.py b/homeassistant/components/zwave/cover.py index 3a389fbf2bb..95cc994e4ff 100644 --- a/homeassistant/components/zwave/cover.py +++ b/homeassistant/components/zwave/cover.py @@ -1,24 +1,26 @@ """Support for Z-Wave covers.""" import logging -from homeassistant.core import callback + from homeassistant.components.cover import ( - DOMAIN, - SUPPORT_OPEN, - SUPPORT_CLOSE, ATTR_POSITION, + DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, ) -from homeassistant.components.cover import CoverDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ( - ZWaveDeviceEntity, CONF_INVERT_OPENCLOSE_BUTTONS, CONF_INVERT_PERCENT, + ZWaveDeviceEntity, workaround, ) from .const import ( - COMMAND_CLASS_SWITCH_MULTILEVEL, - COMMAND_CLASS_SWITCH_BINARY, COMMAND_CLASS_BARRIER_OPERATOR, + COMMAND_CLASS_SWITCH_BINARY, + COMMAND_CLASS_SWITCH_MULTILEVEL, DATA_NETWORK, ) diff --git a/homeassistant/components/zwave/fan.py b/homeassistant/components/zwave/fan.py index a52a7613d72..b77ab8dcf68 100644 --- a/homeassistant/components/zwave/fan.py +++ b/homeassistant/components/zwave/fan.py @@ -2,17 +2,18 @@ import logging import math -from homeassistant.core import callback from homeassistant.components.fan import ( DOMAIN, - FanEntity, - SPEED_OFF, + SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, - SPEED_HIGH, + SPEED_OFF, SUPPORT_SET_SPEED, + FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/light.py b/homeassistant/components/zwave/light.py index bdb9021c02c..e941b2a97dc 100644 --- a/homeassistant/components/zwave/light.py +++ b/homeassistant/components/zwave/light.py @@ -1,26 +1,27 @@ """Support for Z-Wave lights.""" import logging - from threading import Timer -from homeassistant.core import callback + from homeassistant.components.light import ( - ATTR_WHITE_VALUE, ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_WHITE_VALUE, + DOMAIN, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, - DOMAIN, Light, ) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util -from . import CONF_REFRESH_VALUE, CONF_REFRESH_DELAY, const, ZWaveDeviceEntity + +from . import CONF_REFRESH_DELAY, CONF_REFRESH_VALUE, ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index ae5640e812d..f84b1b5cfd4 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -3,10 +3,11 @@ import logging import voluptuous as vol -from homeassistant.core import callback from homeassistant.components.lock import DOMAIN, LockDevice -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) @@ -269,7 +270,7 @@ class ZwaveLock(ZWaveDeviceEntity, LockDevice): workaround = DEVICE_MAPPINGS[specific_sensor_key] if workaround & WORKAROUND_V2BTZE: self._v2btze = 1 - _LOGGER.debug("Polycontrol Danalock v2 BTZE " "workaround enabled") + _LOGGER.debug("Polycontrol Danalock v2 BTZE workaround enabled") if workaround & WORKAROUND_DEVICE_STATE: self._state_workaround = True _LOGGER.debug("Notification device state workaround enabled") @@ -298,7 +299,7 @@ class ZwaveLock(ZWaveDeviceEntity, LockDevice): ): self._state = LOCK_STATUS.get(str(notification_data)) _LOGGER.debug( - "Lock state set from Access Control value and is %s, " "get=%s", + "Lock state set from Access Control value and is %s, get=%s", str(notification_data), self.state, ) diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 44241e91daf..3b94991312a 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -1,26 +1,26 @@ """Entity class that represents Z-Wave node.""" -import logging from itertools import count +import logging +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, ATTR_WAKEUP from homeassistant.core import callback -from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_WAKEUP, ATTR_ENTITY_ID -from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import async_get_registry from .const import ( - ATTR_NODE_ID, - COMMAND_CLASS_WAKE_UP, - ATTR_SCENE_ID, - ATTR_SCENE_DATA, ATTR_BASIC_LEVEL, - EVENT_NODE_EVENT, - EVENT_SCENE_ACTIVATED, + ATTR_NODE_ID, + ATTR_SCENE_DATA, + ATTR_SCENE_ID, COMMAND_CLASS_CENTRAL_SCENE, COMMAND_CLASS_VERSION, + COMMAND_CLASS_WAKE_UP, DOMAIN, + EVENT_NODE_EVENT, + EVENT_SCENE_ACTIVATED, ) -from .util import node_name, is_node_parsed, node_device_id_and_name +from .util import is_node_parsed, node_device_id_and_name, node_name _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/sensor.py b/homeassistant/components/zwave/sensor.py index 0820feb8d0f..08ee54415ad 100644 --- a/homeassistant/components/zwave/sensor.py +++ b/homeassistant/components/zwave/sensor.py @@ -1,10 +1,12 @@ """Support for Z-Wave sensors.""" import logging -from homeassistant.core import callback -from homeassistant.components.sensor import DOMAIN, DEVICE_CLASS_BATTERY + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY, DOMAIN from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import const, ZWaveDeviceEntity + +from . import ZWaveDeviceEntity, const _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 52b135eba81..9d3d2b0cadf 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -141,7 +141,7 @@ start_network: description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. stop_network: - description: Stop the Z-Wave network, all updates into HASS will stop. + description: Stop the Z-Wave network, all updates into Home Assistant will stop. soft_reset: description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to your controller's manual. diff --git a/homeassistant/components/zwave/switch.py b/homeassistant/components/zwave/switch.py index 4b13b06d2b9..3592f534074 100644 --- a/homeassistant/components/zwave/switch.py +++ b/homeassistant/components/zwave/switch.py @@ -1,9 +1,11 @@ """Support for Z-Wave switches.""" import logging import time -from homeassistant.core import callback + from homeassistant.components.switch import DOMAIN, SwitchDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from . import ZWaveDeviceEntity, workaround _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/config.py b/homeassistant/config.py index 71628be8006..6777c1ef5a5 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -1,66 +1,65 @@ """Module to help with parsing and generating configuration files.""" -from collections import OrderedDict - # pylint: disable=no-name-in-module +from collections import OrderedDict from distutils.version import LooseVersion # pylint: disable=import-error import logging import os import re import shutil -from typing import Any, Tuple, Optional, Dict, Union, Callable, Sequence, Set from types import ModuleType +from typing import Any, Callable, Dict, Optional, Sequence, Set, Tuple, Union + import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant import auth from homeassistant.auth import ( - providers as auth_providers, mfa_modules as auth_mfa_modules, + providers as auth_providers, ) from homeassistant.const import ( + ATTR_ASSUMED_STATE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, - ATTR_ASSUMED_STATE, + CONF_AUTH_MFA_MODULES, + CONF_AUTH_PROVIDERS, + CONF_CUSTOMIZE, + CONF_CUSTOMIZE_DOMAIN, + CONF_CUSTOMIZE_GLOB, + CONF_ELEVATION, + CONF_ID, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_PACKAGES, - CONF_UNIT_SYSTEM, - CONF_TIME_ZONE, - CONF_ELEVATION, - CONF_UNIT_SYSTEM_IMPERIAL, CONF_TEMPERATURE_UNIT, + CONF_TIME_ZONE, + CONF_TYPE, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_WHITELIST_EXTERNAL_DIRS, TEMP_CELSIUS, __version__, - CONF_CUSTOMIZE, - CONF_CUSTOMIZE_DOMAIN, - CONF_CUSTOMIZE_GLOB, - CONF_WHITELIST_EXTERNAL_DIRS, - CONF_AUTH_PROVIDERS, - CONF_AUTH_MFA_MODULES, - CONF_TYPE, - CONF_ID, ) from homeassistant.core import DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_per_platform, extract_domain_configs +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_values import EntityValues from homeassistant.loader import Integration, IntegrationNotFound from homeassistant.requirements import ( - async_get_integration_with_requirements, RequirementsNotFound, + async_get_integration_with_requirements, ) -from homeassistant.util.yaml import load_yaml, SECRET_YAML from homeassistant.util.package import is_docker_env -import homeassistant.helpers.config_validation as cv from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from homeassistant.helpers.entity_values import EntityValues -from homeassistant.helpers import config_per_platform, extract_domain_configs +from homeassistant.util.yaml import SECRET_YAML, load_yaml _LOGGER = logging.getLogger(__name__) DATA_PERSISTENT_ERRORS = "bootstrap_persistent_errors" RE_YAML_ERROR = re.compile(r"homeassistant\.util\.yaml") RE_ASCII = re.compile(r"\033\[[^m]*m") -HA_COMPONENT_URL = "[{}](https://home-assistant.io/integrations/{}/)" YAML_CONFIG_FILE = "configuration.yaml" VERSION_FILE = ".HA_VERSION" CONFIG_DIR_NAME = ".homeassistant" @@ -210,7 +209,7 @@ CORE_CONFIG_SCHEMA = CUSTOMIZE_CONFIG_SCHEMA.extend( { CONF_TYPE: vol.NotIn( ["insecure_example"], - "The insecure_example mfa module" " is for testing only.", + "The insecure_example mfa module is for testing only.", ) } ) @@ -412,19 +411,25 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: @callback def async_log_exception( - ex: Exception, domain: str, config: Dict, hass: HomeAssistant + ex: Exception, + domain: str, + config: Dict, + hass: HomeAssistant, + link: Optional[str] = None, ) -> None: """Log an error for configuration validation. This method must be run in the event loop. """ if hass is not None: - async_notify_setup_error(hass, domain, True) - _LOGGER.error(_format_config_error(ex, domain, config)) + async_notify_setup_error(hass, domain, link) + _LOGGER.error(_format_config_error(ex, domain, config, link)) @callback -def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: +def _format_config_error( + ex: Exception, domain: str, config: Dict, link: Optional[str] = None +) -> str: """Generate log exception for configuration validation. This method must be run in the event loop. @@ -455,12 +460,8 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: getattr(domain_config, "__line__", "?"), ) - if domain != CONF_CORE: - integration = domain.split(".")[-1] - message += ( - "Please check the docs at " - f"https://home-assistant.io/integrations/{integration}/" - ) + if domain != CONF_CORE and link: + message += f"Please check the docs at {link}" return message @@ -571,9 +572,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> Non def _log_pkg_error(package: str, component: str, config: Dict, message: str) -> None: """Log an error while merging packages.""" - message = "Package {} setup failed. Integration {} {}".format( - package, component, message - ) + message = f"Package {package} setup failed. Integration {component} {message}" pack_config = config[CONF_CORE][CONF_PACKAGES].get(package, config) message += " (See {}:{}). ".format( @@ -717,7 +716,7 @@ async def async_process_component_config( hass, config ) except (vol.Invalid, HomeAssistantError) as ex: - async_log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass, integration.documentation) return None # No custom config validator, proceed with schema validation @@ -725,7 +724,7 @@ async def async_process_component_config( try: return component.CONFIG_SCHEMA(config) # type: ignore except vol.Invalid as ex: - async_log_exception(ex, domain, config, hass) + async_log_exception(ex, domain, config, hass, integration.documentation) return None component_platform_schema = getattr( @@ -741,7 +740,7 @@ async def async_process_component_config( try: p_validated = component_platform_schema(p_config) except vol.Invalid as ex: - async_log_exception(ex, domain, p_config, hass) + async_log_exception(ex, domain, p_config, hass, integration.documentation) continue # Not all platform components follow same pattern for platforms @@ -770,7 +769,13 @@ async def async_process_component_config( p_config ) except vol.Invalid as ex: - async_log_exception(ex, f"{domain}.{p_name}", p_config, hass) + async_log_exception( + ex, + f"{domain}.{p_name}", + p_config, + hass, + p_integration.documentation, + ) continue platforms.append(p_validated) @@ -806,7 +811,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> Optional[str]: @callback def async_notify_setup_error( - hass: HomeAssistant, component: str, display_link: bool = False + hass: HomeAssistant, component: str, display_link: Optional[str] = None ) -> None: """Print a persistent notification. @@ -821,11 +826,11 @@ def async_notify_setup_error( errors[component] = errors.get(component) or display_link - message = "The following components and platforms could not be set up:\n\n" + message = "The following integrations and platforms could not be set up:\n\n" for name, link in errors.items(): if link: - part = HA_COMPONENT_URL.format(name.replace("_", "-"), name) + part = f"[{name}]({link})" else: part = name diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index ae3aeebb1ee..6fb5595dac4 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1,21 +1,20 @@ """Manage config entries in Home Assistant.""" import asyncio -import logging import functools +import logging +from typing import Any, Callable, Dict, List, Optional, Set, Union, cast import uuid -from typing import Any, Callable, Dict, List, Optional, Set, cast import weakref import attr from homeassistant import data_entry_flow, loader -from homeassistant.core import callback, HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady -from homeassistant.setup import async_setup_component, async_process_deps_reqs -from homeassistant.util.decorator import Registry +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from homeassistant.helpers import entity_registry from homeassistant.helpers.event import Event - +from homeassistant.setup import async_process_deps_reqs, async_setup_component +from homeassistant.util.decorator import Registry _LOGGER = logging.getLogger(__name__) _UNDEF: dict = {} @@ -26,6 +25,16 @@ SOURCE_SSDP = "ssdp" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" +# If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow +# websocket command creates a config entry with this source and while it exists normal discoveries +# with the same unique id are ignored. +SOURCE_IGNORE = "ignore" + +# This is used when a user uses the "Stop Ignoring" button in the UI (the +# config_entries/ignore_flow websocket command). It's triggered after the "ignore" config entry has +# been removed and unloaded. +SOURCE_UNIGNORE = "unignore" + HANDLERS = Registry() STORAGE_KEY = "core.config_entries" @@ -52,7 +61,13 @@ ENTRY_STATE_FAILED_UNLOAD = "failed_unload" UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD) DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" -DISCOVERY_SOURCES = (SOURCE_SSDP, SOURCE_ZEROCONF, SOURCE_DISCOVERY, SOURCE_IMPORT) +DISCOVERY_SOURCES = ( + SOURCE_SSDP, + SOURCE_ZEROCONF, + SOURCE_DISCOVERY, + SOURCE_IMPORT, + SOURCE_UNIGNORE, +) EVENT_FLOW_DISCOVERED = "config_entry_discovered" @@ -86,6 +101,7 @@ class ConfigEntry: "title", "data", "options", + "unique_id", "system_options", "source", "connection_class", @@ -105,6 +121,7 @@ class ConfigEntry: connection_class: str, system_options: dict, options: Optional[dict] = None, + unique_id: Optional[str] = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED, ) -> None: @@ -139,6 +156,9 @@ class ConfigEntry: # State of the entry (LOADED, NOT_LOADED) self.state = state + # Unique ID of this entry. + self.unique_id = unique_id + # Listeners to call on update self.update_listeners: List = [] @@ -153,6 +173,9 @@ class ConfigEntry: tries: int = 0, ) -> None: """Set up an entry.""" + if self.source == SOURCE_IGNORE: + return + if integration is None: integration = await loader.async_get_integration(hass, self.domain) @@ -238,6 +261,10 @@ class ConfigEntry: Returns if unload is possible and was successful. """ + if self.source == SOURCE_IGNORE: + self.state = ENTRY_STATE_NOT_LOADED + return True + if integration is None: integration = await loader.async_get_integration(hass, self.domain) @@ -284,6 +311,9 @@ class ConfigEntry: async def async_remove(self, hass: HomeAssistant) -> None: """Invoke remove callback on component.""" + if self.source == SOURCE_IGNORE: + return + integration = await loader.async_get_integration(hass, self.domain) component = integration.get_component() if not hasattr(component, "async_remove_entry"): @@ -371,9 +401,145 @@ class ConfigEntry: "system_options": self.system_options.as_dict(), "source": self.source, "connection_class": self.connection_class, + "unique_id": self.unique_id, } +class ConfigEntriesFlowManager(data_entry_flow.FlowManager): + """Manage all the config entry flows that are in progress.""" + + def __init__( + self, hass: HomeAssistant, config_entries: "ConfigEntries", hass_config: dict + ): + """Initialize the config entry flow manager.""" + super().__init__(hass) + self.config_entries = config_entries + self._hass_config = hass_config + + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any] + ) -> Dict[str, Any]: + """Finish a config flow and add an entry.""" + flow = cast(ConfigFlow, flow) + + # Remove notification if no other discovery config entries in progress + if not any( + ent["context"]["source"] in DISCOVERY_SOURCES + for ent in self.hass.config_entries.flow.async_progress() + if ent["flow_id"] != flow.flow_id + ): + self.hass.components.persistent_notification.async_dismiss( + DISCOVERY_NOTIFICATION_ID + ) + + if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + return result + + # Check if config entry exists with unique ID. Unload it. + existing_entry = None + + if flow.unique_id is not None: + # Abort all flows in progress with same unique ID. + for progress_flow in self.async_progress(): + if ( + progress_flow["handler"] == flow.handler + and progress_flow["flow_id"] != flow.flow_id + and progress_flow["context"].get("unique_id") == flow.unique_id + ): + self.async_abort(progress_flow["flow_id"]) + + # Find existing entry. + for check_entry in self.config_entries.async_entries(result["handler"]): + if check_entry.unique_id == flow.unique_id: + existing_entry = check_entry + break + + # Unload the entry before setting up the new one. + # We will remove it only after the other one is set up, + # so that device customizations are not getting lost. + if ( + existing_entry is not None + and existing_entry.state not in UNRECOVERABLE_STATES + ): + await self.config_entries.async_unload(existing_entry.entry_id) + + entry = ConfigEntry( + version=result["version"], + domain=result["handler"], + title=result["title"], + data=result["data"], + options={}, + system_options={}, + source=flow.context["source"], + connection_class=flow.CONNECTION_CLASS, + unique_id=flow.unique_id, + ) + + await self.config_entries.async_add(entry) + + if existing_entry is not None: + await self.config_entries.async_remove(existing_entry.entry_id) + + result["result"] = entry + return result + + async def async_create_flow( + self, handler_key: Any, *, context: Optional[Dict] = None, data: Any = None + ) -> "ConfigFlow": + """Create a flow for specified handler. + + Handler key is the domain of the component that we want to set up. + """ + try: + integration = await loader.async_get_integration(self.hass, handler_key) + except loader.IntegrationNotFound: + _LOGGER.error("Cannot find integration %s", handler_key) + raise data_entry_flow.UnknownHandler + + # Make sure requirements and dependencies of component are resolved + await async_process_deps_reqs(self.hass, self._hass_config, integration) + + try: + integration.get_platform("config_flow") + except ImportError as err: + _LOGGER.error( + "Error occurred loading config flow for integration %s: %s", + handler_key, + err, + ) + raise data_entry_flow.UnknownHandler + + handler = HANDLERS.get(handler_key) + + if handler is None: + raise data_entry_flow.UnknownHandler + + if not context or "source" not in context: + raise KeyError("Context not set or doesn't have a source set") + + flow = cast(ConfigFlow, handler()) + flow.init_step = context["source"] + return flow + + async def async_post_init( + self, flow: data_entry_flow.FlowHandler, result: dict + ) -> None: + """After a flow is initialised trigger new flow notifications.""" + source = flow.context["source"] + + # Create notification. + if source in DISCOVERY_SOURCES: + self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) + self.hass.components.persistent_notification.async_create( + title="New devices discovered", + message=( + "We have discovered new devices on your network. " + "[Check it out](/config/integrations)" + ), + notification_id=DISCOVERY_NOTIFICATION_ID, + ) + + class ConfigEntries: """Manage the configuration entries. @@ -383,9 +549,7 @@ class ConfigEntries: def __init__(self, hass: HomeAssistant, hass_config: dict) -> None: """Initialize the entry manager.""" self.hass = hass - self.flow = data_entry_flow.FlowManager( - hass, self._async_create_flow, self._async_finish_flow - ) + self.flow = ConfigEntriesFlowManager(hass, self, hass_config) self.options = OptionsFlowManager(hass) self._hass_config = hass_config self._entries: List[ConfigEntry] = [] @@ -420,6 +584,12 @@ class ConfigEntries: return list(self._entries) return [entry for entry in self._entries if entry.domain == domain] + async def async_add(self, entry: ConfigEntry) -> None: + """Add and setup an entry.""" + self._entries.append(entry) + await self.async_setup(entry.entry_id) + self._async_schedule_save() + async def async_remove(self, entry_id: str) -> Dict[str, Any]: """Remove an entry.""" entry = self.async_get_entry(entry_id) @@ -445,6 +615,19 @@ class ConfigEntries: dev_reg.async_clear_config_entry(entry_id) ent_reg.async_clear_config_entry(entry_id) + # After we have fully removed an "ignore" config entry we can try and rediscover it so that a + # user is able to immediately start configuring it. We do this by starting a new flow with + # the 'unignore' step. If the integration doesn't implement async_step_unignore then + # this will be a no-op. + if entry.source == SOURCE_IGNORE: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + entry.domain, + context={"source": SOURCE_UNIGNORE}, + data={"unique_id": entry.unique_id}, + ) + ) + return {"require_restart": not unload_success} async def async_initialize(self) -> None: @@ -474,6 +657,8 @@ class ConfigEntries: options=entry.get("options"), # New in 0.98 system_options=entry.get("system_options", {}), + # New in 0.104 + unique_id=entry.get("unique_id"), ) for entry in config["entries"] ] @@ -534,11 +719,15 @@ class ConfigEntries: self, entry: ConfigEntry, *, + unique_id: Union[str, dict, None] = _UNDEF, data: dict = _UNDEF, options: dict = _UNDEF, system_options: dict = _UNDEF, ) -> None: """Update a config entry.""" + if unique_id is not _UNDEF: + entry.unique_id = cast(Optional[str], unique_id) + if data is not _UNDEF: entry.data = data @@ -586,90 +775,6 @@ class ConfigEntries: return await entry.async_unload(self.hass, integration=integration) - async def _async_finish_flow( - self, flow: "ConfigFlow", result: Dict[str, Any] - ) -> Dict[str, Any]: - """Finish a config flow and add an entry.""" - # Remove notification if no other discovery config entries in progress - if not any( - ent["context"]["source"] in DISCOVERY_SOURCES - for ent in self.hass.config_entries.flow.async_progress() - if ent["flow_id"] != flow.flow_id - ): - self.hass.components.persistent_notification.async_dismiss( - DISCOVERY_NOTIFICATION_ID - ) - - if result["type"] != data_entry_flow.RESULT_TYPE_CREATE_ENTRY: - return result - - entry = ConfigEntry( - version=result["version"], - domain=result["handler"], - title=result["title"], - data=result["data"], - options={}, - system_options={}, - source=flow.context["source"], - connection_class=flow.CONNECTION_CLASS, - ) - self._entries.append(entry) - self._async_schedule_save() - - await self.async_setup(entry.entry_id) - - result["result"] = entry - return result - - async def _async_create_flow( - self, handler_key: str, *, context: Dict[str, Any], data: Dict[str, Any] - ) -> "ConfigFlow": - """Create a flow for specified handler. - - Handler key is the domain of the component that we want to set up. - """ - try: - integration = await loader.async_get_integration(self.hass, handler_key) - except loader.IntegrationNotFound: - _LOGGER.error("Cannot find integration %s", handler_key) - raise data_entry_flow.UnknownHandler - - # Make sure requirements and dependencies of component are resolved - await async_process_deps_reqs(self.hass, self._hass_config, integration) - - try: - integration.get_platform("config_flow") - except ImportError as err: - _LOGGER.error( - "Error occurred loading config flow for integration %s: %s", - handler_key, - err, - ) - raise data_entry_flow.UnknownHandler - - handler = HANDLERS.get(handler_key) - - if handler is None: - raise data_entry_flow.UnknownHandler - - source = context["source"] - - # Create notification. - if source in DISCOVERY_SOURCES: - self.hass.bus.async_fire(EVENT_FLOW_DISCOVERED) - self.hass.components.persistent_notification.async_create( - title="New devices discovered", - message=( - "We have discovered new devices on your network. " - "[Check it out](/config/integrations)" - ), - notification_id=DISCOVERY_NOTIFICATION_ID, - ) - - flow = cast(ConfigFlow, handler()) - flow.init_step = source - return flow - def _async_schedule_save(self) -> None: """Save the entity registry to a file.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @@ -696,18 +801,67 @@ class ConfigFlow(data_entry_flow.FlowHandler): CONNECTION_CLASS = CONN_CLASS_UNKNOWN + @property + def unique_id(self) -> Optional[str]: + """Return unique ID if available.""" + # pylint: disable=no-member + if not self.context: + return None + + return cast(Optional[str], self.context.get("unique_id")) + @staticmethod @callback def async_get_options_flow(config_entry: ConfigEntry) -> "OptionsFlow": """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler + @callback + def _abort_if_unique_id_configured(self) -> None: + """Abort if the unique ID is already configured.""" + if self.unique_id is None: + return + + if self.unique_id in self._async_current_ids(): + raise data_entry_flow.AbortFlow("already_configured") + + async def async_set_unique_id( + self, unique_id: str, *, raise_on_progress: bool = True + ) -> Optional[ConfigEntry]: + """Set a unique ID for the config flow. + + Returns optionally existing config entry with same ID. + """ + if raise_on_progress: + for progress in self._async_in_progress(): + if progress["context"].get("unique_id") == unique_id: + raise data_entry_flow.AbortFlow("already_in_progress") + + # pylint: disable=no-member + self.context["unique_id"] = unique_id + + for entry in self._async_current_entries(): + if entry.unique_id == unique_id: + return entry + + return None + @callback def _async_current_entries(self) -> List[ConfigEntry]: """Return current entries.""" assert self.hass is not None return self.hass.config_entries.async_entries(self.handler) + @callback + def _async_current_ids(self, include_ignore: bool = True) -> Set[Optional[str]]: + """Return current unique IDs.""" + assert self.hass is not None + return set( + entry.unique_id + for entry in self.hass.config_entries.async_entries(self.handler) + if include_ignore or entry.source != SOURCE_IGNORE + ) + @callback def _async_in_progress(self) -> List[Dict]: """Return other in progress flows for current domain.""" @@ -718,27 +872,33 @@ class ConfigFlow(data_entry_flow.FlowHandler): if flw["handler"] == self.handler and flw["flow_id"] != self.flow_id ] + async def async_step_ignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: + """Ignore this config flow.""" + await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) + return self.async_create_entry(title="Ignored", data={}) -class OptionsFlowManager: + async def async_step_unignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: + """Rediscover a config entry by it's unique_id.""" + return self.async_abort(reason="not_implemented") + + +class OptionsFlowManager(data_entry_flow.FlowManager): """Flow to set options for a configuration entry.""" - def __init__(self, hass: HomeAssistant) -> None: - """Initialize the options manager.""" - self.hass = hass - self.flow = data_entry_flow.FlowManager( - hass, self._async_create_flow, self._async_finish_flow - ) - - async def _async_create_flow( - self, entry_id: str, *, context: Dict[str, Any], data: Dict[str, Any] - ) -> Optional["OptionsFlow"]: + async def async_create_flow( + self, + handler_key: Any, + *, + context: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + ) -> "OptionsFlow": """Create an options flow for a config entry. Entry_id and flow.handler is the same thing to map entry with flow. """ - entry = self.hass.config_entries.async_get_entry(entry_id) + entry = self.hass.config_entries.async_get_entry(handler_key) if entry is None: - return None + raise UnknownEntry(handler_key) if entry.domain not in HANDLERS: raise data_entry_flow.UnknownHandler @@ -746,16 +906,18 @@ class OptionsFlowManager: flow = cast(OptionsFlow, HANDLERS[entry.domain].async_get_options_flow(entry)) return flow - async def _async_finish_flow( - self, flow: "OptionsFlow", result: Dict[str, Any] - ) -> Optional[Dict[str, Any]]: + async def async_finish_flow( + self, flow: data_entry_flow.FlowHandler, result: Dict[str, Any] + ) -> Dict[str, Any]: """Finish an options flow and update options for configuration entry. Flow.handler and entry_id is the same thing to map flow with entry. """ + flow = cast(OptionsFlow, flow) + entry = self.hass.config_entries.async_get_entry(flow.handler) if entry is None: - return None + raise UnknownEntry(flow.handler) self.hass.config_entries.async_update_entry(entry, options=result["data"]) result["result"] = True diff --git a/homeassistant/const.py b/homeassistant/const.py index 8c002853436..9095cdc5ced 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,10 +1,13 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 103 -PATCH_VERSION = "6" -__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) -__version__ = "{}.{}".format(__short_version__, PATCH_VERSION) -REQUIRED_PYTHON_VER = (3, 6, 1) +MINOR_VERSION = 104 +PATCH_VERSION = "0" +__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" +__version__ = f"{__short_version__}.{PATCH_VERSION}" +REQUIRED_PYTHON_VER = (3, 7, 0) +# Truthy date string triggers showing related deprecation warning messages. +REQUIRED_NEXT_PYTHON_VER = (3, 8, 0) +REQUIRED_NEXT_PYTHON_DATE = "" # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" @@ -315,6 +318,7 @@ ATTR_GPS_ACCURACY = "gps_accuracy" ATTR_ASSUMED_STATE = "assumed_state" ATTR_STATE = "state" +ATTR_EDITABLE = "editable" ATTR_OPTION = "option" # Bitfield of supported component features for the entity diff --git a/homeassistant/core.py b/homeassistant/core.py index 2859e0fe157..3f561cdfab8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -14,65 +14,59 @@ import os import pathlib import threading from time import monotonic -import uuid - from types import MappingProxyType from typing import ( - Optional, - Any, - Callable, - List, - TypeVar, - Dict, - Coroutine, - Set, TYPE_CHECKING, + Any, Awaitable, + Callable, + Coroutine, + Dict, + List, Mapping, + Optional, + Set, + TypeVar, ) +import uuid from async_timeout import timeout import attr import voluptuous as vol +from homeassistant import loader, util from homeassistant.const import ( ATTR_DOMAIN, ATTR_FRIENDLY_NAME, ATTR_NOW, + ATTR_SECONDS, ATTR_SERVICE, ATTR_SERVICE_DATA, - ATTR_SECONDS, CONF_UNIT_SYSTEM_IMPERIAL, EVENT_CALL_SERVICE, EVENT_CORE_CONFIG_UPDATE, + EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_CLOSE, - EVENT_SERVICE_REMOVED, EVENT_SERVICE_REGISTERED, + EVENT_SERVICE_REMOVED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, EVENT_TIMER_OUT_OF_SYNC, MATCH_ALL, __version__, ) -from homeassistant import loader from homeassistant.exceptions import ( HomeAssistantError, InvalidEntityFormatError, InvalidStateError, - Unauthorized, ServiceNotFound, + Unauthorized, ) -from homeassistant.util.async_ import run_callback_threadsafe, fire_coroutine_threadsafe -from homeassistant import util -import homeassistant.util.dt as dt_util from homeassistant.util import location, slugify -from homeassistant.util.unit_system import ( - UnitSystem, - IMPERIAL_SYSTEM, - METRIC_SYSTEM, -) +from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe +import homeassistant.util.dt as dt_util +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem # Typing imports that create a circular dependency if TYPE_CHECKING: @@ -198,7 +192,7 @@ class HomeAssistant: return self.state in (CoreState.starting, CoreState.running) def start(self) -> int: - """Start home assistant. + """Start Home Assistant. Note: This function is only used for testing. For regular use, use "await hass.run()". @@ -223,7 +217,7 @@ class HomeAssistant: This method is a coroutine. """ if self.state != CoreState.not_running: - raise RuntimeError("HASS is already running") + raise RuntimeError("Home Assistant is already running") # _async_stop will set this instead of stopping the loop self._stopped = asyncio.Event() @@ -718,18 +712,14 @@ class State: if not valid_entity_id(entity_id) and not temp_invalid_id_bypass: raise InvalidEntityFormatError( - ( - "Invalid entity id encountered: {}. " - "Format should be ." - ).format(entity_id) + f"Invalid entity id encountered: {entity_id}. " + "Format should be ." ) if not valid_state(state): raise InvalidStateError( - ( - "Invalid state encountered for entity id: {}. " - "State max length is 255 characters." - ).format(entity_id) + f"Invalid state encountered for entity id: {entity_id}. " + "State max length is 255 characters." ) self.entity_id = entity_id.lower() @@ -1040,9 +1030,7 @@ class ServiceCall: self.domain, self.service, self.context.id, util.repr_helper(self.data) ) - return "".format( - self.domain, self.service, self.context.id - ) + return f"" class ServiceRegistry: @@ -1140,6 +1128,9 @@ class ServiceRegistry: self._services[domain].pop(service) + if not self._services[domain]: + self._services.pop(domain) + self._hass.bus.async_fire( EVENT_SERVICE_REMOVED, {ATTR_DOMAIN: domain, ATTR_SERVICE: service} ) diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 58d8e4ea131..4dd1c7acf50 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -1,9 +1,12 @@ """Classes to help gather user submissions.""" +import abc import logging -from typing import Dict, Any, Callable, List, Optional +from typing import Any, Dict, List, Optional, cast import uuid + import voluptuous as vol -from .core import callback, HomeAssistant + +from .core import HomeAssistant, callback from .exceptions import HomeAssistantError _LOGGER = logging.getLogger(__name__) @@ -34,20 +37,50 @@ class UnknownStep(FlowError): """Unknown step specified.""" -class FlowManager: +class AbortFlow(FlowError): + """Exception to indicate a flow needs to be aborted.""" + + def __init__(self, reason: str, description_placeholders: Optional[Dict] = None): + """Initialize an abort flow exception.""" + super().__init__(f"Flow aborted: {reason}") + self.reason = reason + self.description_placeholders = description_placeholders + + +class FlowManager(abc.ABC): """Manage all the flows that are in progress.""" - def __init__( - self, - hass: HomeAssistant, - async_create_flow: Callable, - async_finish_flow: Callable, - ) -> None: + def __init__(self, hass: HomeAssistant,) -> None: """Initialize the flow manager.""" self.hass = hass self._progress: Dict[str, Any] = {} - self._async_create_flow = async_create_flow - self._async_finish_flow = async_finish_flow + + @abc.abstractmethod + async def async_create_flow( + self, + handler_key: Any, + *, + context: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + ) -> "FlowHandler": + """Create a flow for specified handler. + + Handler key is the domain of the component that we want to set up. + """ + pass + + @abc.abstractmethod + async def async_finish_flow( + self, flow: "FlowHandler", result: Dict[str, Any] + ) -> Dict[str, Any]: + """Finish a config flow and add an entry.""" + pass + + async def async_post_init( + self, flow: "FlowHandler", result: Dict[str, Any] + ) -> None: + """Entry has finished executing its first step asynchronously.""" + pass @callback def async_progress(self) -> List[Dict]: @@ -55,6 +88,7 @@ class FlowManager: return [ {"flow_id": flow.flow_id, "handler": flow.handler, "context": flow.context} for flow in self._progress.values() + if flow.cur_step is not None ] async def async_init( @@ -63,14 +97,21 @@ class FlowManager: """Start a configuration flow.""" if context is None: context = {} - flow = await self._async_create_flow(handler, context=context, data=data) + flow = await self.async_create_flow(handler, context=context, data=data) + if not flow: + raise UnknownFlow("Flow was not created") flow.hass = self.hass flow.handler = handler flow.flow_id = uuid.uuid4().hex flow.context = context self._progress[flow.flow_id] = flow - return await self._async_handle_step(flow, flow.init_step, data) + result = await self._async_handle_step(flow, flow.init_step, data) + + if result["type"] != RESULT_TYPE_ABORT: + await self.async_post_init(flow, result) + + return result async def async_configure( self, flow_id: str, user_input: Optional[Dict] = None @@ -124,12 +165,15 @@ class FlowManager: if not hasattr(flow, method): self._progress.pop(flow.flow_id) raise UnknownStep( - "Handler {} doesn't support step {}".format( - flow.__class__.__name__, step_id - ) + f"Handler {flow.__class__.__name__} doesn't support step {step_id}" ) - result: Dict = await getattr(flow, method)(user_input) + try: + result: Dict = await getattr(flow, method)(user_input) + except AbortFlow as err: + result = _create_abort_data( + flow.flow_id, flow.handler, err.reason, err.description_placeholders + ) if result["type"] not in ( RESULT_TYPE_FORM, @@ -151,7 +195,7 @@ class FlowManager: return result # We pass a copy of the result because we're mutating our version - result = await self._async_finish_flow(flow, dict(result)) + result = await self.async_finish_flow(flow, dict(result)) # _async_finish_flow may change result type, check it again if result["type"] == RESULT_TYPE_FORM: @@ -226,13 +270,9 @@ class FlowHandler: self, *, reason: str, description_placeholders: Optional[Dict] = None ) -> Dict[str, Any]: """Abort the config flow.""" - return { - "type": RESULT_TYPE_ABORT, - "flow_id": self.flow_id, - "handler": self.handler, - "reason": reason, - "description_placeholders": description_placeholders, - } + return _create_abort_data( + self.flow_id, cast(str, self.handler), reason, description_placeholders + ) @callback def async_external_step( @@ -257,3 +297,20 @@ class FlowHandler: "handler": self.handler, "step_id": next_step_id, } + + +@callback +def _create_abort_data( + flow_id: str, + handler: str, + reason: str, + description_placeholders: Optional[Dict] = None, +) -> Dict[str, Any]: + """Return the definition of an external step for the user to take.""" + return { + "type": RESULT_TYPE_ABORT, + "flow_id": flow_id, + "handler": handler, + "reason": reason, + "description_placeholders": description_placeholders, + } diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index 6147e26c809..745d80d386b 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -1,5 +1,6 @@ """The exceptions used by Home Assistant.""" -from typing import Optional, Tuple, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Tuple + import jinja2 if TYPE_CHECKING: diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8d4be47f5f8..dcae6fd065e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -13,6 +13,7 @@ FLOWS = [ "ambiclimate", "ambient_station", "axis", + "brother", "cast", "cert_expiry", "coolmaster", @@ -20,11 +21,13 @@ FLOWS = [ "deconz", "dialogflow", "ecobee", + "elgato", "emulated_roku", "esphome", "geofency", "geonetnz_quakes", "geonetnz_volcano", + "gios", "glances", "gpslogger", "hangouts", @@ -35,6 +38,7 @@ FLOWS = [ "huawei_lte", "hue", "iaqualink", + "icloud", "ifttt", "ios", "ipma", @@ -43,6 +47,7 @@ FLOWS = [ "life360", "lifx", "linky", + "local_ip", "locative", "logi_circle", "luftdaten", @@ -61,6 +66,8 @@ FLOWS = [ "point", "ps4", "rainmachine", + "ring", + "sentry", "simplisafe", "smartthings", "smhi", @@ -71,6 +78,7 @@ FLOWS = [ "sonos", "starline", "tellduslive", + "tesla", "toon", "tplink", "traccar", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 108fe38e647..306b3850a1b 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -12,6 +12,9 @@ ZEROCONF = { "_coap._udp.local.": [ "tradfri" ], + "_elg._tcp.local.": [ + "elgato" + ], "_esphomelib._tcp.local.": [ "esphome" ], diff --git a/homeassistant/helpers/__init__.py b/homeassistant/helpers/__init__.py index fe60ffc4b33..ad97456968b 100644 --- a/homeassistant/helpers/__init__.py +++ b/homeassistant/helpers/__init__.py @@ -1,6 +1,6 @@ """Helper methods for components within Home Assistant.""" import re -from typing import Any, Iterable, Tuple, Sequence, Dict +from typing import Any, Dict, Iterable, Sequence, Tuple from homeassistant.const import CONF_PLATFORM @@ -36,5 +36,5 @@ def extract_domain_configs(config: ConfigType, domain: str) -> Sequence[str]: Async friendly. """ - pattern = re.compile(r"^{}(| .+)$".format(domain)) + pattern = re.compile(fr"^{domain}(| .+)$") return [key for key in config.keys() if pattern.match(key)] diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index 7f1579cd2c6..eee891b7f88 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -1,18 +1,17 @@ """Helper for aiohttp webclient stuff.""" import asyncio -import sys from ssl import SSLContext -from typing import Any, Awaitable, Optional, cast -from typing import Union +import sys +from typing import Any, Awaitable, Optional, Union, cast import aiohttp -from aiohttp.hdrs import USER_AGENT, CONTENT_TYPE from aiohttp import web -from aiohttp.web_exceptions import HTTPGatewayTimeout, HTTPBadGateway +from aiohttp.hdrs import CONTENT_TYPE, USER_AGENT +from aiohttp.web_exceptions import HTTPBadGateway, HTTPGatewayTimeout import async_timeout -from homeassistant.core import callback, Event from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, __version__ +from homeassistant.core import Event, callback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass from homeassistant.util import ssl as ssl_util diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index e75b195d386..58abecffb8b 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -1,10 +1,9 @@ """Provide a way to connect devices to one physical location.""" -import logging -import uuid from asyncio import Event from collections import OrderedDict -from typing import MutableMapping -from typing import Iterable, Optional, cast +import logging +from typing import Iterable, MutableMapping, Optional, cast +import uuid import attr diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 4052a94b9de..6ac1326545a 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -1,35 +1,38 @@ """Helper to check the configuration file.""" -from collections import OrderedDict, namedtuple -from typing import List +from collections import OrderedDict +from typing import List, NamedTuple, Optional import attr import voluptuous as vol from homeassistant import loader -from homeassistant.core import HomeAssistant from homeassistant.config import ( CONF_CORE, - CORE_CONFIG_SCHEMA, CONF_PACKAGES, - merge_packages_config, + CORE_CONFIG_SCHEMA, _format_config_error, + config_per_platform, + extract_domain_configs, find_config_file, load_yaml_config_file, - extract_domain_configs, - config_per_platform, + merge_packages_config, ) -from homeassistant.requirements import ( - async_get_integration_with_requirements, - RequirementsNotFound, -) - -import homeassistant.util.yaml.loader as yaml_loader +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType +from homeassistant.requirements import ( + RequirementsNotFound, + async_get_integration_with_requirements, +) +import homeassistant.util.yaml.loader as yaml_loader -# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any +class CheckConfigError(NamedTuple): + """Configuration check error.""" -CheckConfigError = namedtuple("CheckConfigError", "message domain config") + message: str + domain: Optional[str] + config: Optional[ConfigType] @attr.s @@ -38,7 +41,12 @@ class HomeAssistantConfig(OrderedDict): errors: List[CheckConfigError] = attr.ib(default=attr.Factory(list)) - def add_error(self, message, domain=None, config=None): + def add_error( + self, + message: str, + domain: Optional[str] = None, + config: Optional[ConfigType] = None, + ) -> "HomeAssistantConfig": """Add a single error.""" self.errors.append(CheckConfigError(str(message), domain, config)) return self @@ -57,16 +65,16 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig config_dir = hass.config.config_dir result = HomeAssistantConfig() - def _pack_error(package, component, config, message): + def _pack_error( + package: str, component: str, config: ConfigType, message: str + ) -> None: """Handle errors from packages: _log_pkg_error.""" - message = "Package {} setup failed. Component {} {}".format( - package, component, message - ) + message = f"Package {package} setup failed. Component {component} {message}" domain = f"homeassistant.packages.{package}.{component}" pack_config = core_config[CONF_PACKAGES].get(package, config) result.add_error(message, domain, pack_config) - def _comp_error(ex, domain, config): + def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None: """Handle errors from components: async_log_exception.""" result.add_error(_format_config_error(ex, domain, config), domain, config) diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py new file mode 100644 index 00000000000..dd0edbd09b9 --- /dev/null +++ b/homeassistant/helpers/collection.py @@ -0,0 +1,412 @@ +"""Helper to deal with YAML + storage.""" +from abc import ABC, abstractmethod +import logging +from typing import Any, Awaitable, Callable, Dict, List, Optional, cast + +import voluptuous as vol +from voluptuous.humanize import humanize_error + +from homeassistant.components import websocket_api +from homeassistant.const import CONF_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.storage import Store +from homeassistant.util import slugify + +STORAGE_VERSION = 1 +SAVE_DELAY = 10 + +CHANGE_ADDED = "added" +CHANGE_UPDATED = "updated" +CHANGE_REMOVED = "removed" + + +ChangeListener = Callable[ + [ + # Change type + str, + # Item ID + str, + # New config (None if removed) + Optional[dict], + ], + Awaitable[None], +] # pylint: disable=invalid-name + + +class CollectionError(HomeAssistantError): + """Base class for collection related errors.""" + + +class ItemNotFound(CollectionError): + """Raised when an item is not found.""" + + def __init__(self, item_id: str): + """Initialize item not found error.""" + super().__init__(f"Item {item_id} not found.") + self.item_id = item_id + + +class IDManager: + """Keep track of IDs across different collections.""" + + def __init__(self) -> None: + """Initiate the ID manager.""" + self.collections: List[Dict[str, Any]] = [] + + def add_collection(self, collection: Dict[str, Any]) -> None: + """Add a collection to check for ID usage.""" + self.collections.append(collection) + + def has_id(self, item_id: str) -> bool: + """Test if the ID exists.""" + return any(item_id in collection for collection in self.collections) + + def generate_id(self, suggestion: str) -> str: + """Generate an ID.""" + base = slugify(suggestion) + proposal = base + attempt = 1 + + while self.has_id(proposal): + attempt += 1 + proposal = f"{base}_{attempt}" + + return proposal + + +class ObservableCollection(ABC): + """Base collection type that can be observed.""" + + def __init__(self, logger: logging.Logger, id_manager: Optional[IDManager] = None): + """Initialize the base collection.""" + self.logger = logger + self.id_manager = id_manager or IDManager() + self.data: Dict[str, dict] = {} + self.listeners: List[ChangeListener] = [] + + self.id_manager.add_collection(self.data) + + @callback + def async_items(self) -> List[dict]: + """Return list of items in collection.""" + return list(self.data.values()) + + @callback + def async_add_listener(self, listener: ChangeListener) -> None: + """Add a listener. + + Will be called with (change_type, item_id, updated_config). + """ + self.listeners.append(listener) + + async def notify_change( + self, change_type: str, item_id: str, item: Optional[dict] + ) -> None: + """Notify listeners of a change.""" + self.logger.debug("%s %s: %s", change_type, item_id, item) + for listener in self.listeners: + await listener(change_type, item_id, item) + + +class YamlCollection(ObservableCollection): + """Offer a fake CRUD interface on top of static YAML.""" + + async def async_load(self, data: List[dict]) -> None: + """Load the YAML collection. Overrides existing data.""" + old_ids = set(self.data) + + for item in data: + item_id = item[CONF_ID] + + if item_id in old_ids: + old_ids.remove(item_id) + event = CHANGE_UPDATED + elif self.id_manager.has_id(item_id): + self.logger.warning("Duplicate ID '%s' detected, skipping", item_id) + continue + else: + event = CHANGE_ADDED + + self.data[item_id] = item + await self.notify_change(event, item[CONF_ID], item) + + for item_id in old_ids: + self.data.pop(item_id) + await self.notify_change(CHANGE_REMOVED, item_id, None) + + +class StorageCollection(ObservableCollection): + """Offer a CRUD interface on top of JSON storage.""" + + def __init__( + self, + store: Store, + logger: logging.Logger, + id_manager: Optional[IDManager] = None, + ): + """Initialize the storage collection.""" + super().__init__(logger, id_manager) + self.store = store + + @property + def hass(self) -> HomeAssistant: + """Home Assistant object.""" + return self.store.hass + + async def async_load(self) -> None: + """Load the storage Manager.""" + raw_storage = cast(Optional[dict], await self.store.async_load()) + + if raw_storage is None: + raw_storage = {"items": []} + + for item in raw_storage["items"]: + self.data[item[CONF_ID]] = item + await self.notify_change(CHANGE_ADDED, item[CONF_ID], item) + + @abstractmethod + async def _process_create_data(self, data: dict) -> dict: + """Validate the config is valid.""" + + @callback + @abstractmethod + def _get_suggested_id(self, info: dict) -> str: + """Suggest an ID based on the config.""" + + @abstractmethod + async def _update_data(self, data: dict, update_data: dict) -> dict: + """Return a new updated data object.""" + + async def async_create_item(self, data: dict) -> dict: + """Create a new item.""" + item = await self._process_create_data(data) + item[CONF_ID] = self.id_manager.generate_id(self._get_suggested_id(item)) + self.data[item[CONF_ID]] = item + self._async_schedule_save() + await self.notify_change(CHANGE_ADDED, item[CONF_ID], item) + return item + + async def async_update_item(self, item_id: str, updates: dict) -> dict: + """Update item.""" + if item_id not in self.data: + raise ItemNotFound(item_id) + + if CONF_ID in updates: + raise ValueError("Cannot update ID") + + current = self.data[item_id] + + updated = await self._update_data(current, updates) + + self.data[item_id] = updated + self._async_schedule_save() + + await self.notify_change(CHANGE_UPDATED, item_id, updated) + + return self.data[item_id] + + async def async_delete_item(self, item_id: str) -> None: + """Delete item.""" + if item_id not in self.data: + raise ItemNotFound(item_id) + + self.data.pop(item_id) + self._async_schedule_save() + + await self.notify_change(CHANGE_REMOVED, item_id, None) + + @callback + def _async_schedule_save(self) -> None: + """Schedule saving the area registry.""" + self.store.async_delay_save(self._data_to_save, SAVE_DELAY) + + @callback + def _data_to_save(self) -> dict: + """Return data of area registry to store in a file.""" + return {"items": list(self.data.values())} + + +@callback +def attach_entity_component_collection( + entity_component: EntityComponent, + collection: ObservableCollection, + create_entity: Callable[[dict], Entity], +) -> None: + """Map a collection to an entity component.""" + entities = {} + + async def _collection_changed( + change_type: str, item_id: str, config: Optional[dict] + ) -> None: + """Handle a collection change.""" + if change_type == CHANGE_ADDED: + entity = create_entity(cast(dict, config)) + await entity_component.async_add_entities([entity]) + entities[item_id] = entity + return + + if change_type == CHANGE_REMOVED: + entity = entities.pop(item_id) + await entity.async_remove() + return + + # CHANGE_UPDATED + await entities[item_id].async_update_config(config) # type: ignore + + collection.async_add_listener(_collection_changed) + + +class StorageCollectionWebsocket: + """Class to expose storage collection management over websocket.""" + + def __init__( + self, + storage_collection: StorageCollection, + api_prefix: str, + model_name: str, + create_schema: dict, + update_schema: dict, + ): + """Initialize a websocket CRUD.""" + self.storage_collection = storage_collection + self.api_prefix = api_prefix + self.model_name = model_name + self.create_schema = create_schema + self.update_schema = update_schema + + assert self.api_prefix[-1] != "/", "API prefix should not end in /" + + @property + def item_id_key(self) -> str: + """Return item ID key.""" + return f"{self.model_name}_id" + + @callback + def async_setup(self, hass: HomeAssistant, *, create_list: bool = True) -> None: + """Set up the websocket commands.""" + if create_list: + websocket_api.async_register_command( + hass, + f"{self.api_prefix}/list", + self.ws_list_item, + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + {vol.Required("type"): f"{self.api_prefix}/list"} + ), + ) + + websocket_api.async_register_command( + hass, + f"{self.api_prefix}/create", + websocket_api.require_admin( + websocket_api.async_response(self.ws_create_item) + ), + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + **self.create_schema, + vol.Required("type"): f"{self.api_prefix}/create", + } + ), + ) + + websocket_api.async_register_command( + hass, + f"{self.api_prefix}/update", + websocket_api.require_admin( + websocket_api.async_response(self.ws_update_item) + ), + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + **self.update_schema, + vol.Required("type"): f"{self.api_prefix}/update", + vol.Required(self.item_id_key): str, + } + ), + ) + + websocket_api.async_register_command( + hass, + f"{self.api_prefix}/delete", + websocket_api.require_admin( + websocket_api.async_response(self.ws_delete_item) + ), + websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( + { + vol.Required("type"): f"{self.api_prefix}/delete", + vol.Required(self.item_id_key): str, + } + ), + ) + + def ws_list_item( + self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict + ) -> None: + """List items.""" + connection.send_result(msg["id"], self.storage_collection.async_items()) + + async def ws_create_item( + self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict + ) -> None: + """Create a item.""" + try: + data = dict(msg) + data.pop("id") + data.pop("type") + item = await self.storage_collection.async_create_item(data) + connection.send_result(msg["id"], item) + except vol.Invalid as err: + connection.send_error( + msg["id"], + websocket_api.const.ERR_INVALID_FORMAT, + humanize_error(data, err), + ) + except ValueError as err: + connection.send_error( + msg["id"], websocket_api.const.ERR_INVALID_FORMAT, str(err) + ) + + async def ws_update_item( + self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict + ) -> None: + """Update a item.""" + data = dict(msg) + msg_id = data.pop("id") + item_id = data.pop(self.item_id_key) + data.pop("type") + + try: + item = await self.storage_collection.async_update_item(item_id, data) + connection.send_result(msg_id, item) + except ItemNotFound: + connection.send_error( + msg["id"], + websocket_api.const.ERR_NOT_FOUND, + f"Unable to find {self.item_id_key} {item_id}", + ) + except vol.Invalid as err: + connection.send_error( + msg["id"], + websocket_api.const.ERR_INVALID_FORMAT, + humanize_error(data, err), + ) + except ValueError as err: + connection.send_error( + msg_id, websocket_api.const.ERR_INVALID_FORMAT, str(err) + ) + + async def ws_delete_item( + self, hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict + ) -> None: + """Delete a item.""" + try: + await self.storage_collection.async_delete_item(msg[self.item_id_key]) + except ItemNotFound: + connection.send_error( + msg["id"], + websocket_api.const.ERR_NOT_FOUND, + f"Unable to find {self.item_id_key} {msg[self.item_id_key]}", + ) + + connection.send_result(msg["id"]) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index df82ba6076f..02853f7615b 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -6,9 +6,6 @@ import logging import sys from typing import Callable, Container, Optional, Union, cast -from homeassistant.helpers.template import Template -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 ( async_get_device_automation_platform, @@ -34,11 +31,14 @@ from homeassistant.const import ( SUN_EVENT_SUNSET, WEEKDAYS, ) -from homeassistant.exceptions import TemplateError, HomeAssistantError +from homeassistant.core import HomeAssistant, State +from homeassistant.exceptions import HomeAssistantError, TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.sun import get_astral_event_date -import homeassistant.util.dt as dt_util +from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.util.async_ import run_callback_threadsafe +import homeassistant.util.dt as dt_util FROM_CONFIG_FORMAT = "{}_from_config" ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config" @@ -192,7 +192,7 @@ def async_numeric_state( fvalue = float(value) except ValueError: _LOGGER.warning( - "Value cannot be processed as a number: %s " "(Offending entity: %s)", + "Value cannot be processed as a number: %s (Offending entity: %s)", entity, value, ) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 41f90effb89..323c6907411 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,6 +1,8 @@ """Helpers for data entry flows for config entries.""" -from typing import Callable, Awaitable, Union +from typing import Awaitable, Callable, Union + from homeassistant import config_entries + from .typing import HomeAssistantType # mypy: allow-untyped-defs, no-check-untyped-defs diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index dc3d3c91f27..d29dae735f8 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -5,26 +5,25 @@ This module exists of the following parts: - OAuth2 implementation that works with local provided client ID/secret """ +from abc import ABC, ABCMeta, abstractmethod import asyncio -from abc import ABCMeta, ABC, abstractmethod import logging -from typing import Optional, Any, Dict, cast, Awaitable, Callable +import secrets import time +from typing import Any, Awaitable, Callable, Dict, Optional, cast +from aiohttp import client, web import async_timeout -from aiohttp import web, client import jwt import voluptuous as vol from yarl import URL -from homeassistant.auth.util import generate_secret -from homeassistant.core import HomeAssistant, callback from homeassistant import config_entries from homeassistant.components.http import HomeAssistantView +from homeassistant.core import HomeAssistant, callback from .aiohttp_client import async_get_clientsession - DATA_JWT_SECRET = "oauth2_jwt_secret" DATA_VIEW_REGISTERED = "oauth2_view_reg" DATA_IMPLEMENTATIONS = "oauth2_impl" @@ -260,10 +259,21 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """ return self.async_create_entry(title=self.flow_impl.name, data=data) + async def async_step_discovery(self, user_input: dict = None) -> dict: + """Handle a flow initialized by discovery.""" + await self.async_set_unique_id(self.DOMAIN) + self._abort_if_unique_id_configured() + + assert self.hass is not None + if self.hass.config_entries.async_entries(self.DOMAIN): + return self.async_abort(reason="already_configured") + + return await self.async_step_pick_implementation() + async_step_user = async_step_pick_implementation - async_step_ssdp = async_step_pick_implementation - async_step_zeroconf = async_step_pick_implementation - async_step_homekit = async_step_pick_implementation + async_step_ssdp = async_step_discovery + async_step_zeroconf = async_step_discovery + async_step_homekit = async_step_discovery @classmethod def async_register_implementation( @@ -442,7 +452,7 @@ def _encode_jwt(hass: HomeAssistant, data: dict) -> str: secret = hass.data.get(DATA_JWT_SECRET) if secret is None: - secret = hass.data[DATA_JWT_SECRET] = generate_secret() + secret = hass.data[DATA_JWT_SECRET] = secrets.token_hex() return jwt.encode(data, secret, algorithm="HS256").decode() diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 948fb017d9d..e357a2ba622 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1,17 +1,30 @@ """Helpers for config validation using voluptuous.""" -import inspect -import logging -import os -import re from datetime import ( - timedelta, + date as date_sys, datetime as datetime_sys, time as time_sys, - date as date_sys, + timedelta, ) -from socket import _GLOBAL_DEFAULT_TIMEOUT +from enum import Enum +import inspect +import logging from numbers import Number -from typing import Any, Union, TypeVar, Callable, List, Dict, Optional +import os +import re +from socket import _GLOBAL_DEFAULT_TIMEOUT # type: ignore # private, not in typeshed +from typing import ( + Any, + Callable, + Dict, + Hashable, + List, + Optional, + Pattern, + Type, + TypeVar, + Union, + cast, +) from urllib.parse import urlparse from uuid import UUID @@ -19,8 +32,9 @@ from pkg_resources import parse_version import voluptuous as vol import voluptuous_serialize -import homeassistant.util.dt as dt_util from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_ENTITY_ID, CONF_ABOVE, CONF_ALIAS, CONF_BELOW, @@ -33,10 +47,10 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_STATE, + CONF_TIMEOUT, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, - CONF_TIMEOUT, ENTITY_MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, @@ -44,17 +58,14 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, WEEKDAYS, __version__, - ATTR_AREA_ID, - ATTR_ENTITY_ID, ) -from homeassistant.core import valid_entity_id, split_entity_id +from homeassistant.core import split_entity_id, valid_entity_id from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template as template_helper from homeassistant.helpers.logging import KeywordStyleAdapter from homeassistant.util import slugify as util_slugify +import homeassistant.util.dt as dt_util - -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any # pylint: disable=invalid-name TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'" @@ -124,32 +135,30 @@ def boolean(value: Any) -> bool: elif isinstance(value, Number): # type ignore: https://github.com/python/mypy/issues/3186 return value != 0 # type: ignore - raise vol.Invalid("invalid boolean value {}".format(value)) + raise vol.Invalid(f"invalid boolean value {value}") -def isdevice(value): +def isdevice(value: Any) -> str: """Validate that value is a real device.""" try: os.stat(value) return str(value) except OSError: - raise vol.Invalid("No device at {} found".format(value)) + raise vol.Invalid(f"No device at {value} found") -def matches_regex(regex): +def matches_regex(regex: str) -> Callable[[Any], str]: """Validate that the value is a string that matches a regex.""" - regex = re.compile(regex) + compiled = re.compile(regex) def validator(value: Any) -> str: """Validate that value matches the given regex.""" if not isinstance(value, str): - raise vol.Invalid("not a string value: {}".format(value)) + raise vol.Invalid(f"not a string value: {value}") - if not regex.match(value): + if not compiled.match(value): raise vol.Invalid( - "value {} does not match regular expression {}".format( - value, regex.pattern - ) + f"value {value} does not match regular expression {compiled.pattern}" ) return value @@ -157,17 +166,17 @@ def matches_regex(regex): return validator -def is_regex(value): +def is_regex(value: Any) -> Pattern[Any]: """Validate that a string is a valid regular expression.""" try: r = re.compile(value) return r except TypeError: raise vol.Invalid( - "value {} is of the wrong type for a regular " "expression".format(value) + f"value {value} is of the wrong type for a regular expression" ) except re.error: - raise vol.Invalid("value {} is not a valid regular expression".format(value)) + raise vol.Invalid(f"value {value} is not a valid regular expression") def isfile(value: Any) -> str: @@ -205,11 +214,11 @@ def ensure_list(value: Union[T, List[T], None]) -> List[T]: def entity_id(value: Any) -> str: """Validate Entity ID.""" - value = string(value).lower() - if valid_entity_id(value): - return value + str_value = string(value).lower() + if valid_entity_id(str_value): + return str_value - raise vol.Invalid("Entity ID {} is an invalid entity id".format(value)) + raise vol.Invalid(f"Entity ID {value} is an invalid entity id") def entity_ids(value: Union[str, List]) -> List[str]: @@ -245,26 +254,24 @@ def entities_domain(domain: str) -> Callable[[Union[str, List]], List[str]]: for ent_id in values: if split_entity_id(ent_id)[0] != domain: raise vol.Invalid( - "Entity ID '{}' does not belong to domain '{}'".format( - ent_id, domain - ) + f"Entity ID '{ent_id}' does not belong to domain '{domain}'" ) return values return validate -def enum(enumClass): +def enum(enumClass: Type[Enum]) -> vol.All: """Create validator for specified enum.""" return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__) -def icon(value): +def icon(value: Any) -> str: """Validate icon.""" - value = str(value) + str_value = str(value) - if ":" in value: - return value + if ":" in str_value: + return str_value raise vol.Invalid('Icons should be specified in the form "prefix:name"') @@ -296,7 +303,7 @@ def time(value: Any) -> time_sys: raise vol.Invalid("Not a parseable type") if time_val is None: - raise vol.Invalid("Invalid time specified: {}".format(value)) + raise vol.Invalid(f"Invalid time specified: {value}") return time_val @@ -357,13 +364,13 @@ def time_period_seconds(value: Union[int, str]) -> timedelta: try: return timedelta(seconds=int(value)) except (ValueError, TypeError): - raise vol.Invalid("Expected seconds, got {}".format(value)) + raise vol.Invalid(f"Expected seconds, got {value}") time_period = vol.Any(time_period_str, time_period_seconds, timedelta, time_period_dict) -def match_all(value): +def match_all(value: T) -> T: """Validate that matches all values.""" return value @@ -383,13 +390,13 @@ def remove_falsy(value: List[T]) -> List[T]: return [v for v in value if v] -def service(value): +def service(value: Any) -> str: """Validate service.""" # Services use same format as entities so we can use same helper. - value = string(value).lower() - if valid_entity_id(value): - return value - raise vol.Invalid("Service {} does not match format .".format(value)) + str_value = string(value).lower() + if valid_entity_id(str_value): + return str_value + raise vol.Invalid(f"Service {value} does not match format .") def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: @@ -408,7 +415,7 @@ def schema_with_slug_keys(value_schema: Union[T, Callable]) -> Callable: for key in value.keys(): slug(key) - return schema(value) + return cast(Dict, schema(value)) return verify @@ -417,11 +424,11 @@ def slug(value: Any) -> str: """Validate value is a valid slug.""" if value is None: raise vol.Invalid("Slug should not be None") - value = str(value) - slg = util_slugify(value) - if value == slg: - return value - raise vol.Invalid("invalid slug {} (try {})".format(value, slg)) + str_value = str(value) + slg = util_slugify(str_value) + if str_value == slg: + return str_value + raise vol.Invalid(f"invalid slug {value} (try {slg})") def slugify(value: Any) -> str: @@ -431,7 +438,7 @@ def slugify(value: Any) -> str: slg = util_slugify(str(value)) if slg: return slg - raise vol.Invalid("Unable to slugify {}".format(value)) + raise vol.Invalid(f"Unable to slugify {value}") def string(value: Any) -> str: @@ -459,42 +466,41 @@ unit_system = vol.All( ) -def template(value): +def template(value: Optional[Any]) -> template_helper.Template: """Validate a jinja2 template.""" - from homeassistant.helpers import template as template_helper if value is None: raise vol.Invalid("template value is None") if isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid("template value should be a string") - value = template_helper.Template(str(value)) + template_value = template_helper.Template(str(value)) # type: ignore try: - value.ensure_valid() - return value + template_value.ensure_valid() + return cast(template_helper.Template, template_value) except TemplateError as ex: - raise vol.Invalid("invalid template ({})".format(ex)) + raise vol.Invalid(f"invalid template ({ex})") -def template_complex(value): +def template_complex(value: Any) -> Any: """Validate a complex jinja2 template.""" if isinstance(value, list): - return_value = value.copy() - for idx, element in enumerate(return_value): - return_value[idx] = template_complex(element) - return return_value + return_list = value.copy() + for idx, element in enumerate(return_list): + return_list[idx] = template_complex(element) + return return_list if isinstance(value, dict): - return_value = value.copy() - for key, element in return_value.items(): - return_value[key] = template_complex(element) - return return_value + return_dict = value.copy() + for key, element in return_dict.items(): + return_dict[key] = template_complex(element) + return return_dict if isinstance(value, str): return template(value) return value -def datetime(value): +def datetime(value: Any) -> datetime_sys: """Validate datetime.""" if isinstance(value, datetime_sys): return value @@ -505,12 +511,12 @@ def datetime(value): date_val = None if date_val is None: - raise vol.Invalid("Invalid datetime specified: {}".format(value)) + raise vol.Invalid(f"Invalid datetime specified: {value}") return date_val -def time_zone(value): +def time_zone(value: str) -> str: """Validate timezone.""" if dt_util.get_time_zone(value) is not None: return value @@ -523,7 +529,7 @@ def time_zone(value): weekdays = vol.All(ensure_list, [vol.In(WEEKDAYS)]) -def socket_timeout(value): +def socket_timeout(value: Optional[Any]) -> object: """Validate timeout float > 0.0. None coerced to socket._GLOBAL_DEFAULT_TIMEOUT bare object. @@ -534,9 +540,9 @@ def socket_timeout(value): float_value = float(value) if float_value > 0.0: return float_value - raise vol.Invalid("Invalid socket timeout value." " float > 0.0 required.") - except Exception as _: - raise vol.Invalid("Invalid socket timeout: {err}".format(err=_)) + raise vol.Invalid("Invalid socket timeout value. float > 0.0 required.") + except Exception as err: + raise vol.Invalid(f"Invalid socket timeout: {err}") # pylint: disable=no-value-for-parameter @@ -545,12 +551,12 @@ def url(value: Any) -> str: url_in = str(value) if urlparse(url_in).scheme in ["http", "https"]: - return vol.Schema(vol.Url())(url_in) + return cast(str, vol.Schema(vol.Url())(url_in)) raise vol.Invalid("invalid url") -def x10_address(value): +def x10_address(value: str) -> str: """Validate an x10 address.""" regex = re.compile(r"([A-Pa-p]{1})(?:[2-9]|1[0-6]?)$") if not regex.match(value): @@ -558,7 +564,7 @@ def x10_address(value): return str(value).lower() -def uuid4_hex(value): +def uuid4_hex(value: Any) -> str: """Validate a v4 UUID in hex format.""" try: result = UUID(value, version=4) @@ -679,17 +685,19 @@ def deprecated( # Validator helpers -def key_dependency(key, dependency): +def key_dependency( + key: Hashable, dependency: Hashable +) -> Callable[[Dict[Hashable, Any]], Dict[Hashable, Any]]: """Validate that all dependencies exist for key.""" - def validator(value): + def validator(value: Dict[Hashable, Any]) -> Dict[Hashable, Any]: """Test dependencies.""" if not isinstance(value, dict): raise vol.Invalid("key dependencies require a dict") if key in value and dependency not in value: raise vol.Invalid( - 'dependency violation - key "{}" requires ' - 'key "{}" to exist'.format(key, dependency) + f'dependency violation - key "{key}" requires ' + f'key "{dependency}" to exist' ) return value @@ -697,7 +705,7 @@ def key_dependency(key, dependency): return validator -def custom_serializer(schema): +def custom_serializer(schema: Any) -> Any: """Serialize additional types for voluptuous_serialize.""" if schema is positive_time_period_dict: return {"type": "positive_time_period_dict"} diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 1d471052474..ac5fb608675 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -2,11 +2,10 @@ import voluptuous as vol -from homeassistant import data_entry_flow, config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator - # mypy: allow-untyped-calls, allow-untyped-defs diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 456678edac7..512334c8d3c 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,9 +1,9 @@ """Provide a way to connect entities belonging to one device.""" -import logging -import uuid from asyncio import Event from collections import OrderedDict -from typing import List, Optional, cast +import logging +from typing import Any, Dict, List, Optional, cast +import uuid import attr @@ -12,9 +12,7 @@ from homeassistant.loader import bind_hass from .typing import HomeAssistantType - -# mypy: 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 _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -49,7 +47,7 @@ class DeviceEntry: is_new = attr.ib(type=bool, default=False) -def format_mac(mac): +def format_mac(mac: str) -> str: """Format the mac address string for entry into dev reg.""" to_test = mac @@ -72,10 +70,11 @@ def format_mac(mac): class DeviceRegistry: """Class to hold a registry of devices.""" - def __init__(self, hass): + devices: Dict[str, DeviceEntry] + + def __init__(self, hass: HomeAssistantType) -> None: """Initialize the device registry.""" self.hass = hass - self.devices = None self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback @@ -261,7 +260,7 @@ class DeviceRegistry: return new - def async_remove_device(self, device_id): + def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" del self.devices[device_id] self.hass.bus.async_fire( @@ -299,12 +298,12 @@ class DeviceRegistry: self.devices = devices @callback - def async_schedule_save(self): + def async_schedule_save(self) -> None: """Schedule saving the device registry.""" self._store.async_delay_save(self._data_to_save, SAVE_DELAY) @callback - def _data_to_save(self): + def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: """Return data of device registry to store in a file.""" data = {} @@ -328,7 +327,7 @@ class DeviceRegistry: return data @callback - def async_clear_config_entry(self, config_entry_id): + def async_clear_config_entry(self, config_entry_id: str) -> None: """Clear config entry from registry entries.""" remove = [] for dev_id, device in self.devices.items(): diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index bc1094613bb..806540e57ce 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,14 +5,14 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -from homeassistant import setup, core -from homeassistant.loader import bind_hass +from typing import Callable, Collection, Union + +from homeassistant import core, setup from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import DEPENDENCY_BLACKLIST +from homeassistant.loader import DEPENDENCY_BLACKLIST, bind_hass from homeassistant.util.async_ import run_callback_threadsafe - # mypy: allow-untyped-defs, no-check-untyped-defs EVENT_LOAD_PLATFORM = "load_platform.{}" @@ -20,7 +20,9 @@ ATTR_PLATFORM = "platform" @bind_hass -def listen(hass, service, callback): +def listen( + hass: core.HomeAssistant, service: Union[str, Collection[str]], callback: Callable +) -> None: """Set up listener for discovery of specific service. Service can be a string or a list/tuple. @@ -30,7 +32,9 @@ def listen(hass, service, callback): @core.callback @bind_hass -def async_listen(hass, service, callback): +def async_listen( + hass: core.HomeAssistant, service: Union[str, Collection[str]], callback: Callable +) -> None: """Set up listener for discovery of specific service. Service can be a string or a list/tuple. @@ -41,7 +45,7 @@ def async_listen(hass, service, callback): service = tuple(service) @core.callback - def discovery_event_listener(event): + def discovery_event_listener(event: core.Event) -> None: """Listen for discovery events.""" if ATTR_SERVICE in event.data and event.data[ATTR_SERVICE] in service: hass.async_add_job( @@ -61,7 +65,7 @@ def discover(hass, service, discovered, component, hass_config): async def async_discover(hass, service, discovered, component, hass_config): """Fire discovery event. Can ensure a component is loaded.""" if component in DEPENDENCY_BLACKLIST: - raise HomeAssistantError("Cannot discover the {} component.".format(component)) + raise HomeAssistantError(f"Cannot discover the {component} component.") if component is not None and component not in hass.config.components: await setup.async_setup_component(hass, component, hass_config) @@ -75,7 +79,9 @@ async def async_discover(hass, service, discovered, component, hass_config): @bind_hass -def listen_platform(hass, component, callback): +def listen_platform( + hass: core.HomeAssistant, component: str, callback: Callable +) -> None: """Register a platform loader listener.""" run_callback_threadsafe( hass.loop, async_listen_platform, hass, component, callback @@ -83,7 +89,9 @@ def listen_platform(hass, component, callback): @bind_hass -def async_listen_platform(hass, component, callback): +def async_listen_platform( + hass: core.HomeAssistant, component: str, callback: Callable +) -> None: """Register a platform loader listener. This method must be run in the event loop. @@ -91,7 +99,7 @@ def async_listen_platform(hass, component, callback): service = EVENT_LOAD_PLATFORM.format(component) @core.callback - def discovery_platform_listener(event): + def discovery_platform_listener(event: core.Event) -> None: """Listen for platform discovery events.""" if event.data.get(ATTR_SERVICE) != service: return @@ -143,7 +151,7 @@ async def async_load_platform(hass, component, platform, discovered, hass_config assert hass_config, "You need to pass in the real hass config" if component in DEPENDENCY_BLACKLIST: - raise HomeAssistantError("Cannot discover the {} component.".format(component)) + raise HomeAssistantError(f"Cannot discover the {component} component.") setup_success = True diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index 81582f4fa54..a4e624f119f 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -6,8 +6,8 @@ from homeassistant.core import callback from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception -from .typing import HomeAssistantType +from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) DATA_DISPATCHER = "dispatcher" diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index ed656061401..7ccc6c35613 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -2,16 +2,20 @@ from abc import ABC import asyncio from datetime import datetime, timedelta -import logging import functools as ft +import logging from timeit import default_timer as timer from typing import Any, Dict, Iterable, List, Optional, Union +from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( ATTR_ASSUMED_STATE, + ATTR_DEVICE_CLASS, + ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_HIDDEN, ATTR_ICON, + ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, DEVICE_DEFAULT_NAME, STATE_OFF, @@ -20,22 +24,16 @@ from homeassistant.const import ( STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, - ATTR_ENTITY_PICTURE, - ATTR_SUPPORTED_FEATURES, - ATTR_DEVICE_CLASS, ) +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback +from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( EVENT_ENTITY_REGISTRY_UPDATED, RegistryEntry, ) -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Context -from homeassistant.config import DATA_CUSTOMIZE -from homeassistant.exceptions import NoEntitySpecifiedError -from homeassistant.util import ensure_unique_string, slugify +from homeassistant.util import dt as dt_util, ensure_unique_string, slugify from homeassistant.util.async_ import run_callback_threadsafe -from homeassistant.util import dt as dt_util - # mypy: allow-untyped-defs, no-check-untyped-defs, no-warn-return-any @@ -313,7 +311,9 @@ class Entity(ABC): start = timer() - attr = self.capability_attributes or {} + attr = self.capability_attributes + attr = dict(attr) if attr else {} + if not self.available: state = STATE_UNAVAILABLE else: @@ -473,8 +473,9 @@ class Entity(ABC): self._on_remove = [] self._on_remove.append(func) - async def async_remove(self): + async def async_remove(self) -> None: """Remove entity from Home Assistant.""" + assert self.hass is not None await self.async_internal_will_remove_from_hass() await self.async_will_remove_from_hass() @@ -562,7 +563,7 @@ class Entity(ABC): def __repr__(self) -> str: """Return the representation.""" - return "".format(self.name, self.state) + return f"" # call an requests async def async_request_call(self, coro): diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 63d1b21fc9a..404fd4ed46d 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -5,22 +5,22 @@ from itertools import chain import logging from homeassistant import config as conf_util -from homeassistant.setup import async_prepare_setup_platform +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ENTITY_ID, - CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, + CONF_SCAN_INTERVAL, ENTITY_MATCH_ALL, ) -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform, discovery from homeassistant.helpers.config_validation import make_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 -from .entity_platform import EntityPlatform +from homeassistant.loader import async_get_integration, bind_hass +from homeassistant.setup import async_prepare_setup_platform +from .entity_platform import EntityPlatform # mypy: allow-untyped-defs, no-check-untyped-defs @@ -29,7 +29,7 @@ DATA_INSTANCES = "entity_components" @bind_hass -async def async_update_entity(hass, entity_id): +async def async_update_entity(hass: HomeAssistant, entity_id: str) -> None: """Trigger an update for an entity.""" domain = entity_id.split(".", 1)[0] entity_comp = hass.data.get(DATA_INSTANCES, {}).get(domain) @@ -58,19 +58,15 @@ class EntityComponent: - Process the configuration and set up a platform based component. - Manage the platforms and their entities. - Help extract the entities from a service call. - - Maintain a group that tracks all platform entities. - Listen for discovery events for platforms related to the domain. """ - def __init__( - self, logger, domain, hass, scan_interval=DEFAULT_SCAN_INTERVAL, group_name=None - ): + def __init__(self, logger, domain, hass, scan_interval=DEFAULT_SCAN_INTERVAL): """Initialize an entity component.""" self.logger = logger self.hass = hass self.domain = domain self.scan_interval = scan_interval - self.group_name = group_name self.config = None @@ -158,7 +154,7 @@ class EntityComponent: return await self._platforms[key].async_setup_entry(config_entry) - async def async_unload_entry(self, config_entry): + async def async_unload_entry(self, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" key = config_entry.entry_id @@ -236,36 +232,7 @@ class EntityComponent: await self._platforms[key].async_setup(platform_config, discovery_info) - @callback - def _async_update_group(self): - """Set up and/or update component group. - - This method must be run in the event loop. - """ - if self.group_name is None: - return - - ids = [ - entity.entity_id - for entity in sorted( - self.entities, key=lambda entity: entity.name or entity.entity_id - ) - ] - - self.hass.async_create_task( - self.hass.services.async_call( - "group", - "set", - dict( - object_id=slugify(self.group_name), - name=self.group_name, - visible=False, - entities=ids, - ), - ) - ) - - async def _async_reset(self): + async def _async_reset(self) -> None: """Remove entities and reset the entity component to initial values. This method must be run in the event loop. @@ -278,18 +245,13 @@ class EntityComponent: self._platforms = {self.domain: self._platforms[self.domain]} self.config = None - if self.group_name is not None: - await self.hass.services.async_call( - "group", "remove", dict(object_id=slugify(self.group_name)) - ) - - async def async_remove_entity(self, entity_id): + async def async_remove_entity(self, entity_id: str) -> None: """Remove an entity managed by one of the platforms.""" for platform in self._platforms.values(): if entity_id in platform.entities: await platform.async_remove_entity(entity_id) - async def async_prepare_reload(self): + async def async_prepare_reload(self, *, skip_reset=False): """Prepare reloading this entity component. This method must be run in the event loop. @@ -309,7 +271,8 @@ class EntityComponent: if conf is None: return None - await self._async_reset() + if not skip_reset: + await self._async_reset() return conf def _async_init_entity_platform( @@ -327,5 +290,4 @@ class EntityComponent: platform=platform, scan_interval=scan_interval, entity_namespace=entity_namespace, - async_entities_added_callback=self._async_update_group, ) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 376a6e23e9a..0e4d80ac080 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,16 +1,16 @@ """Class to manage the entities for a single platform.""" import asyncio from contextvars import ContextVar +from datetime import datetime from typing import Optional from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import callback, valid_entity_id, split_entity_id +from homeassistant.core import callback, split_entity_id, valid_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION -from .event import async_track_time_interval, async_call_later - +from .event import async_call_later, async_track_time_interval # mypy: allow-untyped-defs, no-check-untyped-defs @@ -32,7 +32,6 @@ class EntityPlatform: platform, scan_interval, entity_namespace, - async_entities_added_callback, ): """Initialize the entity platform. @@ -42,7 +41,6 @@ class EntityPlatform: platform_name: str scan_interval: timedelta entity_namespace: str - async_entities_added_callback: @callback method """ self.hass = hass self.logger = logger @@ -51,7 +49,6 @@ class EntityPlatform: self.platform = platform self.scan_interval = scan_interval self.entity_namespace = entity_namespace - self.async_entities_added_callback = async_entities_added_callback self.config_entry = None self.entities = {} self._tasks = [] @@ -65,14 +62,14 @@ class EntityPlatform: # which powers entity_component.add_entities if platform is None: self.parallel_updates = None - self.parallel_updates_semaphore = None + self.parallel_updates_semaphore: Optional[asyncio.Semaphore] = None return self.parallel_updates = getattr(platform, "PARALLEL_UPDATES", None) # semaphore will be created on demand self.parallel_updates_semaphore = None - def _get_parallel_updates_semaphore(self): + def _get_parallel_updates_semaphore(self) -> asyncio.Semaphore: """Get or create a semaphore for parallel updates.""" if self.parallel_updates_semaphore is None: self.parallel_updates_semaphore = asyncio.Semaphore( @@ -86,6 +83,16 @@ class EntityPlatform: platform = self.platform hass = self.hass + if not hasattr(platform, "async_setup_platform") and not hasattr( + platform, "setup_platform" + ): + self.logger.error( + "The %s platform for the %s integration does not support platform setup. Please remove it from your config.", + self.platform_name, + self.domain, + ) + return + @callback def async_create_setup_task(): """Get task to set up platform.""" @@ -139,7 +146,8 @@ class EntityPlatform: warn_task = hass.loop.call_later( SLOW_SETUP_WARNING, logger.warning, - "Setup of platform %s is taking over %s seconds.", + "Setup of %s platform %s is taking over %s seconds.", + self.domain, self.platform_name, SLOW_SETUP_WARNING, ) @@ -250,7 +258,6 @@ class EntityPlatform: return await asyncio.wait(tasks) - self.async_entities_added_callback() if self._async_unsub_polling is not None or not any( entity.should_poll for entity in self.entities.values() @@ -304,9 +311,7 @@ class EntityPlatform: suggested_object_id = entity.name if self.entity_namespace is not None: - suggested_object_id = "{} {}".format( - self.entity_namespace, suggested_object_id - ) + suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" if self.config_entry is not None: config_entry_id = self.config_entry.entry_id @@ -347,6 +352,10 @@ class EntityPlatform: device_id=device_id, known_object_ids=self.entities.keys(), disabled_by=disabled_by, + capabilities=entity.capability_attributes, + supported_features=entity.supported_features, + device_class=entity.device_class, + unit_of_measurement=entity.unit_of_measurement, ) entity.registry_entry = entry @@ -377,9 +386,7 @@ class EntityPlatform: ) if self.entity_namespace is not None: - suggested_object_id = "{} {}".format( - self.entity_namespace, suggested_object_id - ) + suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" entity.entity_id = entity_registry.async_generate_entity_id( self.domain, suggested_object_id, self.entities.keys() ) @@ -387,15 +394,19 @@ 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(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) - ): + + already_exists = entity.entity_id in self.entities + + if not already_exists: + existing = self.hass.states.get(entity.entity_id) + + if existing and not existing.attributes.get("restored"): + already_exists = True + + if already_exists: 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 - ) + msg += f". Platform {self.platform_name} does not generate unique IDs" raise HomeAssistantError(msg) entity_id = entity.entity_id @@ -407,7 +418,7 @@ class EntityPlatform: await entity.async_update_ha_state() - async def async_reset(self): + async def async_reset(self) -> None: """Remove all entities and reset data. This method must be run in the event loop. @@ -427,7 +438,7 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None - async def async_remove_entity(self, entity_id): + async def async_remove_entity(self, entity_id: str) -> None: """Remove entity id from platform.""" await self.entities[entity_id].async_remove() @@ -438,7 +449,7 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None - async def _update_entity_states(self, now): + async def _update_entity_states(self, now: datetime) -> None: """Update the states of all the polling entities. To protect from flooding the executor, we will update async entities @@ -450,7 +461,7 @@ class EntityPlatform: self._process_updates = asyncio.Lock() if self._process_updates.locked(): self.logger.warning( - "Updating %s %s took longer than the scheduled update " "interval %s", + "Updating %s %s took longer than the scheduled update interval %s", self.platform_name, self.domain, self.scan_interval, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 08f29a9fb3e..66d1bb94f60 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -11,10 +11,17 @@ import asyncio from collections import OrderedDict from itertools import chain import logging -from typing import Any, Dict, Iterable, List, Optional, cast +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, cast import attr +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + ATTR_UNIT_OF_MEASUREMENT, + EVENT_HOMEASSISTANT_START, + STATE_UNAVAILABLE, +) from homeassistant.core import Event, callback, split_entity_id, valid_entity_id from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass @@ -23,6 +30,8 @@ from homeassistant.util.yaml import load_yaml from .typing import HomeAssistantType +if TYPE_CHECKING: + from homeassistant.config_entries import ConfigEntry # noqa: F401 # mypy: allow-untyped-defs, no-check-untyped-defs @@ -37,6 +46,8 @@ DISABLED_HASS = "hass" DISABLED_USER = "user" DISABLED_INTEGRATION = "integration" +ATTR_RESTORED = "restored" + STORAGE_VERSION = 1 STORAGE_KEY = "core.entity_registry" @@ -49,7 +60,7 @@ class RegistryEntry: unique_id = attr.ib(type=str) platform = attr.ib(type=str) name = attr.ib(type=str, default=None) - device_id = attr.ib(type=str, default=None) + device_id: Optional[str] = attr.ib(default=None) config_entry_id: Optional[str] = attr.ib(default=None) disabled_by = attr.ib( type=Optional[str], @@ -64,6 +75,10 @@ class RegistryEntry: ) ), ) + capabilities: Optional[Dict[str, Any]] = attr.ib(default=None) + supported_features: int = attr.ib(default=0) + device_class: Optional[str] = attr.ib(default=None) + unit_of_measurement: Optional[str] = attr.ib(default=None) domain = attr.ib(type=str, init=False, repr=False) @domain.default @@ -136,16 +151,23 @@ class EntityRegistry: @callback def async_get_or_create( self, - domain, - platform, - unique_id, + domain: str, + platform: str, + unique_id: str, *, - suggested_object_id=None, - config_entry=None, - device_id=None, - known_object_ids=None, - disabled_by=None, - ): + # To influence entity ID generation + suggested_object_id: Optional[str] = None, + known_object_ids: Optional[Iterable[str]] = None, + # To disable an entity if it gets created + disabled_by: Optional[str] = None, + # Data that we want entry to have + config_entry: Optional["ConfigEntry"] = None, + device_id: Optional[str] = None, + capabilities: Optional[Dict[str, Any]] = None, + supported_features: Optional[int] = None, + device_class: Optional[str] = None, + unit_of_measurement: Optional[str] = None, + ) -> RegistryEntry: """Get entity. Create if it doesn't exist.""" config_entry_id = None if config_entry: @@ -154,10 +176,14 @@ class EntityRegistry: entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: - return self._async_update_entity( + return self._async_update_entity( # type: ignore entity_id, config_entry_id=config_entry_id or _UNDEF, device_id=device_id or _UNDEF, + capabilities=capabilities or _UNDEF, + supported_features=supported_features or _UNDEF, + device_class=device_class or _UNDEF, + unit_of_measurement=unit_of_measurement or _UNDEF, # 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 @@ -185,6 +211,10 @@ class EntityRegistry: unique_id=unique_id, platform=platform, disabled_by=disabled_by, + capabilities=capabilities, + supported_features=supported_features or 0, + device_class=device_class, + unit_of_measurement=unit_of_measurement, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -229,12 +259,15 @@ class EntityRegistry: disabled_by=_UNDEF, ): """Update properties of an entity.""" - return self._async_update_entity( - entity_id, - name=name, - new_entity_id=new_entity_id, - new_unique_id=new_unique_id, - disabled_by=disabled_by, + return cast( # cast until we have _async_update_entity type hinted + RegistryEntry, + self._async_update_entity( + entity_id, + name=name, + new_entity_id=new_entity_id, + new_unique_id=new_unique_id, + disabled_by=disabled_by, + ), ) @callback @@ -248,6 +281,10 @@ class EntityRegistry: device_id=_UNDEF, new_unique_id=_UNDEF, disabled_by=_UNDEF, + capabilities=_UNDEF, + supported_features=_UNDEF, + device_class=_UNDEF, + unit_of_measurement=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] @@ -259,6 +296,10 @@ class EntityRegistry: ("config_entry_id", config_entry_id), ("device_id", device_id), ("disabled_by", disabled_by), + ("capabilities", capabilities), + ("supported_features", supported_features), + ("device_class", device_class), + ("unit_of_measurement", unit_of_measurement), ): if value is not _UNDEF and value != getattr(old, attr_name): changes[attr_name] = value @@ -289,9 +330,8 @@ class EntityRegistry: ) if conflict: raise ValueError( - "Unique id '{}' is already in use by '{}'".format( - new_unique_id, conflict.entity_id - ) + f"Unique id '{new_unique_id}' is already in use by " + f"'{conflict.entity_id}'" ) changes["unique_id"] = new_unique_id @@ -313,6 +353,8 @@ class EntityRegistry: async def async_load(self) -> None: """Load the entity registry.""" + async_setup_entity_restore(self.hass, self) + data = await self.hass.helpers.storage.async_migrator( self.hass.config.path(PATH_REGISTRY), self._store, @@ -331,6 +373,10 @@ class EntityRegistry: platform=entity["platform"], name=entity.get("name"), disabled_by=entity.get("disabled_by"), + capabilities=entity.get("capabilities") or {}, + supported_features=entity.get("supported_features", 0), + device_class=entity.get("device_class"), + unit_of_measurement=entity.get("unit_of_measurement"), ) self.entities = entities @@ -354,6 +400,10 @@ class EntityRegistry: "platform": entry.platform, "name": entry.name, "disabled_by": entry.disabled_by, + "capabilities": entry.capabilities, + "supported_features": entry.supported_features, + "device_class": entry.device_class, + "unit_of_measurement": entry.unit_of_measurement, } for entry in self.entities.values() ] @@ -411,3 +461,56 @@ async def _async_migrate(entities: Dict[str, Any]) -> Dict[str, List[Dict[str, A {"entity_id": entity_id, **info} for entity_id, info in entities.items() ] } + + +@callback +def async_setup_entity_restore( + hass: HomeAssistantType, registry: EntityRegistry +) -> None: + """Set up the entity restore mechanism.""" + + @callback + def cleanup_restored_states(event: Event) -> None: + """Clean up restored states.""" + if event.data["action"] != "remove": + return + + state = hass.states.get(event.data["entity_id"]) + + if state is None or not state.attributes.get(ATTR_RESTORED): + return + + hass.states.async_remove(event.data["entity_id"]) + + hass.bus.async_listen(EVENT_ENTITY_REGISTRY_UPDATED, cleanup_restored_states) + + if hass.is_running: + return + + @callback + def _write_unavailable_states(_: Event) -> None: + """Make sure state machine contains entry for each registered entity.""" + states = hass.states + existing = set(states.async_entity_ids()) + + for entry in registry.entities.values(): + if entry.entity_id in existing or entry.disabled: + continue + + attrs: Dict[str, Any] = {ATTR_RESTORED: True} + + if entry.capabilities is not None: + attrs.update(entry.capabilities) + + if entry.supported_features is not None: + attrs[ATTR_SUPPORTED_FEATURES] = entry.supported_features + + if entry.device_class is not None: + attrs[ATTR_DEVICE_CLASS] = entry.device_class + + if entry.unit_of_measurement is not None: + attrs[ATTR_UNIT_OF_MEASUREMENT] = entry.unit_of_measurement + + states.async_set(entry.entity_id, STATE_UNAVAILABLE, attrs) + + hass.bus.async_listen(EVENT_HOMEASSISTANT_START, _write_unavailable_states) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 715344a3969..b3c8af6f50c 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -5,23 +5,22 @@ from typing import Any, Callable, Dict, Iterable, Optional, Union, cast import attr -from homeassistant.loader import bind_hass -from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.helpers.template import Template -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event, State from homeassistant.const import ( ATTR_NOW, + EVENT_CORE_CONFIG_UPDATE, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, - EVENT_CORE_CONFIG_UPDATE, ) +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback +from homeassistant.helpers.sun import get_astral_event_next +from homeassistant.helpers.template import Template +from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util from homeassistant.util.async_ import run_callback_threadsafe - # PyLint does not like the use of threaded_listener_factory # pylint: disable=invalid-name diff --git a/homeassistant/helpers/icon.py b/homeassistant/helpers/icon.py index b2a1d58717b..dd64e9c92f1 100644 --- a/homeassistant/helpers/icon.py +++ b/homeassistant/helpers/icon.py @@ -8,7 +8,7 @@ def icon_for_battery_level( """Return a battery icon valid identifier.""" icon = "mdi:battery" if battery_level is None: - return icon + "-unknown" + return f"{icon}-unknown" if charging and battery_level > 10: icon += "-charging-{}".format(int(round(battery_level / 20 - 0.01)) * 20) elif charging: diff --git a/homeassistant/helpers/integration_platform.py b/homeassistant/helpers/integration_platform.py new file mode 100644 index 00000000000..01567c72c7b --- /dev/null +++ b/homeassistant/helpers/integration_platform.py @@ -0,0 +1,46 @@ +"""Helpers to help with integration platforms.""" +import asyncio +import logging +from typing import Any, Awaitable, Callable + +from homeassistant.core import Event, HomeAssistant +from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass +from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED + +_LOGGER = logging.getLogger(__name__) + + +@bind_hass +async def async_process_integration_platforms( + hass: HomeAssistant, + platform_name: str, + # Any = platform. + process_platform: Callable[[HomeAssistant, str, Any], Awaitable[None]], +) -> None: + """Process a specific platform for all current and future loaded integrations.""" + + async def _process(component_name: str) -> None: + """Process the intents of a component.""" + try: + integration = await async_get_integration(hass, component_name) + platform = integration.get_platform(platform_name) + except (IntegrationNotFound, ImportError): + return + + try: + await process_platform(hass, component_name, platform) + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Error processing platform %s.%s", component_name, platform_name + ) + + async def async_component_loaded(event: Event) -> None: + """Handle a new component loaded.""" + await _process(event.data[ATTR_COMPONENT]) + + hass.bus.async_listen(EVENT_COMPONENT_LOADED, async_component_loaded) + + tasks = [_process(comp) for comp in hass.config.components] + + if tasks: + await asyncio.gather(*tasks) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 12b346603f0..8fdf617e3f6 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -5,13 +5,12 @@ from typing import Any, Callable, Dict, Iterable, Optional import voluptuous as vol -from homeassistant.const import ATTR_SUPPORTED_FEATURES -from homeassistant.core import callback, State, T, Context +from homeassistant.const import ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES +from homeassistant.core import Context, State, T, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass -from homeassistant.const import ATTR_ENTITY_ID _LOGGER = logging.getLogger(__name__) _SlotsType = Dict[str, Any] @@ -159,7 +158,7 @@ class IntentHandler: def __repr__(self) -> str: """Represent a string of an intent handler.""" - return "<{} - {}>".format(self.__class__.__name__, self.intent_type) + return f"<{self.__class__.__name__} - {self.intent_type}>" def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> Optional[T]: diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index dd9e3833801..0b274458045 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -1,9 +1,7 @@ """Helpers for logging allowing more advanced logging styles to be used.""" import inspect import logging - - -# mypy: allow-untyped-defs, no-check-untyped-defs +from typing import Any, Mapping, MutableMapping, Optional, Tuple class KeywordMessage: @@ -13,13 +11,13 @@ class KeywordMessage: Adapted from: https://stackoverflow.com/a/24683360/2267718 """ - def __init__(self, fmt, args, kwargs): - """Initialize a new BraceMessage object.""" + def __init__(self, fmt: Any, args: Any, kwargs: Mapping[str, Any]) -> None: + """Initialize a new KeywordMessage object.""" self._fmt = fmt self._args = args self._kwargs = kwargs - def __str__(self): + def __str__(self) -> str: """Convert the object to a string for logging.""" return str(self._fmt).format(*self._args, **self._kwargs) @@ -27,26 +25,30 @@ class KeywordMessage: class KeywordStyleAdapter(logging.LoggerAdapter): """Represents an adapter wrapping the logger allowing KeywordMessages.""" - def __init__(self, logger, extra=None): + def __init__( + self, logger: logging.Logger, extra: Optional[Mapping[str, Any]] = None + ) -> None: """Initialize a new StyleAdapter for the provided logger.""" super().__init__(logger, extra or {}) - def log(self, level, msg, *args, **kwargs): + def log(self, level: int, msg: Any, *args: Any, **kwargs: Any) -> None: """Log the message provided at the appropriate level.""" if self.isEnabledFor(level): msg, log_kwargs = self.process(msg, kwargs) - self.logger._log( # pylint: disable=protected-access + self.logger._log( # type: ignore # pylint: disable=protected-access level, KeywordMessage(msg, args, kwargs), (), **log_kwargs ) - def process(self, msg, kwargs): + def process( + self, msg: Any, kwargs: MutableMapping[str, Any] + ) -> Tuple[Any, MutableMapping[str, Any]]: """Process the keyward args in preparation for logging.""" return ( msg, { k: kwargs[k] for k in inspect.getfullargspec( - self.logger._log # pylint: disable=protected-access + self.logger._log # type: ignore # pylint: disable=protected-access ).args[1:] if k in kwargs }, diff --git a/homeassistant/helpers/network.py b/homeassistant/helpers/network.py index 671e7f1fa56..a446b575077 100644 --- a/homeassistant/helpers/network.py +++ b/homeassistant/helpers/network.py @@ -1,6 +1,6 @@ """Network helpers.""" -from typing import Optional, cast from ipaddress import ip_address +from typing import Optional, cast import yarl diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index 5d47f34b002..0c3dbe96bc5 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -1,24 +1,24 @@ """Support for restoring entity states on startup.""" import asyncio +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime -from typing import Any, Dict, List, Set, Optional +from typing import Any, Dict, List, Optional, Set +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import ( - HomeAssistant, - callback, - State, CoreState, + HomeAssistant, + State, + callback, valid_entity_id, ) -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP -import homeassistant.util.dt as dt_util +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry 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 - +import homeassistant.util.dt as dt_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any @@ -124,19 +124,27 @@ class RestoreStateData: """ now = dt_util.utcnow() all_states = self.hass.states.async_all() - current_entity_ids = set(state.entity_id for state in all_states) + # Entities currently backed by an entity object + current_entity_ids = set( + state.entity_id + for state in all_states + if not state.attributes.get(entity_registry.ATTR_RESTORED) + ) # Start with the currently registered states stored_states = [ StoredState(state, now) for state in all_states - if state.entity_id in self.entity_ids + if state.entity_id in self.entity_ids and + # Ignore all states that are entity registry placeholders + not state.attributes.get(entity_registry.ATTR_RESTORED) ] - expiration_time = now - STATE_EXPIRATION for entity_id, stored_state in self.last_states.items(): # Don't save old states that have entities in the current run + # They are either registered and already part of stored_states, + # or no longer care about restoring. if entity_id in current_entity_ids: continue @@ -169,7 +177,7 @@ class RestoreStateData: self.hass.async_create_task(self.async_dump_states()) # Dump the initial states now. This helps minimize the risk of having - # old states loaded by overwritting the last states once home assistant + # old states loaded by overwriting the last states once Home Assistant # has started and the old states have been read. _async_dump_states() diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 21dd0b71487..837a561181d 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -1,16 +1,16 @@ """Helpers to execute scripts.""" import asyncio -import logging from contextlib import suppress from datetime import datetime from itertools import islice -from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any +import logging +from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple import voluptuous as vol +from homeassistant import exceptions import homeassistant.components.device_automation as device_automation import homeassistant.components.scene as scene -from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE from homeassistant.const import ( ATTR_ENTITY_ID, CONF_CONDITION, @@ -19,21 +19,20 @@ from homeassistant.const import ( CONF_TIMEOUT, SERVICE_TURN_ON, ) -from homeassistant import exceptions +from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback from homeassistant.helpers import ( - service, condition, - template as template, config_validation as cv, + service, + template as template, ) from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_template, ) from homeassistant.helpers.typing import ConfigType -import homeassistant.util.dt as date_util from homeassistant.util.async_ import run_callback_threadsafe - +import homeassistant.util.dt as date_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -300,7 +299,7 @@ class Script: _LOGGER.error("Error rendering '%s' delay template: %s", self.name, ex) raise _StopScript - self.last_action = action.get(CONF_ALIAS, "delay {}".format(delay)) + self.last_action = action.get(CONF_ALIAS, f"delay {delay}") self._log("Executing step %s" % self.last_action) unsub = async_track_point_in_utc_time( @@ -409,7 +408,7 @@ class Script: self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION]) check = config(self.hass, variables) - self._log("Test condition {}: {}".format(self.last_action, check)) + self._log(f"Test condition {self.last_action}: {check}") if not check: raise _StopScript @@ -447,6 +446,6 @@ class Script: def _log(self, msg): """Logger helper.""" if self.name is not None: - msg = "Script {}: {}".format(self.name, msg) + msg = f"Script {self.name}: {msg}" _LOGGER.info(msg) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 45393dc0486..16fabe251af 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -7,7 +7,7 @@ from typing import Callable import voluptuous as vol from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_CONTROL -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ATTR_AREA_ID +from homeassistant.const import ATTR_AREA_ID, ATTR_ENTITY_ID, ENTITY_MATCH_ALL import homeassistant.core as ha from homeassistant.exceptions import ( HomeAssistantError, @@ -16,12 +16,11 @@ from homeassistant.exceptions import ( UnknownUser, ) from homeassistant.helpers import template, typing +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration, bind_hass from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml.loader import JSON_TYPE -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import HomeAssistantType - # mypy: allow-untyped-defs, no-check-untyped-defs @@ -241,7 +240,7 @@ def async_set_service_schema(hass, domain, service, schema): "fields": schema.get("fields") or {}, } - hass.data[SERVICE_DESCRIPTION_CACHE]["{}.{}".format(domain, service)] = description + hass.data[SERVICE_DESCRIPTION_CACHE][f"{domain}.{service}"] = description @bind_hass diff --git a/homeassistant/helpers/signal.py b/homeassistant/helpers/signal.py index 12792918742..53802a2a119 100644 --- a/homeassistant/helpers/signal.py +++ b/homeassistant/helpers/signal.py @@ -4,8 +4,8 @@ import signal import sys from types import FrameType -from homeassistant.core import callback, HomeAssistant from homeassistant.const import RESTART_EXIT_CODE +from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index abc97bf1f8a..60e6acc8797 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -1,13 +1,11 @@ """Helpers that help with state related things.""" import asyncio +from collections import defaultdict import datetime as dt import logging -from collections import defaultdict from types import ModuleType, TracebackType from typing import Dict, Iterable, List, Optional, Type, Union -from homeassistant.loader import bind_hass, async_get_integration, IntegrationNotFound -import homeassistant.util.dt as dt_util from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON from homeassistant.const import ( STATE_CLOSED, @@ -21,6 +19,9 @@ from homeassistant.const import ( STATE_UNLOCKED, ) from homeassistant.core import Context, State +from homeassistant.loader import IntegrationNotFound, async_get_integration, bind_hass +import homeassistant.util.dt as dt_util + from .typing import HomeAssistantType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index bd18eebfb25..aed6da37518 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -3,14 +3,13 @@ import asyncio from json import JSONEncoder import logging import os -from typing import Dict, List, Optional, Callable, Union, Any, Type +from typing import Any, Callable, Dict, List, Optional, Type, Union from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass from homeassistant.util import json as json_util -from homeassistant.helpers.event import async_call_later - # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any # mypy: no-check-untyped-defs diff --git a/homeassistant/helpers/sun.py b/homeassistant/helpers/sun.py index 9fa6e074bdd..45ff06f16de 100644 --- a/homeassistant/helpers/sun.py +++ b/homeassistant/helpers/sun.py @@ -1,11 +1,12 @@ """Helpers for sun events.""" import datetime -from typing import Optional, Union, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Union from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET from homeassistant.core import callback -from homeassistant.util import dt as dt_util from homeassistant.loader import bind_hass +from homeassistant.util import dt as dt_util + from .typing import HomeAssistantType if TYPE_CHECKING: diff --git a/homeassistant/helpers/system_info.py b/homeassistant/helpers/system_info.py index b552a634d4b..7d1d6f2b3e7 100644 --- a/homeassistant/helpers/system_info.py +++ b/homeassistant/helpers/system_info.py @@ -6,6 +6,7 @@ from typing import Dict from homeassistant.const import __version__ as current_version from homeassistant.loader import bind_hass from homeassistant.util.package import is_virtual_env + from .typing import HomeAssistantType diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index 30b428a9e17..e0846d6f893 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -2,9 +2,9 @@ from numbers import Number from typing import Optional +from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS from homeassistant.core import HomeAssistant from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS def display_temp( diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 7dcf08ebf92..8565315f87f 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,12 +1,12 @@ """Template helper methods for rendering strings with Home Assistant data.""" import base64 +from datetime import datetime +from functools import wraps import json import logging import math import random import re -from datetime import datetime -from functools import wraps from typing import Any, Dict, Iterable, List, Optional, Union import jinja2 @@ -30,7 +30,6 @@ from homeassistant.loader import bind_hass from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe - # mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any @@ -329,7 +328,7 @@ class AllStates: if not valid_entity_id(name): raise TemplateError(f"Invalid entity ID '{name}'") return _get_state(self._hass, name) - if not valid_entity_id(name + ".entity"): + if not valid_entity_id(f"{name}.entity"): raise TemplateError(f"Invalid domain name '{name}'") return DomainStates(self._hass, name) @@ -452,7 +451,7 @@ class TemplateState(State): """Representation of Template State.""" state = object.__getattribute__(self, "_access_state")() rep = state.__repr__() - return "