diff --git a/.coveragerc b/.coveragerc index 7f3aa2762bd..1d861d69c1d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -158,7 +158,6 @@ omit = homeassistant/components/ecovacs/* homeassistant/components/eddystone_temperature/sensor.py homeassistant/components/edimax/switch.py - homeassistant/components/edp_redy/* homeassistant/components/egardia/* homeassistant/components/eight_sleep/* homeassistant/components/eliqonline/sensor.py @@ -242,7 +241,6 @@ omit = homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py homeassistant/components/google_travel_time/sensor.py - homeassistant/components/googlehome/* homeassistant/components/gpmdp/media_player.py homeassistant/components/gpsd/sensor.py homeassistant/components/greeneye_monitor/* @@ -309,6 +307,7 @@ omit = homeassistant/components/joaoapps_join/* homeassistant/components/juicenet/* homeassistant/components/kankun/switch.py + homeassistant/components/keba/* homeassistant/components/keenetic_ndms2/device_tracker.py homeassistant/components/keyboard/* homeassistant/components/keyboard_remote/* @@ -317,6 +316,8 @@ omit = homeassistant/components/knx/* homeassistant/components/knx/climate.py homeassistant/components/knx/cover.py + homeassistant/components/kodi/__init__.py + homeassistant/components/kodi/const.py homeassistant/components/kodi/media_player.py homeassistant/components/kodi/notify.py homeassistant/components/konnected/* @@ -374,8 +375,9 @@ omit = homeassistant/components/metoffice/weather.py homeassistant/components/microsoft/tts.py homeassistant/components/miflora/sensor.py - homeassistant/components/mikrotik/device_tracker.py + homeassistant/components/mikrotik/* homeassistant/components/mill/climate.py + homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py homeassistant/components/mjpeg/camera.py homeassistant/components/mobile_app/* @@ -467,6 +469,7 @@ omit = homeassistant/components/plaato/* homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py + homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* @@ -532,7 +535,6 @@ omit = homeassistant/components/rtorrent/sensor.py homeassistant/components/russound_rio/media_player.py homeassistant/components/russound_rnet/media_player.py - homeassistant/components/ruter/sensor.py homeassistant/components/sabnzbd/* homeassistant/components/satel_integra/* homeassistant/components/scrape/sensor.py @@ -740,6 +742,7 @@ omit = homeassistant/components/zha/core/device.py homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/helpers.py + homeassistant/components/zha/core/registries.py homeassistant/components/zha/device_entity.py homeassistant/components/zha/entity.py homeassistant/components/zha/light.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index e70706f8af4..22bd4384b23 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,11 +2,12 @@ { "name": "Home Assistant Dev", "context": "..", - "dockerFile": "Dockerfile", + "dockerFile": "../Dockerfile.dev", "postCreateCommand": "pip3 install -e .", "appPort": 8123, "runArgs": [ - "-e", "GIT_EDTIOR='code --wait'" + "-e", + "GIT_EDITOR=\"code --wait\"" ], "extensions": [ "ms-python.python", @@ -22,7 +23,6 @@ "editor.formatOnSave": true, "editor.formatOnType": true, "files.trimTrailingWhitespace": true, - "editor.rulers": [80], "terminal.integrated.shell.linux": "/bin/bash", "yaml.customTags": [ "!secret scalar", @@ -32,4 +32,4 @@ "!include_dir_merge_named scalar" ] } -} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ff3e8d838a3..5389954ca59 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,7 @@ develop-eggs .installed.cfg lib lib64 +pip-wheel-metadata # Logs *.log @@ -58,9 +59,11 @@ pip-log.txt # Unit test / coverage reports .coverage .tox +coverage.xml nosetests.xml htmlcov/ test-reports/ +test-results.xml # Translations *.mo @@ -121,3 +124,6 @@ desktop.ini # monkeytype monkeytype.sqlite3 + +# This is left behind by Azure Restore Cache +tmp_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5134f5f14aa..78b7ec29859 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,15 @@ repos: -- repo: https://github.com/python/black +- repo: https://github.com/psf/black rev: 19.3b0 hooks: - id: black args: - --safe - --quiet +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.8 + hooks: + - id: flake8 + additional_dependencies: + - flake8-docstrings==1.3.1 + - pydocstyle==4.0.0 diff --git a/.travis.yml b/.travis.yml index f54f4027de4..3447571a3e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,14 +16,18 @@ addons: matrix: fast_finish: true include: - - python: "3.6" + - python: "3.6.0" env: TOXENV=lint - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=pylint - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=typing - - python: "3.6" + dist: trusty + - python: "3.6.0" env: TOXENV=py36 + dist: trusty - python: "3.7" env: TOXENV=py37 diff --git a/CODEOWNERS b/CODEOWNERS index c5ff9a21b4b..81c5aafed30 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -9,10 +9,6 @@ homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core -# Virtualization -Dockerfile @home-assistant/docker -virtualization/Docker/* @home-assistant/docker - # Other code homeassistant/scripts/check_config.py @kellerza @@ -24,6 +20,7 @@ homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya +homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core homeassistant/components/aprs/* @PhilRW @@ -43,6 +40,7 @@ homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot +homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/braviatv/* @robbiet480 homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg @@ -73,7 +71,6 @@ homeassistant/components/discogs/* @thibmaek homeassistant/components/doorbird/* @oblogic7 homeassistant/components/dweet/* @fabaff homeassistant/components/ecovacs/* @OverloadUT -homeassistant/components/edp_redy/* @abmantis homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/elv/* @majuss @@ -101,13 +98,13 @@ homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb +homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff homeassistant/components/gntp/* @robbiet480 homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan homeassistant/components/google_travel_time/* @robbiet480 -homeassistant/components/googlehome/* @ludeeus homeassistant/components/gpsd/* @fabaff homeassistant/components/group/* @home-assistant/core homeassistant/components/gtfs/* @robbiet480 @@ -142,6 +139,7 @@ homeassistant/components/ipma/* @dgomes homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/jewish_calendar/* @tsvi +homeassistant/components/keba/* @dannerph homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate @@ -171,6 +169,7 @@ homeassistant/components/meteoalarm/* @rolfberkenbosch homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel homeassistant/components/mill/* @danielhiversen homeassistant/components/min_max/* @fabaff +homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff @@ -181,6 +180,7 @@ homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netdata/* @fabaff +homeassistant/components/netgear_lte/* @amelchio homeassistant/components/nextbus/* @vividboarder homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek @@ -203,6 +203,7 @@ homeassistant/components/philips_js/* @elupus homeassistant/components/pi_hole/* @fabaff homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel +homeassistant/components/plugwise/* @laetificat @CoMPaTech homeassistant/components/point/* @fredrike homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig @@ -220,7 +221,6 @@ homeassistant/components/repetier/* @MTrab homeassistant/components/rfxtrx/* @danielhiversen homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roomba/* @pschmitt -homeassistant/components/ruter/* @ludeeus homeassistant/components/scene/* @home-assistant/core homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core @@ -286,7 +286,7 @@ homeassistant/components/updater/* @home-assistant/core homeassistant/components/upnp/* @robbiet480 homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/utility_meter/* @dgomes -homeassistant/components/velbus/* @ceral2nd +homeassistant/components/velbus/* @cereal2nd homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a9e73699558..00000000000 --- a/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Notice: -# When updating this file, please also update virtualization/Docker/Dockerfile.dev -# This way, the development image and the production image are kept in sync. - -FROM python:3.7-buster -LABEL maintainer="Paulus Schoutsen " - -# Uncomment any of the following lines to disable the installation. -#ENV INSTALL_TELLSTICK no -#ENV INSTALL_OPENALPR no -#ENV INSTALL_FFMPEG no -#ENV INSTALL_LIBCEC no -#ENV INSTALL_SSOCR no -#ENV INSTALL_DLIB no -#ENV INSTALL_IPERF3 no -#ENV INSTALL_LOCALES no - -VOLUME /config - -WORKDIR /usr/src/app - -# Copy build scripts -COPY virtualization/Docker/ virtualization/Docker/ -RUN virtualization/Docker/setup_docker_prereqs - -# Install hass component dependencies -COPY requirements_all.txt requirements_all.txt -RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.12.2 cchardet cython tensorflow - -# Copy source -COPY . . - -EXPOSE 8123 -EXPOSE 8300 -EXPOSE 51827 - -CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/.devcontainer/Dockerfile b/Dockerfile.dev similarity index 92% rename from .devcontainer/Dockerfile rename to Dockerfile.dev index 8abf28cddff..00f5576bdbb 100644 --- a/.devcontainer/Dockerfile +++ b/Dockerfile.dev @@ -16,14 +16,15 @@ RUN apt-get update \ WORKDIR /usr/src +# Setup hass-release RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ && cd hass-release \ && pip3 install -e . -WORKDIR /workspace +WORKDIR /workspaces # Install Python dependencies from requirements.txt if it exists -COPY requirements_test_all.txt homeassistant/package_constraints.txt /workspace/ +COPY requirements_test_all.txt homeassistant/package_constraints.txt /workspaces/ RUN pip3 install -r requirements_test_all.txt -c package_constraints.txt # Set the default shell to bash instead of sh diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 9158d8045f0..0ee272f900d 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -18,9 +18,12 @@ resources: image: homeassistant/ci-azure:3.6 - container: 37 image: homeassistant/ci-azure:3.7 + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' variables: - - name: ArtifactFeed - value: '2df3ae11-3bf6-49bc-a809-ba0d340d6a6d' - name: PythonMain value: '36' - group: codecov @@ -95,42 +98,23 @@ stages: python.container: '37' container: $[ variables['python.container'] ] steps: - - script: | - python --version > .cache - displayName: 'Set python $(python.container) for requirement cache' - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - displayName: 'Restore artifacts based on Requirements' - inputs: + - template: templates/azp-step-cache.yaml@azure + parameters: keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' - targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' - - script: | - set -e - python -m venv venv + build: | + set -e + python -m venv venv - . venv/bin/activate - pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt - pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt - # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. - # Find offending deps with `pipdeptree -r -p typing` - pip uninstall -y typing - displayName: 'Create Virtual Environment & Install Requirements' - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - # Explicit Cache Save (instead of using RestoreAndSaveCache) - # Dont wait with cache save for all the other task in this job to complete (±30 minutes), other parallel jobs might utilize this - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - displayName: 'Save artifacts based on Requirements' - inputs: - keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' - targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' - - script: | - . venv/bin/activate - pip install -e . - displayName: 'Install Home Assistant for python $(python.container)' + . venv/bin/activate + pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt + pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt + # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. + # Find offending deps with `pipdeptree -r -p typing` + pip uninstall -y typing - script: | . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml -qq -o console_output_style=count -p no:sugar tests + script/check_dirty displayName: 'Run pytest for python $(python.container)' condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) - script: | @@ -139,6 +123,7 @@ stages: . venv/bin/activate pytest --timeout=9 --durations=10 --junitxml=test-results.xml --cov --cov-report=xml -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) + script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - task: PublishTestResults@2 @@ -162,35 +147,17 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python --version > .cache - displayName: 'Set python $(PythonMain) for requirement cache' - - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - displayName: 'Restore artifacts based on Requirements' - inputs: + - template: templates/azp-step-cache.yaml@azure + parameters: keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' - targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' - - script: | - set -e - python -m venv venv + build: | + set -e + python -m venv venv - . venv/bin/activate - pip install -U pip setuptools - pip install -r requirements_all.txt -c homeassistant/package_constraints.txt - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Create Virtual Environment & Install Requirements' - condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - displayName: 'Save artifacts based on Requirements' - inputs: - keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' - targetfolder: './venv' - vstsFeed: '$(ArtifactFeed)' - - script: | - . venv/bin/activate - pip install -e . - displayName: 'Install Home Assistant for python $(PythonMain)' + . venv/bin/activate + pip install -U pip setuptools + pip install -r requirements_all.txt -c homeassistant/package_constraints.txt + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate pylint homeassistant @@ -204,6 +171,7 @@ stages: python -m venv venv . venv/bin/activate + pip install -e . pip install -r requirements_test.txt -c homeassistant/package_constraints.txt displayName: 'Setup Env' - script: | diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 768e9627e4c..7c88e615fa5 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -3,37 +3,39 @@ trigger: tags: include: - - '*' + - '*' pr: none +schedules: + - cron: "0 1 * * *" + displayName: "nightly builds" + branches: + include: + - dev + always: true variables: - name: versionBuilder - value: '5.2' + value: '6.3' - group: docker - group: github - group: twine - +resources: + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' stages: - stage: 'Validate' jobs: - - job: 'VersionValidate' + - template: templates/azp-job-version.yaml@azure + parameters: + ignoreDev: true + - job: 'Permission' pool: vmImage: 'ubuntu-latest' steps: - - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' - inputs: - versionSpec: '3.7' - - script: | - setup_version="$(python setup.py -V)" - branch_version="$(Build.SourceBranchName)" - - if [ "${setup_version}" != "${branch_version}" ]; then - echo "Version of tag ${branch_version} don't match with ${setup_version}!" - exit 1 - fi - displayName: 'Check version of branch/tag' - script: | sudo apt-get install -y --no-install-recommends \ jq curl @@ -48,10 +50,12 @@ stages: echo "${created_by} is not allowed to create an release!" exit 1 displayName: 'Check rights' + condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags')) - stage: 'Build' jobs: - job: 'ReleasePython' + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') pool: vmImage: 'ubuntu-latest' steps: @@ -66,7 +70,7 @@ stages: - script: | export TWINE_USERNAME="$(twineUser)" export TWINE_PASSWORD="$(twinePassword)" - + twine upload dist/* --skip-existing displayName: 'Upload pypi' - job: 'ReleaseDocker' @@ -92,26 +96,29 @@ stages: buildArch: 'aarch64' buildMachine: 'qemuarm-64,raspberrypi3-64,raspberrypi4-64,odroid-c2,orangepi-prime' steps: - - script: sudo docker login -u $(dockerUser) -p $(dockerPassword) + - template: templates/azp-step-ha-version.yaml@azure + - script: | + docker login -u $(dockerUser) -p $(dockerPassword) displayName: 'Docker hub login' - - script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder) + - script: docker pull homeassistant/amd64-builder:$(versionBuilder) displayName: 'Install Builder' - script: | set -e - sudo docker run --rm --privileged \ - -v ~/.docker:/root/.docker \ + docker run --rm --privileged \ + -v ~/.docker:/root/.docker:rw \ -v /run/docker.sock:/run/docker.sock:rw \ + -v $(pwd):/homeassistant:ro \ homeassistant/amd64-builder:$(versionBuilder) \ - --homeassistant $(Build.SourceBranchName) "--$(buildArch)" \ + --homeassistant $(homeassistantRelease) "--$(buildArch)" \ -r https://github.com/home-assistant/hassio-homeassistant \ -t generic --docker-hub homeassistant - sudo docker run --rm --privileged \ + docker run --rm --privileged \ -v ~/.docker:/root/.docker \ -v /run/docker.sock:/run/docker.sock:rw \ homeassistant/amd64-builder:$(versionBuilder) \ - --homeassistant-machine "$(Build.SourceBranchName)=$(buildMachine)" \ + --homeassistant-machine "$(homeassistantRelease)=$(buildMachine)" \ -r https://github.com/home-assistant/hassio-homeassistant \ -t machine --docker-hub homeassistant displayName: 'Build Release' @@ -122,6 +129,7 @@ stages: pool: vmImage: 'ubuntu-latest' steps: + - template: templates/azp-step-ha-version.yaml@azure - script: | sudo apt-get install -y --no-install-recommends \ git jq curl @@ -135,7 +143,7 @@ stages: - script: | set -e - version="$(Build.SourceBranchName)" + version="$(homeassistantRelease)" git clone https://github.com/home-assistant/hassio-version cd hassio-version @@ -144,11 +152,11 @@ stages: beta_version="$(jq --raw-output '.homeassistant.default' beta.json)" stable_version="$(jq --raw-output '.homeassistant.default' stable.json)" - if [[ "$version" =~ b ]]; then + if [[ "$version" =~ d ]]; then sed -i "s|$dev_version|$version|g" dev.json + elif [[ "$version" =~ b ]]; then sed -i "s|$beta_version|$version|g" beta.json else - sed -i "s|$dev_version|$version|g" dev.json sed -i "s|$beta_version|$version|g" beta.json sed -i "s|$stable_version|$version|g" stable.json fi @@ -156,3 +164,72 @@ stages: git commit -am "Bump Home Assistant $version" git push displayName: 'Update version files' + - job: 'ReleaseDocker' + pool: + vmImage: 'ubuntu-latest' + steps: + - template: templates/azp-step-ha-version.yaml@azure + - script: | + docker login -u $(dockerUser) -p $(dockerPassword) + displayName: 'Docker login' + - script: | + set -e + export DOCKER_CLI_EXPERIMENTAL=enabled + + function create_manifest() { + local tag_l=$1 + local tag_r=$2 + + docker manifest create homeassistant/home-assistant:${tag_l} \ + homeassistant/amd64-homeassistant:${tag_r} \ + homeassistant/i386-homeassistant:${tag_r} \ + homeassistant/armhf-homeassistant:${tag_r} \ + homeassistant/armv7-homeassistant:${tag_r} \ + homeassistant/aarch64-homeassistant:${tag_r} + + docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/amd64-homeassistant:${tag_r} \ + --os linux --arch amd64 + + docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/i386-homeassistant:${tag_r} \ + --os linux --arch 386 + + docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/armhf-homeassistant:${tag_r} \ + --os linux --arch arm --variant=v6 + + docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/armv7-homeassistant:${tag_r} \ + --os linux --arch arm --variant=v7 + + docker manifest annotate homeassistant/home-assistant:${tag_l} \ + homeassistant/aarch64-homeassistant:${tag_r} \ + --os linux --arch arm64 --variant=v8 + + docker manifest push --purge homeassistant/home-assistant:${tag_l} + } + + docker pull homeassistant/amd64-homeassistant:$(homeassistantRelease) + docker pull homeassistant/i386-homeassistant:$(homeassistantRelease) + docker pull homeassistant/armhf-homeassistant:$(homeassistantRelease) + docker pull homeassistant/armv7-homeassistant:$(homeassistantRelease) + docker pull homeassistant/aarch64-homeassistant:$(homeassistantRelease) + + # Create version tag + create_manifest "$(homeassistantRelease)" "$(homeassistantRelease)" + + # Create general tags + if [[ "$(homeassistantRelease)" =~ d ]]; then + create_manifest "dev" "$(homeassistantRelease)" + elif [[ "$(homeassistantRelease)" =~ b ]]; then + create_manifest "beta" "$(homeassistantRelease)" + create_manifest "rc" "$(homeassistantRelease)" + else + create_manifest "stable" "$(homeassistantRelease)" + create_manifest "latest" "$(homeassistantRelease)" + create_manifest "beta" "$(homeassistantRelease)" + create_manifest "rc" "$(homeassistantRelease)" + fi + + displayName: 'Create Meta-Image' diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 89e45fc31da..77e9cbb95f1 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -9,91 +9,61 @@ trigger: include: - requirements_all.txt pr: none +schedules: +- cron: '0 */8 * * *' + displayName: 'daily builds' + branches: + include: + - dev + always: true variables: - name: versionWheels - value: '1.0-3.7-alpine3.10' - - group: wheels - + value: '1.1-3.7-alpine3.10' +resources: + repositories: + - repository: azure + type: github + name: 'home-assistant/ci-azure' + endpoint: 'home-assistant' jobs: +- template: templates/azp-job-wheels.yaml@azure + parameters: + builderVersion: '$(versionWheels)' + builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev' + wheelsRequirement: 'requirements_wheels.txt' + wheelsRequirementDiff: 'requirements_diff.txt' + preBuild: + - script: | + cp requirements_all.txt requirements_wheels.txt + if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then + touch requirements_diff.txt + else + curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.txt + fi -- job: 'Wheels' - timeoutInMinutes: 360 - pool: - vmImage: 'ubuntu-latest' - strategy: - maxParallel: 5 - matrix: - amd64: - buildArch: 'amd64' - i386: - buildArch: 'i386' - armhf: - buildArch: 'armhf' - armv7: - buildArch: 'armv7' - aarch64: - buildArch: 'aarch64' - steps: - - script: | - sudo apt-get update - sudo apt-get install -y --no-install-recommends \ - qemu-user-static \ - binfmt-support \ - curl - - sudo mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc - sudo update-binfmts --enable qemu-arm - sudo update-binfmts --enable qemu-aarch64 - displayName: 'Initial cross build' - - script: | - mkdir -p .ssh - echo -e "-----BEGIN RSA PRIVATE KEY-----\n$(wheelsSSH)\n-----END RSA PRIVATE KEY-----" >> .ssh/id_rsa - ssh-keyscan -H $(wheelsHost) >> .ssh/known_hosts - chmod 600 .ssh/* - displayName: 'Install ssh key' - - script: sudo docker pull homeassistant/$(buildArch)-wheels:$(versionWheels) - displayName: 'Install wheels builder' - - script: | - cp requirements_all.txt requirements_wheels.txt - if [[ "$(Build.Reason)" =~ (Schedule|Manual) ]]; then - touch requirements_diff.txt - else - curl -s -o requirements_diff.txt https://raw.githubusercontent.com/home-assistant/home-assistant/master/requirements_all.txt - fi - - requirement_files="requirements_wheels.txt requirements_diff.txt" - for requirement_file in ${requirement_files}; do - sed -i "s|# pytradfri|pytradfri|g" ${requirement_file} - sed -i "s|# pybluez|pybluez|g" ${requirement_file} - sed -i "s|# bluepy|bluepy|g" ${requirement_file} - sed -i "s|# beacontools|beacontools|g" ${requirement_file} - sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} - sed -i "s|# raspihats|raspihats|g" ${requirement_file} - sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file} - sed -i "s|# blinkt|blinkt|g" ${requirement_file} - sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} - sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} - sed -i "s|# evdev|evdev|g" ${requirement_file} - sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file} - sed -i "s|# i2csense|i2csense|g" ${requirement_file} - sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} - sed -i "s|# pycups|pycups|g" ${requirement_file} - sed -i "s|# homekit|homekit|g" ${requirement_file} - sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} - sed -i "s|# decora|decora|g" ${requirement_file} - sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} - sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} - sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} - done - displayName: 'Prepare requirements files for Hass.io' - - script: | - sudo docker run --rm -v $(pwd):/data:ro -v $(pwd)/.ssh:/root/.ssh:rw \ - homeassistant/$(buildArch)-wheels:$(versionWheels) \ - --apk "build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;linux-headers;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev" \ - --index $(wheelsIndex) \ - --requirement requirements_wheels.txt \ - --requirement-diff requirements_diff.txt \ - --upload rsync \ - --remote wheels@$(wheelsHost):/opt/wheels - displayName: 'Run wheels build' + requirement_files="requirements_wheels.txt requirements_diff.txt" + for requirement_file in ${requirement_files}; do + sed -i "s|# pytradfri|pytradfri|g" ${requirement_file} + sed -i "s|# pybluez|pybluez|g" ${requirement_file} + sed -i "s|# bluepy|bluepy|g" ${requirement_file} + sed -i "s|# beacontools|beacontools|g" ${requirement_file} + sed -i "s|# RPi.GPIO|RPi.GPIO|g" ${requirement_file} + sed -i "s|# raspihats|raspihats|g" ${requirement_file} + sed -i "s|# rpi-rf|rpi-rf|g" ${requirement_file} + sed -i "s|# blinkt|blinkt|g" ${requirement_file} + sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file} + sed -i "s|# pyuserinput|pyuserinput|g" ${requirement_file} + sed -i "s|# evdev|evdev|g" ${requirement_file} + sed -i "s|# smbus-cffi|smbus-cffi|g" ${requirement_file} + sed -i "s|# i2csense|i2csense|g" ${requirement_file} + sed -i "s|# python-eq3bt|python-eq3bt|g" ${requirement_file} + sed -i "s|# pycups|pycups|g" ${requirement_file} + sed -i "s|# homekit|homekit|g" ${requirement_file} + sed -i "s|# decora_wifi|decora_wifi|g" ${requirement_file} + sed -i "s|# decora|decora|g" ${requirement_file} + sed -i "s|# PySwitchbot|PySwitchbot|g" ${requirement_file} + sed -i "s|# pySwitchmate|pySwitchmate|g" ${requirement_file} + sed -i "s|# face_recognition|face_recognition|g" ${requirement_file} + done + displayName: 'Prepare requirements files for Hass.io' diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d21bfb5a71a..8ec2a8c2d3c 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -10,12 +10,7 @@ import threading from typing import List, Dict, Any, TYPE_CHECKING # noqa pylint: disable=unused-import from homeassistant import monkey_patch -from homeassistant.const import ( - __version__, - EVENT_HOMEASSISTANT_START, - REQUIRED_PYTHON_VER, - RESTART_EXIT_CODE, -) +from homeassistant.const import __version__, REQUIRED_PYTHON_VER, RESTART_EXIT_CODE if TYPE_CHECKING: from homeassistant import core @@ -309,23 +304,10 @@ async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: log_no_color=args.log_no_color, ) - if args.open_ui: - # Imported here to avoid importing asyncio before monkey patch - from homeassistant.util.async_ import run_callback_threadsafe + if args.open_ui and hass.config.api is not None: + import webbrowser - def open_browser(_: Any) -> None: - """Open the web interface in a browser.""" - if hass.config.api is not None: - import webbrowser - - webbrowser.open(hass.config.api.base_url) - - run_callback_threadsafe( - hass.loop, - hass.bus.async_listen_once, - EVENT_HOMEASSISTANT_START, - open_browser, - ) + hass.add_job(webbrowser.open, hass.config.api.base_url) return await hass.async_run() diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index f00687b828c..2641f0b8f7e 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -458,7 +458,7 @@ class AuthManager: result["data"] ) - if flow.context is not None and flow.context.get("credential_only"): + if flow.context.get("credential_only"): result["result"] = credentials return result diff --git a/homeassistant/auth/mfa_modules/__init__.py b/homeassistant/auth/mfa_modules/__init__.py index fa9b1f50224..5481b8fe08b 100644 --- a/homeassistant/auth/mfa_modules/__init__.py +++ b/homeassistant/auth/mfa_modules/__init__.py @@ -164,14 +164,9 @@ async def _load_mfa_module(hass: HomeAssistant, module_name: str) -> types.Modul processed = hass.data[DATA_REQS] = set() # https://github.com/python/mypy/issues/1424 - req_success = await requirements.async_process_requirements( + await requirements.async_process_requirements( hass, module_path, module.REQUIREMENTS # type: ignore ) - if not req_success: - raise HomeAssistantError( - "Unable to process requirements of mfa module {}".format(module_name) - ) - processed.add(module_name) return module diff --git a/homeassistant/auth/permissions/__init__.py b/homeassistant/auth/permissions/__init__.py index 5680b0aecb2..25253a1601c 100644 --- a/homeassistant/auth/permissions/__init__.py +++ b/homeassistant/auth/permissions/__init__.py @@ -7,6 +7,7 @@ from typing import ( # noqa: F401 Dict, List, Mapping, + Optional, Set, Tuple, Union, @@ -31,7 +32,7 @@ _LOGGER = logging.getLogger(__name__) class AbstractPermissions: """Default permissions class.""" - _cached_entity_func = None + _cached_entity_func: Optional[Callable[[str, str], bool]] = None def _entity_func(self) -> Callable[[str, str], bool]: """Return a function that can test entity access.""" diff --git a/homeassistant/auth/providers/__init__.py b/homeassistant/auth/providers/__init__.py index c720cf0df64..c35af2e0b96 100644 --- a/homeassistant/auth/providers/__init__.py +++ b/homeassistant/auth/providers/__init__.py @@ -165,15 +165,10 @@ async def load_auth_provider_module( # https://github.com/python/mypy/issues/1424 reqs = module.REQUIREMENTS # type: ignore - req_success = await requirements.async_process_requirements( + await requirements.async_process_requirements( hass, "auth provider {}".format(provider), reqs ) - if not req_success: - raise HomeAssistantError( - "Unable to process requirements of auth provider {}".format(provider) - ) - processed.add(provider) return module diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 8c14276ccc9..7bb572dcf6b 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -11,6 +11,9 @@ import logging from homeassistant.core import split_entity_id + +# mypy: allow-untyped-defs + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/adguard/.translations/es-419.json b/homeassistant/components/adguard/.translations/es-419.json index d62402f2eee..ed8e0c3a358 100644 --- a/homeassistant/components/adguard/.translations/es-419.json +++ b/homeassistant/components/adguard/.translations/es-419.json @@ -20,7 +20,8 @@ "username": "Nombre de usuario", "verify_ssl": "AdGuard Home utiliza un certificado adecuado" }, - "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control." + "description": "Configure su instancia de AdGuard Home para permitir la supervisi\u00f3n y el control.", + "title": "Enlace su AdGuard Home." } }, "title": "AdGuard Home" diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json index 338a5a77dad..7a58c396345 100644 --- a/homeassistant/components/adguard/.translations/fr.json +++ b/homeassistant/components/adguard/.translations/fr.json @@ -19,6 +19,7 @@ "username": "Nom d'utilisateur" } } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/hr.json b/homeassistant/components/adguard/.translations/hr.json new file mode 100644 index 00000000000..869cc46ea10 --- /dev/null +++ b/homeassistant/components/adguard/.translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "Postoje\u0107a konfiguracija je a\u017eurirana." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/id.json b/homeassistant/components/adguard/.translations/id.json new file mode 100644 index 00000000000..3548361e396 --- /dev/null +++ b/homeassistant/components/adguard/.translations/id.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "connection_error": "Gagal terhubung." + }, + "step": { + "user": { + "data": { + "password": "Kata sandi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 282df1756d7..83c7da41c16 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -47,7 +47,7 @@ CONF_DISPLAY_CATEGORIES = "display_categories" API_TEMP_UNITS = {TEMP_FAHRENHEIT: "FAHRENHEIT", TEMP_CELSIUS: "CELSIUS"} # Needs to be ordered dict for `async_api_set_thermostat_mode` which does a -# reverse mapping of this dict and we want to map the first occurrance of OFF +# reverse mapping of this dict and we want to map the first occurrence of OFF # back to HA state. API_THERMOSTAT_MODES = OrderedDict( [ diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index b060d35be90..03d153f5927 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -264,7 +264,9 @@ class ClimateCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" # If we support two modes, one being off, we allow turning on too. - if climate.HVAC_MODE_OFF in self.entity.attributes[climate.ATTR_HVAC_MODES]: + if climate.HVAC_MODE_OFF in self.entity.attributes.get( + climate.ATTR_HVAC_MODES, [] + ): yield AlexaPowerController(self.entity) yield AlexaThermostatController(self.hass, self.entity) diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index 2b7d15ac841..3195656ed09 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -163,7 +163,7 @@ class AlexaResponse: The Alexa response includes a list of properties which provides feedback on how states have changed. For example if a user asks, - "Alexa, set theromstat to 20 degrees", the API expects a response with + "Alexa, set thermostat to 20 degrees", the API expects a response with the new value of the property, and Alexa will respond to the user "Thermostat set to 20 degrees". diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index f27b0428072..c7098867ee8 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -45,7 +45,7 @@ SUPPORTED_VOICES = [ "Ruben", "Lotte", # Dutch "Russell", - "Nicole", # English Austrailian + "Nicole", # English Australian "Brian", "Amy", "Emma", # English diff --git a/homeassistant/components/ambiclimate/.translations/es-419.json b/homeassistant/components/ambiclimate/.translations/es-419.json index eaac252d605..607454f4402 100644 --- a/homeassistant/components/ambiclimate/.translations/es-419.json +++ b/homeassistant/components/ambiclimate/.translations/es-419.json @@ -7,6 +7,17 @@ }, "create_entry": { "default": "Autenticaci\u00f3n exitosa con Ambiclimate" - } + }, + "error": { + "follow_link": "Por favor, siga el enlace y autent\u00edquese antes de presionar Enviar", + "no_token": "No autenticado con Ambiclimate" + }, + "step": { + "auth": { + "description": "Por favor, siga este [link]('authorization_url') y Permitir acceso a su cuenta de Ambiclimate, luego vuelva y presione Enviar a continuaci\u00f3n.\n(Aseg\u00farese de que la url de devoluci\u00f3n de llamada especificada es {cb_url})", + "title": "Autenticaci\u00f3n de Ambiclimate" + } + }, + "title": "Ambiclimate" } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 510edd540ec..056930edfc7 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/ambient_station", "requirements": [ - "aioambient==0.3.1" + "aioambient==0.3.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 9f1233179e7..047eaaaf5db 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/components/androidtv", "requirements": [ - "androidtv==0.0.18" + "androidtv==0.0.24" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@JeffLIrion"] } diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 381f0bb7cf1..db4ff9e851e 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -3,6 +3,9 @@ import functools import logging import voluptuous as vol +from androidtv import setup, ha_state_detection_rules_validator +from androidtv.constants import APPS, KEYS + from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, @@ -64,6 +67,7 @@ CONF_ADB_SERVER_IP = "adb_server_ip" CONF_ADB_SERVER_PORT = "adb_server_port" CONF_APPS = "apps" CONF_GET_SOURCES = "get_sources" +CONF_STATE_DETECTION_RULES = "state_detection_rules" CONF_TURN_ON_COMMAND = "turn_on_command" CONF_TURN_OFF_COMMAND = "turn_off_command" @@ -99,6 +103,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_APPS, default=dict()): vol.Schema({cv.string: cv.string}), vol.Optional(CONF_TURN_ON_COMMAND): cv.string, vol.Optional(CONF_TURN_OFF_COMMAND): cv.string, + vol.Optional(CONF_STATE_DETECTION_RULES, default={}): vol.Schema( + {cv.string: ha_state_detection_rules_validator(vol.Invalid)} + ), } ) @@ -114,8 +121,6 @@ ANDROIDTV_STATES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Android TV / Fire TV platform.""" - from androidtv import setup - hass.data.setdefault(ANDROIDTV_DOMAIN, {}) host = "{0}:{1}".format(config[CONF_HOST], config[CONF_PORT]) @@ -125,12 +130,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_log = "using Python ADB implementation " if CONF_ADBKEY in config: aftv = setup( - host, config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS] + host, + config[CONF_ADBKEY], + device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) adb_log += "with adbkey='{0}'".format(config[CONF_ADBKEY]) else: - aftv = setup(host, device_class=config[CONF_DEVICE_CLASS]) + aftv = setup( + host, + device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], + ) adb_log += "without adbkey authentication" else: # Use "pure-python-adb" (communicate with ADB server) @@ -139,6 +151,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): adb_server_ip=config[CONF_ADB_SERVER_IP], adb_server_port=config[CONF_ADB_SERVER_PORT], device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) adb_log = "using ADB server at {0}:{1}".format( config[CONF_ADB_SERVER_IP], config[CONF_ADB_SERVER_PORT] @@ -182,7 +195,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_name = config[CONF_NAME] if CONF_NAME in config else "Fire TV" add_entities([device]) - _LOGGER.debug("Setup %s at %s%s", device_name, host, adb_log) + _LOGGER.debug("Setup %s at %s %s", device_name, host, adb_log) hass.data[ANDROIDTV_DOMAIN][host] = device if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): @@ -251,14 +264,15 @@ class ADBDevice(MediaPlayerDevice): def __init__(self, aftv, name, apps, turn_on_command, turn_off_command): """Initialize the Android TV / Fire TV device.""" - from androidtv.constants import APPS, KEYS - self.aftv = aftv self._name = name - self._apps = APPS + self._apps = APPS.copy() self._apps.update(apps) self._keys = KEYS + self._device_properties = self.aftv.device_properties + self._unique_id = self._device_properties.get("serialno") + self.turn_on_command = turn_on_command self.turn_off_command = turn_off_command @@ -327,6 +341,11 @@ class ADBDevice(MediaPlayerDevice): """Return the state of the player.""" return self._state + @property + def unique_id(self): + """Return the device unique id.""" + return self._unique_id + @adb_decorator() def media_play(self): """Send play command.""" @@ -401,9 +420,7 @@ class AndroidTVDevice(ADBDevice): super().__init__(aftv, name, apps, turn_on_command, turn_off_command) self._device = None - self._device_properties = self.aftv.device_properties self._is_volume_muted = None - self._unique_id = self._device_properties.get("serialno") self._volume_level = None @adb_decorator(override_available=True) @@ -443,11 +460,6 @@ class AndroidTVDevice(ADBDevice): """Flag media player features that are supported.""" return SUPPORT_ANDROIDTV - @property - def unique_id(self): - """Return the device unique id.""" - return self._unique_id - @property def volume_level(self): """Return the volume level.""" diff --git a/homeassistant/components/arcam_fmj/.translations/ca.json b/homeassistant/components/arcam_fmj/.translations/ca.json deleted file mode 100644 index b0ad4660d0f..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/ca.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "config": { - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/ko.json b/homeassistant/components/arcam_fmj/.translations/ko.json deleted file mode 100644 index b0ad4660d0f..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/ko.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "config": { - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/nl.json b/homeassistant/components/arcam_fmj/.translations/nl.json deleted file mode 100644 index 7197976d212..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/nl.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "config": { - "abort": { - "one": "Een", - "other": "Ander" - }, - "error": { - "one": "Een", - "other": "Ander" - }, - "step": { - "one": "Een", - "other": "Ander" - }, - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/pl.json b/homeassistant/components/arcam_fmj/.translations/pl.json deleted file mode 100644 index 5521c18c079..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/pl.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "config": { - "abort": { - "few": "kilka", - "many": "wiele", - "one": "jeden", - "other": "inne" - }, - "error": { - "few": "kilka", - "many": "wiele", - "one": "jeden", - "other": "inne" - }, - "step": { - "few": "kilka", - "many": "wiele", - "one": "jeden", - "other": "inne" - }, - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/zh-Hant.json b/homeassistant/components/arcam_fmj/.translations/zh-Hant.json deleted file mode 100644 index b0ad4660d0f..00000000000 --- a/homeassistant/components/arcam_fmj/.translations/zh-Hant.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "config": { - "title": "Arcam FMJ" - } -} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/media_player.py b/homeassistant/components/arcam_fmj/media_player.py index 5cbfe2dd482..971abc3e26d 100644 --- a/homeassistant/components/arcam_fmj/media_player.py +++ b/homeassistant/components/arcam_fmj/media_player.py @@ -124,7 +124,7 @@ class ArcamFmj(MediaPlayerDevice): return support async def async_added_to_hass(self): - """Once registed add listener for events.""" + """Once registered, add listener for events.""" await self._state.start() @callback diff --git a/homeassistant/components/arcam_fmj/strings.json b/homeassistant/components/arcam_fmj/strings.json index 5844c277364..b0006dbb5ae 100644 --- a/homeassistant/components/arcam_fmj/strings.json +++ b/homeassistant/components/arcam_fmj/strings.json @@ -1,8 +1,5 @@ { "config": { - "title": "Arcam FMJ", - "step": {}, - "error": {}, - "abort": {} + "title": "Arcam FMJ" } } diff --git a/homeassistant/components/auth/.translations/es-419.json b/homeassistant/components/auth/.translations/es-419.json index 852965596e0..4ac97068905 100644 --- a/homeassistant/components/auth/.translations/es-419.json +++ b/homeassistant/components/auth/.translations/es-419.json @@ -16,9 +16,13 @@ "description": "Se ha enviado una contrase\u00f1a \u00fanica a trav\u00e9s de **notify.{notify_service}**. Por favor ingr\u00e9selo a continuaci\u00f3n:", "title": "Verificar la configuracion" } - } + }, + "title": "Notificar contrase\u00f1a de un solo uso" }, "totp": { + "error": { + "invalid_code": "C\u00f3digo no v\u00e1lido, por favor vuelva a intentarlo. Si recibe este error constantemente, aseg\u00farese de que el reloj de su sistema Home Assistant sea exacto." + }, "step": { "init": { "description": "Para activar la autenticaci\u00f3n de dos factores utilizando contrase\u00f1as de un solo uso basadas en el tiempo, escanee el c\u00f3digo QR con su aplicaci\u00f3n de autenticaci\u00f3n. Si no tiene uno, le recomendamos [Autenticador de Google] (https://support.google.com/accounts/answer/1066447) o [Authy] (https://authy.com/). \n\n {qr_code} \n \n Despu\u00e9s de escanear el c\u00f3digo, ingrese el c\u00f3digo de seis d\u00edgitos de su aplicaci\u00f3n para verificar la configuraci\u00f3n. Si tiene problemas para escanear el c\u00f3digo QR, realice una configuraci\u00f3n manual con el c\u00f3digo ** ` {code} ` **.", diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 89c3e87f78a..c18bc276a44 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -31,7 +31,7 @@ async def async_setup(hass): """Init mfa setup flow manager.""" async def _async_create_setup_flow(handler, context, data): - """Create a setup flow. hanlder is a mfa module.""" + """Create a setup flow. handler is a mfa module.""" mfa_module = hass.auth.get_auth_mfa_module(handler) if mfa_module is None: raise ValueError("Mfa module {} is not found".format(handler)) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 935f6ea0d02..5de9336d1d9 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -30,6 +30,10 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.loader import bind_hass from homeassistant.util.dt import parse_datetime, utcnow + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs, no-warn-return-any + DOMAIN = "automation" ENTITY_ID_FORMAT = DOMAIN + ".{}" diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index 2bb70fa1c96..b090484ab67 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -5,6 +5,8 @@ from homeassistant.const import CONF_DOMAIN, CONF_PLATFORM from homeassistant.loader import async_get_integration +# mypy: allow-untyped-defs, no-check-untyped-defs + TRIGGER_SCHEMA = vol.Schema( {vol.Required(CONF_PLATFORM): "device", vol.Required(CONF_DOMAIN): str}, extra=vol.ALLOW_EXTRA, diff --git a/homeassistant/components/automation/event.py b/homeassistant/components/automation/event.py index b353eb56196..d372aedd1d7 100644 --- a/homeassistant/components/automation/event.py +++ b/homeassistant/components/automation/event.py @@ -7,6 +7,9 @@ from homeassistant.core import callback from homeassistant.const import CONF_PLATFORM from homeassistant.helpers import config_validation as cv + +# mypy: allow-untyped-defs + CONF_EVENT_TYPE = "event_type" CONF_EVENT_DATA = "event_data" diff --git a/homeassistant/components/automation/geo_location.py b/homeassistant/components/automation/geo_location.py index 7c0994c4b30..3f2aa1c00d7 100644 --- a/homeassistant/components/automation/geo_location.py +++ b/homeassistant/components/automation/geo_location.py @@ -13,6 +13,9 @@ from homeassistant.const import ( 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" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER diff --git a/homeassistant/components/automation/homeassistant.py b/homeassistant/components/automation/homeassistant.py index 96931e62766..bd1da7e7e1f 100644 --- a/homeassistant/components/automation/homeassistant.py +++ b/homeassistant/components/automation/homeassistant.py @@ -6,6 +6,9 @@ import voluptuous as vol from homeassistant.core import callback, CoreState from homeassistant.const import CONF_PLATFORM, CONF_EVENT, EVENT_HOMEASSISTANT_STOP + +# mypy: allow-untyped-defs + EVENT_START = "start" EVENT_SHUTDOWN = "shutdown" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/litejet.py b/homeassistant/components/automation/litejet.py index c642781ca66..7bc4c937765 100644 --- a/homeassistant/components/automation/litejet.py +++ b/homeassistant/components/automation/litejet.py @@ -9,6 +9,9 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util from homeassistant.helpers.event import track_point_in_utc_time + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_NUMBER = "number" diff --git a/homeassistant/components/automation/mqtt.py b/homeassistant/components/automation/mqtt.py index 26c1ea5683d..fd9a778dbfc 100644 --- a/homeassistant/components/automation/mqtt.py +++ b/homeassistant/components/automation/mqtt.py @@ -8,6 +8,9 @@ from homeassistant.components import mqtt from homeassistant.const import CONF_PLATFORM, CONF_PAYLOAD import homeassistant.helpers.config_validation as cv + +# mypy: allow-untyped-defs + CONF_ENCODING = "encoding" CONF_TOPIC = "topic" DEFAULT_ENCODING = "utf-8" diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index f990e599552..b33d724d770 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -16,6 +16,9 @@ from homeassistant.const import ( from homeassistant.helpers.event import async_track_state_change, async_track_same_state from homeassistant.helpers import condition, config_validation as cv, template + +# mypy: allow-untyped-defs, no-check-untyped-defs + TRIGGER_SCHEMA = vol.All( vol.Schema( { diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index ccea3d9ec5a..5fbe97185a7 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -9,6 +9,9 @@ from homeassistant.const import MATCH_ALL, CONF_PLATFORM, CONF_FOR from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import async_track_state_change, async_track_same_state + +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_ENTITY_ID = "entity_id" diff --git a/homeassistant/components/automation/sun.py b/homeassistant/components/automation/sun.py index e4d41830e0f..7cbbe56f326 100644 --- a/homeassistant/components/automation/sun.py +++ b/homeassistant/components/automation/sun.py @@ -14,6 +14,9 @@ from homeassistant.const import ( from homeassistant.helpers.event import async_track_sunrise, async_track_sunset import homeassistant.helpers.config_validation as cv + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index a48f252312b..c83d660912c 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -10,6 +10,9 @@ from homeassistant.helpers import condition 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 + _LOGGER = logging.getLogger(__name__) TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema( diff --git a/homeassistant/components/automation/time.py b/homeassistant/components/automation/time.py index 958c1f007bc..3942d0efadb 100644 --- a/homeassistant/components/automation/time.py +++ b/homeassistant/components/automation/time.py @@ -8,6 +8,9 @@ from homeassistant.const import CONF_AT, CONF_PLATFORM 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__) TRIGGER_SCHEMA = vol.Schema( diff --git a/homeassistant/components/automation/time_pattern.py b/homeassistant/components/automation/time_pattern.py index 15180e07094..f749a308bf7 100644 --- a/homeassistant/components/automation/time_pattern.py +++ b/homeassistant/components/automation/time_pattern.py @@ -8,6 +8,9 @@ from homeassistant.const import CONF_PLATFORM 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" CONF_MINUTES = "minutes" CONF_SECONDS = "seconds" diff --git a/homeassistant/components/automation/webhook.py b/homeassistant/components/automation/webhook.py index ceb764cea96..706afbe9042 100644 --- a/homeassistant/components/automation/webhook.py +++ b/homeassistant/components/automation/webhook.py @@ -11,6 +11,9 @@ import homeassistant.helpers.config_validation as cv from . import DOMAIN as AUTOMATION_DOMAIN + +# mypy: allow-untyped-defs + DEPENDENCIES = ("webhook",) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index 1f0f558f0de..35b11006024 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -12,6 +12,9 @@ from homeassistant.const import ( from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers import condition, config_validation as cv, location + +# mypy: allow-untyped-defs, no-check-untyped-defs + EVENT_ENTER = "enter" EVENT_LEAVE = "leave" DEFAULT_EVENT = EVENT_ENTER diff --git a/homeassistant/components/axis/.translations/es-419.json b/homeassistant/components/axis/.translations/es-419.json index 1e9301a19da..c5404a173f6 100644 --- a/homeassistant/components/axis/.translations/es-419.json +++ b/homeassistant/components/axis/.translations/es-419.json @@ -2,10 +2,13 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "bad_config_file": "Datos err\u00f3neos del archivo de configuraci\u00f3n" + "bad_config_file": "Datos err\u00f3neos del archivo de configuraci\u00f3n", + "link_local_address": "Las direcciones locales de enlace no son compatibles", + "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso.", "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, @@ -15,8 +18,10 @@ "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario" - } + }, + "title": "Configurar dispositivo Axis" } - } + }, + "title": "Dispositivo Axis" } } \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index 020cd8f5946..e85fceaf463 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "bad_config_file": "Mauvaises donn\u00e9es du fichier de configuration", - "link_local_address": "Les adresses locales ne sont pas prises en charge" + "link_local_address": "Les adresses locales ne sont pas prises en charge", + "not_axis_device": "L'appareil d\u00e9couvert n'est pas un appareil Axis" }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index 951f4a423e5..9af6a10c425 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -13,6 +13,9 @@ from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA_BASE, ) + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "binary_sensor" SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 9b44012e758..c257470bb2d 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -143,7 +143,10 @@ class BMWConnectedDriveAccount: for listener in self._update_listeners: listener() except IOError as exception: - _LOGGER.error("Error updating the vehicle state") + _LOGGER.error( + "Could not connect to the BMW Connected Drive portal. " + "The vehicle state could not be updated." + ) _LOGGER.exception(exception) def add_update_listener(self, listener): diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index d52bec330fb..418ccbabffe 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -9,17 +9,17 @@ from . import DOMAIN as BMW_DOMAIN _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "lids": ["Doors", "opening"], - "windows": ["Windows", "opening"], - "door_lock_state": ["Door lock state", "safety"], - "lights_parking": ["Parking lights", "light"], - "condition_based_services": ["Condition based services", "problem"], - "check_control_messages": ["Control messages", "problem"], + "lids": ["Doors", "opening", "mdi:car-door"], + "windows": ["Windows", "opening", "mdi:car-door"], + "door_lock_state": ["Door lock state", "safety", "mdi:car-key"], + "lights_parking": ["Parking lights", "light", "mdi:car-parking-lights"], + "condition_based_services": ["Condition based services", "problem", "mdi:wrench"], + "check_control_messages": ["Control messages", "problem", "mdi:car-tire-alert"], } SENSOR_TYPES_ELEC = { - "charging_status": ["Charging status", "power"], - "connection_status": ["Connection status", "plug"], + "charging_status": ["Charging status", "power", "mdi:ev-station"], + "connection_status": ["Connection status", "plug", "mdi:car-electric"], } SENSOR_TYPES_ELEC.update(SENSOR_TYPES) @@ -35,24 +35,28 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if vehicle.has_hv_battery: _LOGGER.debug("BMW with a high voltage battery") for key, value in sorted(SENSOR_TYPES_ELEC.items()): - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1] - ) - devices.append(device) + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + devices.append(device) elif vehicle.has_internal_combustion_engine: _LOGGER.debug("BMW with an internal combustion engine") for key, value in sorted(SENSOR_TYPES.items()): - device = BMWConnectedDriveSensor( - account, vehicle, key, value[0], value[1] - ) - devices.append(device) + if key in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, key, value[0], value[1], value[2] + ) + devices.append(device) add_entities(devices, True) class BMWConnectedDriveSensor(BinarySensorDevice): """Representation of a BMW vehicle binary sensor.""" - def __init__(self, account, vehicle, attribute: str, sensor_name, device_class): + def __init__( + self, account, vehicle, attribute: str, sensor_name, device_class, icon + ): """Constructor.""" self._account = account self._vehicle = vehicle @@ -61,6 +65,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): self._unique_id = "{}-{}".format(self._vehicle.vin, self._attribute) self._sensor_name = sensor_name self._device_class = device_class + self._icon = icon self._state = None @property @@ -81,6 +86,11 @@ class BMWConnectedDriveSensor(BinarySensorDevice): """Return the name of the binary sensor.""" return self._name + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + @property def device_class(self): """Return the class of the binary sensor.""" @@ -112,23 +122,19 @@ class BMWConnectedDriveSensor(BinarySensorDevice): for report in vehicle_state.condition_based_services: result.update(self._format_cbs_report(report)) elif self._attribute == "check_control_messages": - check_control_messages = vehicle_state.check_control_messages - if not check_control_messages: - result["check_control_messages"] = "OK" - else: + check_control_messages = vehicle_state.has_check_control_messages + if check_control_messages: cbs_list = [] for message in check_control_messages: cbs_list.append(message["ccmDescriptionShort"]) result["check_control_messages"] = cbs_list + else: + result["check_control_messages"] = "OK" elif self._attribute == "charging_status": result["charging_status"] = vehicle_state.charging_status.value - # pylint: disable=protected-access - result["last_charging_end_result"] = vehicle_state._attributes[ - "lastChargingEndResult" - ] - if self._attribute == "connection_status": - # pylint: disable=protected-access - result["connection_status"] = vehicle_state._attributes["connectionStatus"] + result["last_charging_end_result"] = vehicle_state.last_charging_end_result + elif self._attribute == "connection_status": + result["connection_status"] = vehicle_state.connection_status return sorted(result.items()) @@ -166,8 +172,7 @@ class BMWConnectedDriveSensor(BinarySensorDevice): # device class plug: On means device is plugged in, # Off means device is unplugged if self._attribute == "connection_status": - # pylint: disable=protected-access - self._state = vehicle_state._attributes["connectionStatus"] == "CONNECTED" + self._state = vehicle_state.connection_status == "CONNECTED" def _format_cbs_report(self, report): result = {} diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index eec81aa6525..0cc875c50f9 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -1,11 +1,12 @@ { "domain": "bmw_connected_drive", - "name": "Bmw connected drive", + "name": "BMW Connected Drive", "documentation": "https://www.home-assistant.io/components/bmw_connected_drive", "requirements": [ - "bimmer_connected==0.5.3" + "bimmer_connected==0.6.0" ], "dependencies": [], "codeowners": [ + "@gerard33" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index bc133fa4034..8248ded4f8b 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -51,14 +51,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for account in accounts: for vehicle in account.account.vehicles: for attribute_name in vehicle.drive_train_attributes: - device = BMWConnectedDriveSensor( - account, vehicle, attribute_name, attribute_info - ) - devices.append(device) - device = BMWConnectedDriveSensor( - account, vehicle, "mileage", attribute_info - ) - devices.append(device) + if attribute_name in vehicle.available_attributes: + device = BMWConnectedDriveSensor( + account, vehicle, attribute_name, attribute_info + ) + devices.append(device) add_entities(devices, True) diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index 2a7255f5a61..277260c0336 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -34,7 +34,9 @@ TIME_BETWEEN_UPDATES = timedelta(seconds=5) DEFAULT_NAME = "Broadlink switch" DEFAULT_TIMEOUT = 10 +DEFAULT_RETRY = 2 CONF_SLOTS = "slots" +CONF_RETRY = "retry" RM_TYPES = [ "rm", @@ -82,6 +84,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_FRIENDLY_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_TYPE, default=SWITCH_TYPES[0]): vol.In(SWITCH_TYPES), vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, + vol.Optional(CONF_RETRY, default=DEFAULT_RETRY): cv.positive_int, } ) @@ -96,6 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): friendly_name = config.get(CONF_FRIENDLY_NAME) mac_addr = binascii.unhexlify(config.get(CONF_MAC).encode().replace(b":", b"")) switch_type = config.get(CONF_TYPE) + retry_times = config.get(CONF_RETRY) def _get_mp1_slot_name(switch_friendly_name, slot): """Get slot name.""" @@ -116,21 +120,26 @@ def setup_platform(hass, config, add_entities, discovery_info=None): broadlink_device, device_config.get(CONF_COMMAND_ON), device_config.get(CONF_COMMAND_OFF), + retry_times, ) ) elif switch_type in SP1_TYPES: broadlink_device = broadlink.sp1((ip_addr, 80), mac_addr, None) - switches = [BroadlinkSP1Switch(friendly_name, broadlink_device)] + switches = [BroadlinkSP1Switch(friendly_name, broadlink_device, retry_times)] elif switch_type in SP2_TYPES: broadlink_device = broadlink.sp2((ip_addr, 80), mac_addr, None) - switches = [BroadlinkSP2Switch(friendly_name, broadlink_device)] + switches = [BroadlinkSP2Switch(friendly_name, broadlink_device, retry_times)] elif switch_type in MP1_TYPES: switches = [] broadlink_device = broadlink.mp1((ip_addr, 80), mac_addr, None) - parent_device = BroadlinkMP1Switch(broadlink_device) + parent_device = BroadlinkMP1Switch(broadlink_device, retry_times) for i in range(1, 5): slot = BroadlinkMP1Slot( - _get_mp1_slot_name(friendly_name, i), broadlink_device, i, parent_device + _get_mp1_slot_name(friendly_name, i), + broadlink_device, + i, + parent_device, + retry_times, ) switches.append(slot) @@ -146,7 +155,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): """Representation of an Broadlink switch.""" - def __init__(self, name, friendly_name, device, command_on, command_off): + def __init__( + self, name, friendly_name, device, command_on, command_off, retry_times + ): """Initialize the switch.""" self.entity_id = ENTITY_ID_FORMAT.format(slugify(name)) self._name = friendly_name @@ -155,6 +166,8 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): self._command_off = command_off self._device = device self._is_available = False + self._retry_times = retry_times + _LOGGER.debug("_retry_times : %s", self._retry_times) async def async_added_to_hass(self): """Call when entity about to be added to hass.""" @@ -190,17 +203,17 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): def turn_on(self, **kwargs): """Turn the device on.""" - if self._sendpacket(self._command_on): + if self._sendpacket(self._command_on, self._retry_times): self._state = True self.schedule_update_ha_state() def turn_off(self, **kwargs): """Turn the device off.""" - if self._sendpacket(self._command_off): + if self._sendpacket(self._command_off, self._retry_times): self._state = False self.schedule_update_ha_state() - def _sendpacket(self, packet, retry=2): + def _sendpacket(self, packet, retry): """Send packet to device.""" if packet is None: _LOGGER.debug("Empty packet") @@ -211,12 +224,13 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): if retry < 1: _LOGGER.error("Error during sending a packet: %s", error) return False - if not self._auth(): + if not self._auth(self._retry_times): return False return self._sendpacket(packet, retry - 1) return True - def _auth(self, retry=2): + def _auth(self, retry): + _LOGGER.debug("_auth : retry=%s", retry) try: auth = self._device.auth() except OSError: @@ -231,14 +245,14 @@ class BroadlinkRMSwitch(SwitchDevice, RestoreEntity): class BroadlinkSP1Switch(BroadlinkRMSwitch): """Representation of an Broadlink switch.""" - def __init__(self, friendly_name, device): + def __init__(self, friendly_name, device, retry_times): """Initialize the switch.""" - super().__init__(friendly_name, friendly_name, device, None, None) + super().__init__(friendly_name, friendly_name, device, None, None, retry_times) self._command_on = 1 self._command_off = 0 self._load_power = None - def _sendpacket(self, packet, retry=2): + def _sendpacket(self, packet, retry): """Send packet to device.""" try: self._device.set_power(packet) @@ -246,7 +260,7 @@ class BroadlinkSP1Switch(BroadlinkRMSwitch): if retry < 1: _LOGGER.error("Error during sending a packet: %s", error) return False - if not self._auth(): + if not self._auth(self._retry_times): return False return self._sendpacket(packet, retry - 1) return True @@ -275,10 +289,11 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): def update(self): """Synchronize state with switch.""" - self._update() + self._update(self._retry_times) - def _update(self, retry=2): + def _update(self, retry): """Update the state of the device.""" + _LOGGER.debug("_update : retry=%s", retry) try: state = self._device.check_power() load_power = self._device.get_energy() @@ -287,7 +302,7 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): _LOGGER.error("Error during updating the state: %s", error) self._is_available = False return - if not self._auth(): + if not self._auth(self._retry_times): return return self._update(retry - 1) if state is None and retry > 0: @@ -300,9 +315,9 @@ class BroadlinkSP2Switch(BroadlinkSP1Switch): class BroadlinkMP1Slot(BroadlinkRMSwitch): """Representation of a slot of Broadlink switch.""" - def __init__(self, friendly_name, device, slot, parent_device): + def __init__(self, friendly_name, device, slot, parent_device, retry_times): """Initialize the slot of switch.""" - super().__init__(friendly_name, friendly_name, device, None, None) + super().__init__(friendly_name, friendly_name, device, None, None, retry_times) self._command_on = 1 self._command_off = 0 self._slot = slot @@ -313,7 +328,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): """Return true if unable to access real state of entity.""" return False - def _sendpacket(self, packet, retry=2): + def _sendpacket(self, packet, retry): """Send packet to device.""" try: self._device.set_power(self._slot, packet) @@ -322,7 +337,7 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): _LOGGER.error("Error during sending a packet: %s", error) self._is_available = False return False - if not self._auth(): + if not self._auth(self._retry_times): return False return self._sendpacket(packet, max(0, retry - 1)) self._is_available = True @@ -337,15 +352,20 @@ class BroadlinkMP1Slot(BroadlinkRMSwitch): """Trigger update for all switches on the parent device.""" self._parent_device.update() self._state = self._parent_device.get_outlet_status(self._slot) + if self._state is None: + self._is_available = False + else: + self._is_available = True class BroadlinkMP1Switch: """Representation of a Broadlink switch - To fetch states of all slots.""" - def __init__(self, device): + def __init__(self, device, retry_times): """Initialize the switch.""" self._device = device self._states = None + self._retry_times = retry_times def get_outlet_status(self, slot): """Get status of outlet from cached status list.""" @@ -356,9 +376,9 @@ class BroadlinkMP1Switch: @Throttle(TIME_BETWEEN_UPDATES) def update(self): """Fetch new state data for this device.""" - self._update() + self._update(self._retry_times) - def _update(self, retry=2): + def _update(self, retry): """Update the state of the device.""" try: states = self._device.check_power() @@ -366,14 +386,14 @@ class BroadlinkMP1Switch: if retry < 1: _LOGGER.error("Error during updating the state: %s", error) return - if not self._auth(): + if not self._auth(self._retry_times): return return self._update(max(0, retry - 1)) if states is None and retry > 0: return self._update(max(0, retry - 1)) self._states = states - def _auth(self, retry=2): + def _auth(self, retry): """Authenticate the device.""" try: auth = self._device.auth() diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index e538b6b802a..32817242642 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -17,6 +17,9 @@ 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__) DOMAIN = "calendar" diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index efe7a37b310..597d67fcdee 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -52,6 +52,9 @@ from homeassistant.setup import async_when_setup from .const import DOMAIN, DATA_CAMERA_PREFS from .prefs import CameraPreferences + +# mypy: allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) SERVICE_ENABLE_MOTION = "enable_motion_detection" @@ -311,7 +314,7 @@ class Camera(Entity): """Initialize a camera.""" self.is_streaming = False self.content_type = DEFAULT_CONTENT_TYPE - self.access_tokens = collections.deque([], 2) + self.access_tokens: collections.deque = collections.deque([], 2) self.async_update_token() @property diff --git a/homeassistant/components/camera/prefs.py b/homeassistant/components/camera/prefs.py index 5e22b882d0a..d83e0b55c96 100644 --- a/homeassistant/components/camera/prefs.py +++ b/homeassistant/components/camera/prefs.py @@ -1,6 +1,9 @@ """Preference management for camera component.""" from .const import DOMAIN, PREF_PRELOAD_STREAM + +# mypy: allow-untyped-defs, no-check-untyped-defs + STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 _UNDEF = object() diff --git a/homeassistant/components/cast/.translations/hr.json b/homeassistant/components/cast/.translations/hr.json new file mode 100644 index 00000000000..91dafab0201 --- /dev/null +++ b/homeassistant/components/cast/.translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Google Cast" + } + }, + "title": "Google Cast" + } +} \ No newline at end of file diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 6c64d667254..af67be5eccc 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -70,7 +70,6 @@ from .const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE, ) -from .reproduce_state import async_reproduce_states # noqa DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 diff --git a/homeassistant/components/climate/const.py b/homeassistant/components/climate/const.py index ba13c03babd..4012aa8be1b 100644 --- a/homeassistant/components/climate/const.py +++ b/homeassistant/components/climate/const.py @@ -108,7 +108,7 @@ ATTR_TARGET_TEMP_STEP = "target_temp_step" DEFAULT_MIN_TEMP = 7 DEFAULT_MAX_TEMP = 35 -DEFAULT_MIN_HUMITIDY = 30 +DEFAULT_MIN_HUMIDITY = 30 DEFAULT_MAX_HUMIDITY = 99 DOMAIN = "climate" diff --git a/homeassistant/components/climate/reproduce_state.py b/homeassistant/components/climate/reproduce_state.py index 98f085c1e8d..34e72a27c92 100644 --- a/homeassistant/components/climate/reproduce_state.py +++ b/homeassistant/components/climate/reproduce_state.py @@ -5,7 +5,6 @@ from typing import Iterable, Optional from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.loader import bind_hass from .const import ( ATTR_AUX_HEAT, @@ -69,7 +68,6 @@ async def _async_reproduce_states( await call_service(SERVICE_SET_HUMIDITY, [ATTR_HUMIDITY]) -@bind_hass async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None ) -> None: diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 58739bededc..3daeac43da9 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Cloud", "documentation": "https://www.home-assistant.io/components/cloud", - "requirements": ["hass-nabucasa==0.16"], + "requirements": ["hass-nabucasa==0.17"], "dependencies": ["http", "webhook"], "codeowners": ["@home-assistant/cloud"] } diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index 140b5a2b270..b21991a8479 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,6 +1,10 @@ """Http views to control the config manager.""" +import aiohttp.web_exceptions +import voluptuous as vol + from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES +from homeassistant.components import websocket_api from homeassistant.components.http import HomeAssistantView from homeassistant.exceptions import Unauthorized from homeassistant.helpers.data_entry_flow import ( @@ -17,12 +21,18 @@ async def async_setup(hass): hass.http.register_view(ConfigManagerFlowIndexView(hass.config_entries.flow)) 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.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) + return True @@ -54,8 +64,18 @@ class ConfigManagerEntryIndexView(HomeAssistantView): """List available config entries.""" hass = request.app["hass"] - return self.json( - [ + results = [] + + for entry in hass.config_entries.async_entries(): + handler = config_entries.HANDLERS.get(entry.domain) + supports_options = ( + # Guard in case handler is no longer registered (custom compnoent etc) + handler is not None + # pylint: disable=comparison-with-callable + and handler.async_get_options_flow + != config_entries.ConfigFlow.async_get_options_flow + ) + results.append( { "entry_id": entry.entry_id, "domain": entry.domain, @@ -63,14 +83,11 @@ class ConfigManagerEntryIndexView(HomeAssistantView): "source": entry.source, "state": entry.state, "connection_class": entry.connection_class, - "supports_options": hasattr( - config_entries.HANDLERS.get(entry.domain), - "async_get_options_flow", - ), + "supports_options": supports_options, } - for entry in hass.config_entries.async_entries() - ] - ) + ) + + return self.json(results) class ConfigManagerEntryResourceView(HomeAssistantView): @@ -101,23 +118,8 @@ class ConfigManagerFlowIndexView(FlowManagerIndexView): name = "api:config:config_entries:flow" async def get(self, request): - """List flows that are in progress but not started by a user. - - Example of a non-user initiated flow is a discovered Hue hub that - requires user interaction to finish setup. - """ - if not request["hass_user"].is_admin: - raise Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add") - - hass = request.app["hass"] - - return self.json( - [ - flw - for flw in hass.config_entries.flow.async_progress() - if flw["context"]["source"] != config_entries.SOURCE_USER - ] - ) + """Not implemented.""" + raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"]) # pylint: disable=arguments-differ async def post(self, request): @@ -187,8 +189,8 @@ class ConfigManagerAvailableFlowView(HomeAssistantView): class OptionManagerFlowIndexView(FlowManagerIndexView): """View to create option flows.""" - url = "/api/config/config_entries/entry/option/flow" - name = "api:config:config_entries:entry:resource:option:flow" + url = "/api/config/config_entries/options/flow" + name = "api:config:config_entries:option:flow" # pylint: disable=arguments-differ async def post(self, request): @@ -224,3 +226,62 @@ class OptionManagerFlowResourceView(FlowManagerResourceView): # pylint: disable=no-value-for-parameter return await super().post(request, flow_id) + + +@websocket_api.require_admin +@websocket_api.websocket_command({"type": "config_entries/flow/progress"}) +def config_entries_progress(hass, connection, msg): + """List flows that are in progress but not started by a user. + + Example of a non-user initiated flow is a discovered Hue hub that + requires user interaction to finish setup. + """ + connection.send_result( + msg["id"], + [ + flw + for flw in hass.config_entries.flow.async_progress() + if flw["context"]["source"] != config_entries.SOURCE_USER + ], + ) + + +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + {"type": "config_entries/system_options/list", "entry_id": str} +) +async def system_options_list(hass, connection, msg): + """List all system options for a config entry.""" + entry_id = msg["entry_id"] + entry = hass.config_entries.async_get_entry(entry_id) + + if entry: + 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/system_options/update", + "entry_id": str, + vol.Optional("disable_new_entities"): bool, + } +) +async def system_options_update(hass, connection, msg): + """Update config entry system options.""" + changes = dict(msg) + changes.pop("id") + changes.pop("type") + entry_id = changes.pop("entry_id") + entry = hass.config_entries.async_get_entry(entry_id) + + if entry is None: + connection.send_error( + msg["id"], websocket_api.const.ERR_NOT_FOUND, "Config entry not found" + ) + return + + hass.config_entries.async_update_entry(entry, system_options=changes) + connection.send_result(msg["id"], entry.system_options.as_dict()) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 431723893c1..125b2260f08 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -11,51 +11,18 @@ from homeassistant.components.websocket_api.decorators import ( ) from homeassistant.helpers import config_validation as cv -WS_TYPE_LIST = "config/entity_registry/list" -SCHEMA_WS_LIST = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_LIST} -) - -WS_TYPE_GET = "config/entity_registry/get" -SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET, vol.Required("entity_id"): cv.entity_id} -) - -WS_TYPE_UPDATE = "config/entity_registry/update" -SCHEMA_WS_UPDATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - { - vol.Required("type"): WS_TYPE_UPDATE, - vol.Required("entity_id"): cv.entity_id, - # If passed in, we update value. Passing None will remove old value. - vol.Optional("name"): vol.Any(str, None), - vol.Optional("new_entity_id"): str, - } -) - -WS_TYPE_REMOVE = "config/entity_registry/remove" -SCHEMA_WS_REMOVE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_REMOVE, vol.Required("entity_id"): cv.entity_id} -) - async def async_setup(hass): """Enable the Entity Registry views.""" - hass.components.websocket_api.async_register_command( - WS_TYPE_LIST, websocket_list_entities, SCHEMA_WS_LIST - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET, websocket_get_entity, SCHEMA_WS_GET - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_UPDATE, websocket_update_entity, SCHEMA_WS_UPDATE - ) - hass.components.websocket_api.async_register_command( - WS_TYPE_REMOVE, websocket_remove_entity, SCHEMA_WS_REMOVE - ) + hass.components.websocket_api.async_register_command(websocket_list_entities) + hass.components.websocket_api.async_register_command(websocket_get_entity) + hass.components.websocket_api.async_register_command(websocket_update_entity) + hass.components.websocket_api.async_register_command(websocket_remove_entity) return True @async_response +@websocket_api.websocket_command({vol.Required("type"): "config/entity_registry/list"}) async def websocket_list_entities(hass, connection, msg): """Handle list registry entries command. @@ -70,6 +37,12 @@ async def websocket_list_entities(hass, connection, msg): @async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/get", + vol.Required("entity_id"): cv.entity_id, + } +) async def websocket_get_entity(hass, connection, msg): """Handle get entity registry entry command. @@ -89,6 +62,17 @@ async def websocket_get_entity(hass, connection, msg): @require_admin @async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/update", + vol.Required("entity_id"): cv.entity_id, + # If passed in, we update value. Passing None will remove old value. + vol.Optional("name"): vol.Any(str, None), + vol.Optional("new_entity_id"): str, + # We only allow setting disabled_by user via API. + vol.Optional("disabled_by"): vol.Any("user", None), + } +) async def websocket_update_entity(hass, connection, msg): """Handle update entity websocket command. @@ -107,6 +91,9 @@ async def websocket_update_entity(hass, connection, msg): if "name" in msg: changes["name"] = msg["name"] + if "disabled_by" in msg: + changes["disabled_by"] = msg["disabled_by"] + if "new_entity_id" in msg and msg["new_entity_id"] != msg["entity_id"]: changes["new_entity_id"] = msg["new_entity_id"] if hass.states.get(msg["new_entity_id"]) is not None: @@ -132,6 +119,12 @@ async def websocket_update_entity(hass, connection, msg): @require_admin @async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "config/entity_registry/remove", + vol.Required("entity_id"): cv.entity_id, + } +) async def websocket_remove_entity(hass, connection, msg): """Handle remove entity websocket command. diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 7379d66777b..8a319c655f6 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -7,7 +7,7 @@ import voluptuous as vol from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( HVAC_MODE_OFF, - HVAC_MODE_AUTO, + HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -33,14 +33,14 @@ AVAILABLE_MODES = [ HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_DRY, - HVAC_MODE_AUTO, + HVAC_MODE_HEAT_COOL, HVAC_MODE_FAN_ONLY, ] CM_TO_HA_STATE = { "heat": HVAC_MODE_HEAT, "cool": HVAC_MODE_COOL, - "auto": HVAC_MODE_AUTO, + "auto": HVAC_MODE_HEAT_COOL, "dry": HVAC_MODE_DRY, "fan": HVAC_MODE_FAN_ONLY, } diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 696524f5792..d491765bb00 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -32,6 +32,9 @@ from homeassistant.const import ( STATE_CLOSING, ) + +# mypy: allow-untyped-calls, allow-incomplete-defs, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "cover" diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json index 4ae633ef165..1a5d992ef7b 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -2,7 +2,9 @@ "config": { "abort": { "already_configured": "El Bridge ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en progreso.", "no_bridges": "No se descubrieron puentes deCONZ", + "not_deconz_bridge": "No es un puente deCONZ", "one_instance_only": "El componente solo admite una instancia deCONZ" }, "error": { @@ -13,7 +15,8 @@ "data": { "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" - } + }, + "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?" }, "init": { "data": { @@ -23,6 +26,7 @@ "title": "Definir el gateway deCONZ" }, "link": { + "description": "Desbloquee su puerta de enlace deCONZ para registrarse con Home Assistant. \n\n 1. Vaya a Configuraci\u00f3n deCONZ - > Gateway - > Avanzado \n 2. Presione el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", "title": "Enlazar con deCONZ" }, "options": { diff --git a/homeassistant/components/deconz/.translations/hr.json b/homeassistant/components/deconz/.translations/hr.json new file mode 100644 index 00000000000..2f2eb6df214 --- /dev/null +++ b/homeassistant/components/deconz/.translations/hr.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "init": { + "data": { + "host": "Host", + "port": "Port" + } + }, + "options": { + "data": { + "allow_clip_sensor": "Dopusti uvoz virtualnih senzora" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index fbe422bf927..0173c90c3b7 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -25,10 +25,10 @@ "host": "\u4e3b\u6a5f\u7aef", "port": "\u901a\u8a0a\u57e0" }, - "title": "\u5b9a\u7fa9 deCONZ \u7db2\u95dc" + "title": "\u5b9a\u7fa9 deCONZ \u9598\u9053\u5668" }, "link": { - "description": "\u89e3\u9664 deCONZ \u7db2\u95dc\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u7db2\u95dc -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", + "description": "\u89e3\u9664 deCONZ \u9598\u9053\u5668\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u9598\u9053\u5668 -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", "title": "\u9023\u7d50\u81f3 deCONZ" }, "options": { @@ -39,6 +39,6 @@ "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } }, - "title": "deCONZ Zigbee \u7db2\u95dc" + "title": "deCONZ Zigbee \u9598\u9053\u5668" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index f4b8d3ebe02..650c0285750 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -1,5 +1,6 @@ """Config flow to configure deCONZ component.""" import asyncio +from copy import copy import async_timeout import voluptuous as vol @@ -12,7 +13,13 @@ 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 CONF_BRIDGEID, DEFAULT_PORT, DOMAIN +from .const import ( + CONF_ALLOW_CLIP_SENSOR, + CONF_ALLOW_DECONZ_GROUPS, + CONF_BRIDGEID, + DEFAULT_PORT, + DOMAIN, +) DECONZ_MANUFACTURERURL = "http://www.dresden-elektronik.de" CONF_SERIAL = "serial" @@ -45,6 +52,12 @@ class DeconzFlowHandler(config_entries.ConfigFlow): _hassio_discovery = None + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return DeconzOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the deCONZ config flow.""" self.bridges = [] @@ -234,3 +247,41 @@ class DeconzFlowHandler(config_entries.ConfigFlow): step_id="hassio_confirm", description_placeholders={"addon": self._hassio_discovery["addon"]}, ) + + +class DeconzOptionsFlowHandler(config_entries.OptionsFlow): + """Handle deCONZ options.""" + + def __init__(self, config_entry): + """Initialize deCONZ options flow.""" + self.config_entry = config_entry + self.options = copy(config_entry.options) + + async def async_step_init(self, user_input=None): + """Manage the deCONZ options.""" + return await self.async_step_deconz_devices() + + async def async_step_deconz_devices(self, user_input=None): + """Manage the deconz devices options.""" + if user_input is not None: + self.options[CONF_ALLOW_CLIP_SENSOR] = user_input[CONF_ALLOW_CLIP_SENSOR] + self.options[CONF_ALLOW_DECONZ_GROUPS] = user_input[ + CONF_ALLOW_DECONZ_GROUPS + ] + return self.async_create_entry(title="", data=self.options) + + return self.async_show_form( + step_id="deconz_devices", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALLOW_CLIP_SENSOR, + default=self.config_entry.options[CONF_ALLOW_CLIP_SENSOR], + ): bool, + vol.Optional( + CONF_ALLOW_DECONZ_GROUPS, + default=self.config_entry.options[CONF_ALLOW_DECONZ_GROUPS], + ): bool, + } + ), + ) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index caa46e10f99..be4088a5c86 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -14,8 +14,6 @@ from .const import COVER_TYPES, DAMPERS, NEW_LIGHT, WINDOW_COVERS from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry -ZIGBEE_SPEC = ["lumi.curtain"] - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Old way of setting up deCONZ platforms.""" @@ -35,13 +33,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for light in lights: - if light.type in COVER_TYPES: - if light.modelid in ZIGBEE_SPEC: - entities.append(DeconzCoverZigbeeSpec(light, gateway)) - - else: - entities.append(DeconzCover(light, gateway)) + entities.append(DeconzCover(light, gateway)) async_add_entities(entities, True) @@ -69,14 +62,12 @@ class DeconzCover(DeconzDevice, CoverDevice): @property def current_cover_position(self): """Return the current position of the cover.""" - if self.is_closed: - return 0 - return int(self._device.brightness / 255 * 100) + return 100 - int(self._device.brightness / 255 * 100) @property def is_closed(self): """Return if the cover is closed.""" - return not self._device.state + return self._device.state @property def device_class(self): @@ -96,9 +87,9 @@ class DeconzCover(DeconzDevice, CoverDevice): position = kwargs[ATTR_POSITION] data = {"on": False} - if position > 0: + if position < 100: data["on"] = True - data["bri"] = int(position / 100 * 255) + data["bri"] = 255 - int(position / 100 * 255) await self._device.async_set_state(data) @@ -116,28 +107,3 @@ class DeconzCover(DeconzDevice, CoverDevice): """Stop cover.""" data = {"bri_inc": 0} await self._device.async_set_state(data) - - -class DeconzCoverZigbeeSpec(DeconzCover): - """Zigbee spec is the inverse of how deCONZ normally reports attributes.""" - - @property - def current_cover_position(self): - """Return the current position of the cover.""" - return 100 - int(self._device.brightness / 255 * 100) - - @property - def is_closed(self): - """Return if the cover is closed.""" - return self._device.state - - async def async_set_cover_position(self, **kwargs): - """Move the cover to a specific position.""" - position = kwargs[ATTR_POSITION] - data = {"on": False} - - if position < 100: - data["on"] = True - data["bri"] = 255 - int(position / 100 * 255) - - await self._device.async_set_state(data) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index d1c70793063..ea9ea280515 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -40,5 +40,16 @@ "one_instance_only": "Component only supports one deCONZ instance", "updated_instance": "Updated deCONZ instance with new host address" } + }, + "options": { + "step": { + "async_step_deconz_devices": { + "description": "Configure visibility of deCONZ device types", + "data": { + "allow_clip_sensor": "Allow deCONZ CLIP sensors", + "allow_deconz_groups": "Allow deCONZ light groups" + } + } + } } } diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index e704ef6556f..2cd238d0787 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -50,7 +50,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) await line.get_passages() if line.passages is None: - _LOGGER.warning("No data recieved from De Lijn") + _LOGGER.warning("No data received from De Lijn") return sensors.append(DeLijnPublicTransportSensor(line, name)) @@ -71,7 +71,7 @@ class DeLijnPublicTransportSensor(Entity): """Get the latest data from the De Lijn API.""" await self.line.get_passages() if self.line.passages is None: - _LOGGER.warning("No data recieved from De Lijn") + _LOGGER.warning("No data received from De Lijn") return try: first = self.line.passages[0] diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index f8b61167ef7..967b7852c6f 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -176,8 +176,15 @@ async def async_setup(hass, config): async def finish_setup(hass, config): """Finish set up once demo platforms are set up.""" - lights = sorted(hass.states.async_entity_ids("light")) - switches = sorted(hass.states.async_entity_ids("switch")) + switches = None + lights = None + + while not switches and not lights: + # Not all platforms might be loaded. + if switches is not None: + await asyncio.sleep(0) + switches = sorted(hass.states.async_entity_ids("switch")) + lights = sorted(hass.states.async_entity_ids("light")) # Set up history graph await bootstrap.async_setup_component( diff --git a/homeassistant/components/denon/media_player.py b/homeassistant/components/denon/media_player.py index d9ff52b47d0..7bed8423e8f 100644 --- a/homeassistant/components/denon/media_player.py +++ b/homeassistant/components/denon/media_player.py @@ -51,7 +51,7 @@ NORMAL_INPUTS = { "Dvd": "DVD", "Blue ray": "BD", "TV": "TV", - "Satelite / Cable": "SAT/CBL", + "Satellite / Cable": "SAT/CBL", "Game": "GAME", "Game2": "GAME2", "Video Aux": "V.AUX", diff --git a/homeassistant/components/dialogflow/.translations/pl.json b/homeassistant/components/dialogflow/.translations/pl.json index 3395b31b4c7..ee222c83b51 100644 --- a/homeassistant/components/dialogflow/.translations/pl.json +++ b/homeassistant/components/dialogflow/.translations/pl.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Wymagana jest tylko jedna instancja." }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Dialogflow Webhook]({twilio_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistant'a, musisz skonfigurowa\u0107 [Dialogflow Webhook]({dialogflow_url}). \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n - Typ zawarto\u015bci: application/json\n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." }, "step": { "user": { diff --git a/homeassistant/components/dialogflow/__init__.py b/homeassistant/components/dialogflow/__init__.py index 4f2876df296..45fee0f867e 100644 --- a/homeassistant/components/dialogflow/__init__.py +++ b/homeassistant/components/dialogflow/__init__.py @@ -17,6 +17,9 @@ SOURCE = "Home Assistant Dialogflow" CONFIG_SCHEMA = vol.Schema({DOMAIN: {}}, extra=vol.ALLOW_EXTRA) +V1 = 1 +V2 = 2 + class DialogFlowError(HomeAssistantError): """Raised when a DialogFlow error happens.""" @@ -84,23 +87,45 @@ async_remove_entry = config_entry_flow.webhook_async_remove_entry def dialogflow_error_response(message, error): """Return a response saying the error message.""" - dialogflow_response = DialogflowResponse(message["result"]["parameters"]) + api_version = get_api_version(message) + if api_version is V1: + parameters = message["result"]["parameters"] + elif api_version is V2: + parameters = message["queryResult"]["parameters"] + dialogflow_response = DialogflowResponse(parameters, api_version) dialogflow_response.add_speech(error) return dialogflow_response.as_dict() +def get_api_version(message): + """Get API version of Dialogflow message.""" + if message.get("id") is not None: + return V1 + if message.get("responseId") is not None: + return V2 + + async def async_handle_message(hass, message): """Handle a DialogFlow message.""" - req = message.get("result") - action_incomplete = req["actionIncomplete"] + _api_version = get_api_version(message) + if _api_version is V1: + _LOGGER.warning( + "Dialogflow V1 API will be removed on October 23, 2019. Please change your DialogFlow settings to use the V2 api" + ) + req = message.get("result") + action_incomplete = req.get("actionIncomplete", True) + if action_incomplete: + return - if action_incomplete: - return None + elif _api_version is V2: + req = message.get("queryResult") + if req.get("allRequiredParamsPresent", False) is False: + return action = req.get("action", "") parameters = req.get("parameters").copy() parameters["dialogflow_query"] = message - dialogflow_response = DialogflowResponse(parameters) + dialogflow_response = DialogflowResponse(parameters, _api_version) if action == "": raise DialogFlowError( @@ -123,10 +148,11 @@ async def async_handle_message(hass, message): class DialogflowResponse: """Help generating the response for Dialogflow.""" - def __init__(self, parameters): + def __init__(self, parameters, api_version): """Initialize the Dialogflow response.""" self.speech = None self.parameters = {} + self.api_version = api_version # Parameter names replace '.' and '-' for '_' for key, value in parameters.items(): underscored_key = key.replace(".", "_").replace("-", "_") @@ -143,4 +169,8 @@ class DialogflowResponse: def as_dict(self): """Return response in a Dialogflow valid dictionary.""" - return {"speech": self.speech, "displayText": self.speech, "source": SOURCE} + if self.api_version is V1: + return {"speech": self.speech, "displayText": self.speech, "source": SOURCE} + + if self.api_version is V2: + return {"fulfillmentText": self.speech, "source": SOURCE} diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 5aeb88e6399..82a81118dbd 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -63,6 +63,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ["Power Consumption", obis_ref.CURRENT_ELECTRICITY_USAGE], ["Power Production", obis_ref.CURRENT_ELECTRICITY_DELIVERY], ["Power Tariff", obis_ref.ELECTRICITY_ACTIVE_TARIFF], + ["Power Consumption (total)", obis_ref.ELECTRICITY_IMPORTED_TOTAL], ["Power Consumption (low)", obis_ref.ELECTRICITY_USED_TARIFF_1], ["Power Consumption (normal)", obis_ref.ELECTRICITY_USED_TARIFF_2], ["Power Production (low)", obis_ref.ELECTRICITY_DELIVERED_TARIFF_1], diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index d9af0f93e11..181f1561eba 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -304,7 +304,7 @@ class Thermostat(ClimateDevice): self.vacation = event["name"] return PRESET_VACATION - return None + return self._preset_modes[self.thermostat["program"]["currentClimateRef"]] @property def hvac_mode(self): @@ -357,6 +357,9 @@ class Thermostat(ClimateDevice): status = self.thermostat["equipmentStatus"] return { "fan": self.fan, + "climate_mode": self._preset_modes[ + self.thermostat["program"]["currentClimateRef"] + ], "equipment_running": status, "fan_min_on_time": self.thermostat["settings"]["fanMinOnTime"], } diff --git a/homeassistant/components/edp_redy/__init__.py b/homeassistant/components/edp_redy/__init__.py deleted file mode 100644 index 8c079078176..00000000000 --- a/homeassistant/components/edp_redy/__init__.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Support for EDP re:dy.""" -from datetime import timedelta -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_START -from homeassistant.core import callback -from homeassistant.helpers import aiohttp_client, discovery, dispatcher -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_time -from homeassistant.util import dt as dt_util - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "edp_redy" -EDP_REDY = "edp_redy" -DATA_UPDATE_TOPIC = "{0}_data_update".format(DOMAIN) -UPDATE_INTERVAL = 60 - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the EDP re:dy component.""" - from edp_redy import EdpRedySession - - session = EdpRedySession( - config[DOMAIN][CONF_USERNAME], - config[DOMAIN][CONF_PASSWORD], - aiohttp_client.async_get_clientsession(hass), - hass.loop, - ) - hass.data[EDP_REDY] = session - platform_loaded = False - - async def async_update_and_sched(time): - update_success = await session.async_update() - - if update_success: - nonlocal platform_loaded - # pylint: disable=used-before-assignment - if not platform_loaded: - for component in ["sensor", "switch"]: - await discovery.async_load_platform( - hass, component, DOMAIN, {}, config - ) - platform_loaded = True - - dispatcher.async_dispatcher_send(hass, DATA_UPDATE_TOPIC) - - # schedule next update - async_track_point_in_time( - hass, async_update_and_sched, time + timedelta(seconds=UPDATE_INTERVAL) - ) - - async def start_component(event): - _LOGGER.debug("Starting updates") - await async_update_and_sched(dt_util.utcnow()) - - # only start fetching data after HA boots to prevent delaying the boot - # process - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_component) - - return True - - -class EdpRedyDevice(Entity): - """Representation a base re:dy device.""" - - def __init__(self, session, device_id, name): - """Initialize the device.""" - self._session = session - self._state = None - self._is_available = True - self._device_state_attributes = {} - self._id = device_id - self._unique_id = device_id - self._name = name if name else device_id - - async def async_added_to_hass(self): - """Subscribe to the data updates topic.""" - dispatcher.async_dispatcher_connect( - self.hass, DATA_UPDATE_TOPIC, self._data_updated - ) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id - - @property - def available(self): - """Return True if entity is available.""" - return self._is_available - - @property - def should_poll(self): - """Return the polling state. No polling needed.""" - return False - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._device_state_attributes - - @callback - def _data_updated(self): - """Update state, trigger updates.""" - self.async_schedule_update_ha_state(True) - - def _parse_data(self, data): - """Parse data received from the server.""" - if "OutOfOrder" in data: - try: - self._is_available = not data["OutOfOrder"] - except ValueError: - _LOGGER.error("Could not parse OutOfOrder for %s", self._id) - self._is_available = False diff --git a/homeassistant/components/edp_redy/manifest.json b/homeassistant/components/edp_redy/manifest.json deleted file mode 100644 index 90404b21678..00000000000 --- a/homeassistant/components/edp_redy/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "edp_redy", - "name": "Edp redy", - "documentation": "https://www.home-assistant.io/components/edp_redy", - "requirements": [ - "edp_redy==0.0.3" - ], - "dependencies": [], - "codeowners": [ - "@abmantis" - ] -} diff --git a/homeassistant/components/edp_redy/sensor.py b/homeassistant/components/edp_redy/sensor.py deleted file mode 100644 index f8fffefb5da..00000000000 --- a/homeassistant/components/edp_redy/sensor.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Support for EDP re:dy sensors.""" -import logging - -from homeassistant.const import POWER_WATT -from homeassistant.helpers.entity import Entity - -from . import EDP_REDY, EdpRedyDevice - -_LOGGER = logging.getLogger(__name__) - -# Load power in watts (W) -ATTR_ACTIVE_POWER = "active_power" - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Perform the setup for re:dy devices.""" - from edp_redy.session import ACTIVE_POWER_ID - - session = hass.data[EDP_REDY] - devices = [] - - # Create sensors for modules - for device_json in session.modules_dict.values(): - if "HA_POWER_METER" not in device_json["Capabilities"]: - continue - devices.append(EdpRedyModuleSensor(session, device_json)) - - # Create a sensor for global active power - devices.append( - EdpRedySensor(session, ACTIVE_POWER_ID, "Power Home", "mdi:flash", POWER_WATT) - ) - - async_add_entities(devices, True) - - -class EdpRedySensor(EdpRedyDevice, Entity): - """Representation of a EDP re:dy generic sensor.""" - - def __init__(self, session, sensor_id, name, icon, unit): - """Initialize the sensor.""" - super().__init__(session, sensor_id, name) - - self._icon = icon - self._unit = unit - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return self._icon - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this sensor.""" - return self._unit - - async def async_update(self): - """Parse the data for this sensor.""" - if self._id in self._session.values_dict: - self._state = self._session.values_dict[self._id] - self._is_available = True - else: - self._is_available = False - - -class EdpRedyModuleSensor(EdpRedyDevice, Entity): - """Representation of a EDP re:dy module sensor.""" - - def __init__(self, session, device_json): - """Initialize the sensor.""" - super().__init__( - session, device_json["PKID"], "Power {0}".format(device_json["Name"]) - ) - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:flash" - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this sensor.""" - return POWER_WATT - - async def async_update(self): - """Parse the data for this sensor.""" - if self._id in self._session.modules_dict: - device_json = self._session.modules_dict[self._id] - self._parse_data(device_json) - else: - self._is_available = False - - def _parse_data(self, data): - """Parse data received from the server.""" - super()._parse_data(data) - - _LOGGER.debug("Sensor data: %s", str(data)) - - for state_var in data["StateVars"]: - if state_var["Name"] == "ActivePower": - try: - self._state = float(state_var["Value"]) * 1000 - except ValueError: - _LOGGER.error("Could not parse power for %s", self._id) - self._state = 0 - self._is_available = False diff --git a/homeassistant/components/edp_redy/switch.py b/homeassistant/components/edp_redy/switch.py deleted file mode 100644 index 18078fab537..00000000000 --- a/homeassistant/components/edp_redy/switch.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Support for EDP re:dy plugs/switches.""" -import logging - -from homeassistant.components.switch import SwitchDevice - -from . import EDP_REDY, EdpRedyDevice - -_LOGGER = logging.getLogger(__name__) - -# Load power in watts (W) -ATTR_ACTIVE_POWER = "active_power" - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Perform the setup for re:dy devices.""" - session = hass.data[EDP_REDY] - devices = [] - for device_json in session.modules_dict.values(): - if "HA_SWITCH" not in device_json["Capabilities"]: - continue - devices.append(EdpRedySwitch(session, device_json)) - - async_add_entities(devices, True) - - -class EdpRedySwitch(EdpRedyDevice, SwitchDevice): - """Representation of a Edp re:dy switch (plugs, switches, etc).""" - - def __init__(self, session, device_json): - """Initialize the switch.""" - super().__init__(session, device_json["PKID"], device_json["Name"]) - - self._active_power = None - - @property - def icon(self): - """Return the icon to use in the frontend.""" - return "mdi:power-plug" - - @property - def is_on(self): - """Return true if it is on.""" - return self._state - - @property - def device_state_attributes(self): - """Return the state attributes.""" - if self._active_power is not None: - attrs = {ATTR_ACTIVE_POWER: self._active_power} - else: - attrs = {} - attrs.update(super().device_state_attributes) - return attrs - - async def async_turn_on(self, **kwargs): - """Turn the switch on.""" - if await self._async_send_state_cmd(True): - self._state = True - self.async_schedule_update_ha_state() - - async def async_turn_off(self, **kwargs): - """Turn the switch off.""" - if await self._async_send_state_cmd(False): - self._state = False - self.async_schedule_update_ha_state() - - async def _async_send_state_cmd(self, state): - state_json = {"devModuleId": self._id, "key": "RelayState", "value": state} - return await self._session.async_set_state_var(state_json) - - async def async_update(self): - """Parse the data for this switch.""" - if self._id in self._session.modules_dict: - device_json = self._session.modules_dict[self._id] - self._parse_data(device_json) - else: - self._is_available = False - - def _parse_data(self, data): - """Parse data received from the server.""" - super()._parse_data(data) - - for state_var in data["StateVars"]: - if state_var["Name"] == "RelayState": - self._state = state_var["Value"] == "true" - elif state_var["Name"] == "ActivePower": - try: - self._active_power = float(state_var["Value"]) * 1000 - except ValueError: - _LOGGER.error("Could not parse power for %s", self._id) - self._active_power = None diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index b9e3ecaf093..791085b46f3 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -1,4 +1,4 @@ -"""Support for local control of entities by emulating a Phillips Hue bridge.""" +"""Support for local control of entities by emulating a Philips Hue bridge.""" import logging from aiohttp import web diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 1b08b43c9af..fc00746fc7f 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -562,17 +562,27 @@ def get_entity_state(config, entity): def entity_to_json(config, entity, state): """Convert an entity to its Hue bridge JSON representation.""" + entity_features = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if (entity_features & SUPPORT_BRIGHTNESS) or entity.domain != light.DOMAIN: + return { + "state": { + HUE_API_STATE_ON: state[STATE_ON], + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + HUE_API_STATE_HUE: state[STATE_HUE], + HUE_API_STATE_SAT: state[STATE_SATURATION], + "reachable": True, + }, + "type": "Dimmable light", + "name": config.get_entity_name(entity), + "modelid": "HASS123", + "uniqueid": entity.entity_id, + "swversion": "123", + } return { - "state": { - HUE_API_STATE_ON: state[STATE_ON], - HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], - HUE_API_STATE_HUE: state[STATE_HUE], - HUE_API_STATE_SAT: state[STATE_SATURATION], - "reachable": True, - }, - "type": "Dimmable light", + "state": {HUE_API_STATE_ON: state[STATE_ON], "reachable": True}, + "type": "On/off light", "name": config.get_entity_name(entity), - "modelid": "HASS123", + "modelid": "HASS321", "uniqueid": entity.entity_id, "swversion": "123", } diff --git a/homeassistant/components/environment_canada/camera.py b/homeassistant/components/environment_canada/camera.py index 709e4251fbf..2a23fb95a18 100755 --- a/homeassistant/components/environment_canada/camera.py +++ b/homeassistant/components/environment_canada/camera.py @@ -23,6 +23,7 @@ _LOGGER = logging.getLogger(__name__) ATTR_STATION = "station" ATTR_LOCATION = "location" +ATTR_UPDATED = "updated" CONF_ATTRIBUTION = "Data provided by Environment Canada" CONF_STATION = "station" @@ -35,7 +36,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_LOOP, default=True): cv.boolean, vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_STATION): cv.string, + vol.Optional(CONF_STATION): cv.matches_regex(r"^C[A-Z]{4}$|^[A-Z]{3}$"), vol.Inclusive(CONF_LATITUDE, "latlon"): cv.latitude, vol.Inclusive(CONF_LONGITUDE, "latlon"): cv.longitude, vol.Optional(CONF_PRECIP_TYPE): ["RAIN", "SNOW"], @@ -70,6 +71,7 @@ class ECCamera(Camera): self.camera_name = camera_name self.content_type = "image/gif" self.image = None + self.timestamp = None def camera_image(self): """Return bytes of camera image.""" @@ -90,6 +92,7 @@ class ECCamera(Camera): ATTR_ATTRIBUTION: CONF_ATTRIBUTION, ATTR_LOCATION: self.radar_object.station_name, ATTR_STATION: self.radar_object.station_code, + ATTR_UPDATED: self.timestamp, } return attr @@ -101,3 +104,4 @@ class ECCamera(Camera): self.image = self.radar_object.get_loop() else: self.image = self.radar_object.get_latest_frame() + self.timestamp = self.radar_object.timestamp.isoformat() diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 85df0495428..0625fd4c27f 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,7 +3,7 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/components/environment_canada", "requirements": [ - "env_canada==0.0.20" + "env_canada==0.0.24" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/environment_canada/sensor.py b/homeassistant/components/environment_canada/sensor.py index 0182e7c67ed..2413edaebce 100755 --- a/homeassistant/components/environment_canada/sensor.py +++ b/homeassistant/components/environment_canada/sensor.py @@ -133,10 +133,15 @@ class ECSensor(Entity): ATTR_TIME: " | ".join([str(s.get("date")) for s in value]), } ) + elif self.sensor_type == "tendency": + self._state = str(value).capitalize() else: self._state = value - if sensor_data.get("unit") == "C": + if sensor_data.get("unit") == "C" or self.sensor_type in [ + "wind_chill", + "humidex", + ]: self._unit = TEMP_CELSIUS else: self._unit = sensor_data.get("unit") diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index 219578c34be..8499a0de5a0 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -91,7 +91,7 @@ class EQ3BTSmartThermostat(ClimateDevice): @property def available(self) -> bool: """Return if thermostat is available.""" - return self._thermostat.mode > 0 + return self._thermostat.mode >= 0 @property def name(self): diff --git a/homeassistant/components/esphome/.translations/es-419.json b/homeassistant/components/esphome/.translations/es-419.json index 58dbba34fa8..a0a2d77d48c 100644 --- a/homeassistant/components/esphome/.translations/es-419.json +++ b/homeassistant/components/esphome/.translations/es-419.json @@ -8,6 +8,7 @@ "invalid_password": "\u00a1Contrase\u00f1a invalida!", "resolve_error": "No se puede resolver la direcci\u00f3n de la ESP. Si este error persiste, configure una direcci\u00f3n IP est\u00e1tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { @@ -17,6 +18,7 @@ "title": "Escriba la contrase\u00f1a" }, "discovery_confirm": { + "description": "\u00bfDesea agregar el nodo ESPHome `{name}` a Home Assistant?", "title": "Nodo ESPHome descubierto" }, "user": { diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json index 41313cb44a9..aeb3b48311e 100644 --- a/homeassistant/components/essent/manifest.json +++ b/homeassistant/components/essent/manifest.json @@ -2,7 +2,7 @@ "domain": "essent", "name": "Essent", "documentation": "https://www.home-assistant.io/components/essent", - "requirements": ["PyEssent==0.12"], + "requirements": ["PyEssent==0.13"], "dependencies": [], "codeowners": ["@TheLastProject"] } diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index f0e7a26e1f5..05308782362 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -273,10 +273,10 @@ class EvoBroker: else: self.timers["statusUpdated"] = utcnow() - _LOGGER.debug("Status = %s", status) + _LOGGER.debug("Status = %s", status) - # inform the evohome devices that state data has been updated - async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) + # inform the evohome devices that state data has been updated + async_dispatcher_send(self.hass, DOMAIN, {"signal": "refresh"}) class EvoDevice(Entity): diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 235e8cf5fad..f5edfe5bb59 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import functools as ft import logging +from typing import Optional import voluptuous as vol @@ -74,7 +75,7 @@ FAN_SET_DIRECTION_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( @bind_hass -def is_on(hass, entity_id: str = None) -> bool: +def is_on(hass, entity_id: Optional[str] = None) -> 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) @@ -149,12 +150,12 @@ class FanEntity(ToggleEntity): return self.hass.async_add_job(self.set_direction, direction) # pylint: disable=arguments-differ - def turn_on(self, speed: str = None, **kwargs) -> None: + def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: """Turn on the fan.""" raise NotImplementedError() # pylint: disable=arguments-differ - def async_turn_on(self, speed: str = None, **kwargs): + def async_turn_on(self, speed: Optional[str] = None, **kwargs): """Turn on the fan. This method must be run in the event loop and returns a coroutine. @@ -180,7 +181,7 @@ class FanEntity(ToggleEntity): return self.speed not in [SPEED_OFF, None] @property - def speed(self) -> str: + def speed(self) -> Optional[str]: """Return the current speed.""" return None @@ -190,7 +191,7 @@ class FanEntity(ToggleEntity): return [] @property - def current_direction(self) -> str: + def current_direction(self) -> Optional[str]: """Return the current direction of the fan.""" return None diff --git a/homeassistant/components/fedex/sensor.py b/homeassistant/components/fedex/sensor.py index a18d6a4c651..2f499e52e23 100644 --- a/homeassistant/components/fedex/sensor.py +++ b/homeassistant/components/fedex/sensor.py @@ -44,6 +44,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Fedex platform.""" import fedexdeliverymanager + _LOGGER.warning( + "The fedex integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + name = config.get(CONF_NAME) update_interval = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL) diff --git a/homeassistant/components/fibaro/climate.py b/homeassistant/components/fibaro/climate.py index 4670a3f0c62..ed399fac209 100644 --- a/homeassistant/components/fibaro/climate.py +++ b/homeassistant/components/fibaro/climate.py @@ -50,7 +50,7 @@ HA_FANMODES = {v: k for k, v in FANMODES.items()} # SDS13781-10 Z-Wave Application Command Class Specification 2019-01-04 # Table 130, Thermostat Mode Set version 3::Mode encoding. -# 4 AUXILARY +# 4 AUXILIARY OPMODES_PRESET = { 5: PRESET_RESUME, 7: PRESET_FURNACE, diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index b1ce967d6cd..2a8798d3729 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -6,6 +6,7 @@ from numbers import Number from functools import partial from copy import copy from datetime import timedelta +from typing import Optional import voluptuous as vol @@ -241,7 +242,8 @@ class SensorFilter(Entity): entity_id=self._entity, ) ) - history_list.extend([state for state in filter_history[self._entity]]) + if self._entity in filter_history: + history_list.extend(filter_history[self._entity]) if largest_window_time > timedelta(seconds=0): start = dt_util.utcnow() - largest_window_time filter_history = await self.hass.async_add_job( @@ -252,13 +254,14 @@ class SensorFilter(Entity): entity_id=self._entity, ) ) - history_list.extend( - [ - state - for state in filter_history[self._entity] - if state not in history_list - ] - ) + if self._entity in filter_history: + history_list.extend( + [ + state + for state in filter_history[self._entity] + if state not in history_list + ] + ) # Sort the window states history_list = sorted(history_list, key=lambda s: s.last_updated) @@ -333,17 +336,21 @@ class FilterState: class Filter: - """Filter skeleton. + """Filter skeleton.""" - Args: - window_size (int): size of the sliding window that holds previous - values - precision (int): round filtered value to precision value - entity (string): used for debugging only - """ + def __init__( + self, + name, + window_size: int = 1, + precision: Optional[int] = None, + entity: Optional[str] = None, + ): + """Initialize common attributes. - def __init__(self, name, window_size=1, precision=None, entity=None): - """Initialize common attributes.""" + :param window_size: size of the sliding window that holds previous values + :param precision: round filtered value to precision value + :param entity: used for debugging only + """ if isinstance(window_size, int): self.states = deque(maxlen=window_size) self.window_unit = WINDOW_SIZE_UNIT_NUMBER_EVENTS @@ -394,14 +401,19 @@ class RangeFilter(Filter): Determines if new state is in the range of upper_bound and lower_bound. If not inside, lower or upper bound is returned instead. - - Args: - upper_bound (float): band upper bound - lower_bound (float): band lower bound """ - def __init__(self, entity, lower_bound=None, upper_bound=None): - """Initialize Filter.""" + def __init__( + self, + entity, + lower_bound: Optional[float] = None, + upper_bound: Optional[float] = None, + ): + """Initialize Filter. + + :param upper_bound: band upper bound + :param lower_bound: band lower bound + """ super().__init__(FILTER_NAME_RANGE, entity=entity) self._lower_bound = lower_bound self._upper_bound = upper_bound @@ -441,13 +453,13 @@ class OutlierFilter(Filter): """BASIC outlier filter. Determines if new state is in a band around the median. - - Args: - radius (float): band radius """ - def __init__(self, window_size, precision, entity, radius): - """Initialize Filter.""" + def __init__(self, window_size, precision, entity, radius: float): + """Initialize Filter. + + :param radius: band radius + """ super().__init__(FILTER_NAME_OUTLIER, window_size, precision, entity) self._radius = radius self._stats_internal = Counter() @@ -475,13 +487,9 @@ class OutlierFilter(Filter): @FILTERS.register(FILTER_NAME_LOWPASS) class LowPassFilter(Filter): - """BASIC Low Pass Filter. + """BASIC Low Pass Filter.""" - Args: - time_constant (int): time constant. - """ - - def __init__(self, window_size, precision, entity, time_constant): + def __init__(self, window_size, precision, entity, time_constant: int): """Initialize Filter.""" super().__init__(FILTER_NAME_LOWPASS, window_size, precision, entity) self._time_constant = time_constant @@ -505,15 +513,15 @@ class TimeSMAFilter(Filter): """Simple Moving Average (SMA) Filter. The window_size is determined by time, and SMA is time weighted. - - Args: - type (enum): type of algorithm used to connect discrete values """ def __init__( self, window_size, precision, entity, type ): # pylint: disable=redefined-builtin - """Initialize Filter.""" + """Initialize Filter. + + :param type: type of algorithm used to connect discrete values + """ super().__init__(FILTER_NAME_TIME_SMA, window_size, precision, entity) self._time_window = window_size self.last_leak = None diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index f34cda67ad9..afe0aa3ed02 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -85,6 +85,6 @@ class FritzBoxScanner(DeviceScanner): if not self.success_init: return False - _LOGGER.info("Scanning") + _LOGGER.debug("Scanning") self.last_results = self.fritz_box.get_hosts_info() return True diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py index ec1922e0d56..ff0694afaab 100644 --- a/homeassistant/components/fronius/sensor.py +++ b/homeassistant/components/fronius/sensor.py @@ -1,5 +1,6 @@ """Support for Fronius devices.""" import copy +from datetime import timedelta import logging import voluptuous as vol @@ -11,10 +12,13 @@ from homeassistant.const import ( CONF_SENSOR_TYPE, CONF_DEVICE, CONF_MONITORED_CONDITIONS, + CONF_SCAN_INTERVAL, ) 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__) @@ -30,6 +34,7 @@ SCOPE_SYSTEM = "system" DEFAULT_SCOPE = SCOPE_DEVICE DEFAULT_DEVICE = 0 DEFAULT_INVERTER = 1 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) SENSOR_TYPES = [TYPE_INVERTER, TYPE_STORAGE, TYPE_METER, TYPE_POWER_FLOW] SCOPE_TYPES = [SCOPE_DEVICE, SCOPE_SYSTEM] @@ -78,47 +83,64 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= session = async_get_clientsession(hass) fronius = Fronius(session, config[CONF_RESOURCE]) - sensors = [] + scan_interval = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + adapters = [] + # Creates all adapters for monitored conditions for condition in config[CONF_MONITORED_CONDITIONS]: device = condition[CONF_DEVICE] - name = "Fronius {} {} {}".format( - condition[CONF_SENSOR_TYPE].replace("_", " ").capitalize(), - device, - config[CONF_RESOURCE], - ) sensor_type = condition[CONF_SENSOR_TYPE] scope = condition[CONF_SCOPE] + name = "Fronius {} {} {}".format( + condition[CONF_SENSOR_TYPE].replace("_", " ").capitalize(), + device if scope == SCOPE_DEVICE else SCOPE_SYSTEM, + config[CONF_RESOURCE], + ) if sensor_type == TYPE_INVERTER: if scope == SCOPE_SYSTEM: - sensor_cls = FroniusInverterSystem + adapter_cls = FroniusInverterSystem else: - sensor_cls = FroniusInverterDevice + adapter_cls = FroniusInverterDevice elif sensor_type == TYPE_METER: if scope == SCOPE_SYSTEM: - sensor_cls = FroniusMeterSystem + adapter_cls = FroniusMeterSystem else: - sensor_cls = FroniusMeterDevice + adapter_cls = FroniusMeterDevice elif sensor_type == TYPE_POWER_FLOW: - sensor_cls = FroniusPowerFlow + adapter_cls = FroniusPowerFlow else: - sensor_cls = FroniusStorage + adapter_cls = FroniusStorage - sensors.append(sensor_cls(fronius, name, device)) + adapters.append(adapter_cls(fronius, name, device, async_add_entities)) - async_add_entities(sensors, True) + # Creates a lamdba that fetches an update when called + def adapter_data_fetcher(data_adapter): + async def fetch_data(*_): + await data_adapter.async_update() + + return fetch_data + + # Set up the fetching in a fixed interval for each adapter + for adapter in adapters: + fetch = adapter_data_fetcher(adapter) + # fetch data once at set-up + await fetch() + async_track_time_interval(hass, fetch, scan_interval) -class FroniusSensor(Entity): - """The Fronius sensor implementation.""" +class FroniusAdapter: + """The Fronius sensor fetching component.""" - def __init__(self, data, name, device): + def __init__(self, bridge, name, device, add_entities): """Initialize the sensor.""" - self.data = data + self.bridge = bridge self._name = name self._device = device - self._state = None - self._attributes = {} + self._fetched = {} + + self.sensors = set() + self._registered_sensors = set() + self._add_entities = add_entities @property def name(self): @@ -126,14 +148,9 @@ class FroniusSensor(Entity): return self._name @property - def state(self): - """Return the current state.""" - return self._state - - @property - def device_state_attributes(self): + def data(self): """Return the state attributes.""" - return self._attributes + return self._fetched async def async_update(self): """Retrieve and update latest state.""" @@ -148,62 +165,129 @@ class FroniusSensor(Entity): "Maybe the configured device is not supported" ) - if values: - self._state = values["status"]["Code"] - attributes = {} - for key in values: - if "value" in values[key]: - attributes[key] = values[key].get("value", 0) - self._attributes = attributes + if not values: + return + attributes = self._fetched + # Copy data of current fronius device + for key, entry in values.items(): + # If the data is directly a sensor + if "value" in entry: + attributes[key] = entry + self._fetched = attributes + + # Add discovered value fields as sensors + # because some fields are only sent temporarily + new_sensors = [] + for key in attributes: + if key not in self.sensors: + self.sensors.add(key) + _LOGGER.info("Discovered %s, adding as sensor", key) + new_sensors.append(FroniusTemplateSensor(self, key)) + self._add_entities(new_sensors, True) + + # Schedule an update for all included sensors + for sensor in self._registered_sensors: + sensor.async_schedule_update_ha_state(True) async def _update(self): """Return values of interest.""" pass + async def register(self, sensor): + """Register child sensor for update subscriptions.""" + self._registered_sensors.add(sensor) -class FroniusInverterSystem(FroniusSensor): - """Sensor for the fronius inverter with system scope.""" + +class FroniusInverterSystem(FroniusAdapter): + """Adapter for the fronius inverter with system scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_system_inverter_data() + return await self.bridge.current_system_inverter_data() -class FroniusInverterDevice(FroniusSensor): - """Sensor for the fronius inverter with device scope.""" +class FroniusInverterDevice(FroniusAdapter): + """Adapter for the fronius inverter with device scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_inverter_data(self._device) + return await self.bridge.current_inverter_data(self._device) -class FroniusStorage(FroniusSensor): - """Sensor for the fronius battery storage.""" +class FroniusStorage(FroniusAdapter): + """Adapter for the fronius battery storage.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_storage_data(self._device) + return await self.bridge.current_storage_data(self._device) -class FroniusMeterSystem(FroniusSensor): - """Sensor for the fronius meter with system scope.""" +class FroniusMeterSystem(FroniusAdapter): + """Adapter for the fronius meter with system scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_system_meter_data() + return await self.bridge.current_system_meter_data() -class FroniusMeterDevice(FroniusSensor): - """Sensor for the fronius meter with device scope.""" +class FroniusMeterDevice(FroniusAdapter): + """Adapter for the fronius meter with device scope.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_meter_data(self._device) + return await self.bridge.current_meter_data(self._device) -class FroniusPowerFlow(FroniusSensor): - """Sensor for the fronius power flow.""" +class FroniusPowerFlow(FroniusAdapter): + """Adapter for the fronius power flow.""" async def _update(self): """Get the values for the current state.""" - return await self.data.current_power_flow() + return await self.bridge.current_power_flow() + + +class FroniusTemplateSensor(Entity): + """Sensor for the single values (e.g. pv power, ac power).""" + + def __init__(self, parent: FroniusAdapter, name): + """Initialize a singular value sensor.""" + self._name = name + self.parent = parent + self._state = None + self._unit = None + + @property + def name(self): + """Return the name of the sensor.""" + return "{} {}".format( + self._name.replace("_", " ").capitalize(), self.parent.name + ) + + @property + def state(self): + """Return the current state.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def should_poll(self): + """Device should not be polled, returns False.""" + return False + + async def async_update(self): + """Update the internal state.""" + state = self.parent.data.get(self._name) + self._state = state.get("value") + self._unit = state.get("unit") + + async def async_added_to_hass(self): + """Register at parent component for updates.""" + await self.parent.register(self) + + def __hash__(self): + """Hash sensor by hashing its name.""" + return hash(self.name) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index b769d6b9aea..d8790b746be 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -22,6 +22,8 @@ from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage +# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs + # Fix mimetypes for borked Windows machines # https://github.com/home-assistant/home-assistant-polymer/issues/3336 mimetypes.add_type("text/css", ".css") @@ -45,7 +47,14 @@ MANIFEST_JSON = { "description": "Home automation platform that puts local control and privacy first.", "dir": "ltr", "display": "standalone", - "icons": [], + "icons": [ + { + "src": "/static/icons/favicon-{size}x{size}.png".format(size=size), + "sizes": "{size}x{size}".format(size=size), + "type": "image/png", + } + for size in (192, 384, 512, 1024) + ], "lang": "en-US", "name": "Home Assistant", "short_name": "Assistant", @@ -53,15 +62,6 @@ MANIFEST_JSON = { "theme_color": DEFAULT_THEME_COLOR, } -for size in (192, 384, 512, 1024): - MANIFEST_JSON["icons"].append( - { - "src": "/static/icons/favicon-{size}x{size}.png".format(size=size), - "sizes": "{size}x{size}".format(size=size), - "type": "image/png", - } - ) - DATA_PANELS = "frontend_panels" DATA_JS_VERSION = "frontend_js_version" DATA_EXTRA_HTML_URL = "frontend_extra_html_url" diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 6d25e846db9..fa6145a7af2 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/components/frontend", "requirements": [ - "home-assistant-frontend==20190805.0" + "home-assistant-frontend==20190828.0" ], "dependencies": [ "api", diff --git a/homeassistant/components/frontend/storage.py b/homeassistant/components/frontend/storage.py index 56f23da5253..75b7b356ef9 100644 --- a/homeassistant/components/frontend/storage.py +++ b/homeassistant/components/frontend/storage.py @@ -4,6 +4,9 @@ import voluptuous as vol from homeassistant.components import websocket_api + +# mypy: allow-untyped-calls, allow-untyped-defs + DATA_STORAGE = "frontend_storage" STORAGE_VERSION_USER_DATA = 1 STORAGE_KEY_USER_DATA = "frontend.user_data_{}" diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 3bbec4258bc..45f3f91cd6d 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -1,16 +1,23 @@ """Support for a Genius Hub system.""" from datetime import timedelta import logging +from typing import Awaitable +import aiohttp import voluptuous as vol -from geniushubclient import GeniusHubClient +from geniushubclient import GeniusHub from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +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.discovery import async_load_platform -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_send, + async_dispatcher_connect, +) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -41,32 +48,23 @@ async def async_setup(hass, hass_config): args = (kwargs.pop(CONF_TOKEN),) hass.data[DOMAIN] = {} - data = hass.data[DOMAIN]["data"] = GeniusData(hass, args, kwargs) + broker = GeniusBroker(hass, args, kwargs) + try: - await data._client.hub.update() # pylint: disable=protected-access - except AssertionError: # assert response.status == HTTP_OK - _LOGGER.warning("Setup failed, check your configuration.", exc_info=True) + await broker.client.update() + except aiohttp.ClientResponseError as err: + _LOGGER.error("Setup failed, check your configuration, %s", err) return False + broker.make_debug_log_entries() - _LOGGER.debug( - # noqa; pylint: disable=protected-access - "zones_raw = %s", - data._client.hub._zones_raw, - ) - _LOGGER.debug( - # noqa; pylint: disable=protected-access - "devices_raw = %s", - data._client.hub._devices_raw, - ) - - async_track_time_interval(hass, data.async_update, SCAN_INTERVAL) + async_track_time_interval(hass, broker.async_update, SCAN_INTERVAL) for platform in ["climate", "water_heater"]: hass.async_create_task( async_load_platform(hass, platform, DOMAIN, {}, hass_config) ) - if data._client.api_version == 3: # pylint: disable=protected-access + if broker.client.api_version == 3: # pylint: disable=no-member for platform in ["sensor", "binary_sensor"]: hass.async_create_task( async_load_platform(hass, platform, DOMAIN, {}, hass_config) @@ -75,33 +73,58 @@ async def async_setup(hass, hass_config): return True -class GeniusData: +class GeniusBroker: """Container for geniushub client and data.""" def __init__(self, hass, args, kwargs): """Initialize the geniushub client.""" - self._hass = hass - self._client = hass.data[DOMAIN]["client"] = GeniusHubClient( + self.hass = hass + self.client = hass.data[DOMAIN]["client"] = GeniusHub( *args, **kwargs, session=async_get_clientsession(hass) ) async def async_update(self, now, **kwargs): """Update the geniushub client's data.""" try: - await self._client.hub.update() - except AssertionError: # assert response.status == HTTP_OK - _LOGGER.warning("Update failed.", exc_info=True) + await self.client.update() + except aiohttp.ClientResponseError as err: + _LOGGER.warning("Update failed, %s", err) return + self.make_debug_log_entries() + async_dispatcher_send(self.hass, DOMAIN) + + def make_debug_log_entries(self): + """Make any useful debug log entries.""" + # pylint: disable=protected-access _LOGGER.debug( - # noqa; pylint: disable=protected-access - "zones_raw = %s", - self._client.hub._zones_raw, - ) - _LOGGER.debug( - # noqa; pylint: disable=protected-access - "devices_raw = %s", - self._client.hub._devices_raw, + "Raw JSON: \n\nclient._zones = %s \n\nclient._devices = %s", + self.client._zones, + self.client._devices, ) - async_dispatcher_send(self._hass, DOMAIN) + +class GeniusEntity(Entity): + """Base for all Genius Hub endtities.""" + + def __init__(self): + """Initialize the entity.""" + self._name = None + + async def async_added_to_hass(self) -> Awaitable[None]: + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def name(self) -> str: + """Return the name of the geniushub entity.""" + return self._name + + @property + def should_poll(self) -> bool: + """Return False as geniushub entities should not be polled.""" + return False diff --git a/homeassistant/components/geniushub/binary_sensor.py b/homeassistant/components/geniushub/binary_sensor.py index afdc0ef5f89..1cc8cd3f406 100644 --- a/homeassistant/components/geniushub/binary_sensor.py +++ b/homeassistant/components/geniushub/binary_sensor.py @@ -1,14 +1,10 @@ """Support for Genius Hub binary_sensor devices.""" -import logging +from typing import Any, Dict from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import utc_from_timestamp -from . import DOMAIN - -_LOGGER = logging.getLogger(__name__) +from . import DOMAIN, GeniusEntity GH_IS_SWITCH = ["Dual Channel Receiver", "Electric Switch", "Smart Plug"] @@ -17,58 +13,39 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Genius Hub sensor entities.""" client = hass.data[DOMAIN]["client"] - devices = [d for d in client.hub.device_objs if d.type is not None] switches = [ - GeniusBinarySensor(client, d) for d in devices if d.type[:21] in GH_IS_SWITCH + GeniusBinarySensor(d) for d in client.device_objs if d.type[:21] in GH_IS_SWITCH ] async_add_entities(switches) -class GeniusBinarySensor(BinarySensorDevice): +class GeniusBinarySensor(GeniusEntity, BinarySensorDevice): """Representation of a Genius Hub binary_sensor.""" - def __init__(self, client, device): + def __init__(self, device) -> None: """Initialize the binary sensor.""" - self._client = client - self._device = device + super().__init__() + self._device = device if device.type[:21] == "Dual Channel Receiver": self._name = "Dual Channel Receiver {}".format(device.id) else: self._name = "{} {}".format(device.type, device.id) - async def async_added_to_hass(self): - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self): - self.async_schedule_update_ha_state(force_refresh=True) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - - @property - def is_on(self): + def is_on(self) -> bool: """Return the status of the sensor.""" - return self._device.state["outputOnOff"] + return self._device.data["state"]["outputOnOff"] @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" attrs = {} - attrs["assigned_zone"] = self._device.assignedZones[0]["name"] + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - # noqa; pylint: disable=protected-access - last_comms = self._device._raw_json["childValues"]["lastComms"]["val"] + # pylint: disable=protected-access + last_comms = self._device._raw["childValues"]["lastComms"]["val"] if last_comms != 0: attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index ae1d714dd2b..a856e48438f 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,5 +1,4 @@ """Support for Genius Hub climate devices.""" -import logging from typing import Any, Awaitable, Dict, Optional, List from homeassistant.components.climate import ClimateDevice @@ -12,12 +11,8 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN - -_LOGGER = logging.getLogger(__name__) +from . import DOMAIN, GeniusEntity ATTR_DURATION = "duration" @@ -40,36 +35,25 @@ async def async_setup_platform( """Set up the Genius Hub climate entities.""" client = hass.data[DOMAIN]["client"] - async_add_entities( - [ - GeniusClimateZone(client, z) - for z in client.hub.zone_objs - if z.type in GH_ZONES - ] - ) + entities = [ + GeniusClimateZone(z) for z in client.zone_objs if z.data["type"] in GH_ZONES + ] + async_add_entities(entities) -class GeniusClimateZone(ClimateDevice): +class GeniusClimateZone(GeniusEntity, ClimateDevice): """Representation of a Genius Hub climate device.""" - def __init__(self, client, zone): + def __init__(self, zone) -> None: """Initialize the climate device.""" - self._client = client - self._zone = zone + super().__init__() + self._zone = zone if hasattr(self._zone, "occupied"): # has a movement sensor self._preset_modes = list(HA_PRESET_TO_GH) else: self._preset_modes = [PRESET_BOOST] - async def async_added_to_hass(self) -> Awaitable[None]: - """Run when entity about to be added.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - @property def name(self) -> str: """Return the name of the climate device.""" @@ -78,14 +62,9 @@ class GeniusClimateZone(ClimateDevice): @property def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - tmp = self._zone.__dict__.items() + tmp = self._zone.data.items() return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} - @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - @property def icon(self) -> str: """Return the icon to use in the frontend UI.""" @@ -94,12 +73,12 @@ class GeniusClimateZone(ClimateDevice): @property def current_temperature(self) -> Optional[float]: """Return the current temperature.""" - return self._zone.temperature + return self._zone.data["temperature"] @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self._zone.setpoint + return self._zone.data["setpoint"] @property def min_temp(self) -> float: @@ -124,7 +103,7 @@ class GeniusClimateZone(ClimateDevice): @property def hvac_mode(self) -> str: """Return hvac operation ie. heat, cool mode.""" - return GH_HVAC_TO_HA.get(self._zone.mode, HVAC_MODE_HEAT) + return GH_HVAC_TO_HA.get(self._zone.data["mode"], HVAC_MODE_HEAT) @property def hvac_modes(self) -> List[str]: @@ -134,7 +113,7 @@ class GeniusClimateZone(ClimateDevice): @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - return GH_PRESET_TO_HA.get(self._zone.mode) + return GH_PRESET_TO_HA.get(self._zone.data["mode"]) @property def preset_modes(self) -> Optional[List[str]]: diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 98145ea0944..12f7c266840 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/components/geniushub", "requirements": [ - "geniushub-client==0.5.4" + "geniushub-client==0.6.5" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index f87c957d3da..5e39be1620a 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -1,16 +1,11 @@ """Support for Genius Hub sensor devices.""" from datetime import timedelta -import logging +from typing import Any, Awaitable, Dict from homeassistant.const import DEVICE_CLASS_BATTERY -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util.dt import utc_from_timestamp, utcnow -from . import DOMAIN - -_LOGGER = logging.getLogger(__name__) +from . import DOMAIN, GeniusEntity GH_HAS_BATTERY = ["Room Thermostat", "Genius Valve", "Room Sensor", "Radiator Valve"] @@ -25,45 +20,27 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the Genius Hub sensor entities.""" client = hass.data[DOMAIN]["client"] - sensors = [ - GeniusDevice(client, d) - for d in client.hub.device_objs - if d.type in GH_HAS_BATTERY - ] - + sensors = [GeniusBattery(d) for d in client.device_objs if d.type in GH_HAS_BATTERY] issues = [GeniusIssue(client, i) for i in list(GH_LEVEL_MAPPING)] async_add_entities(sensors + issues, update_before_add=True) -class GeniusDevice(Entity): +class GeniusBattery(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, client, device): + def __init__(self, device) -> None: """Initialize the sensor.""" - self._client = client - self._device = device + super().__init__() + self._device = device self._name = "{} {}".format(device.type, device.id) - async def async_added_to_hass(self): - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self): - self.async_schedule_update_ha_state(force_refresh=True) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def icon(self): + def icon(self) -> str: """Return the icon of the sensor.""" - # noqa; pylint: disable=protected-access - values = self._device._raw_json["childValues"] + + values = self._device._raw["childValues"] # pylint: disable=protected-access last_comms = utc_from_timestamp(values["lastComms"]["val"]) if "WakeUp_Interval" in values: @@ -74,7 +51,7 @@ class GeniusDevice(Entity): if last_comms < utcnow() - interval * 3: return "mdi:battery-unknown" - battery_level = self._device.state["batteryLevel"] + battery_level = self._device.data["state"]["batteryLevel"] if battery_level == 255: return "mdi:battery-unknown" if battery_level < 40: @@ -87,78 +64,57 @@ class GeniusDevice(Entity): return icon @property - def device_class(self): + def device_class(self) -> str: """Return the device class of the sensor.""" return DEVICE_CLASS_BATTERY @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of the sensor.""" return "%" @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - - @property - def state(self): + def state(self) -> str: """Return the state of the sensor.""" - level = self._device.state.get("batteryLevel", 255) + level = self._device.data["state"].get("batteryLevel", 255) return level if level != 255 else 0 @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" attrs = {} - attrs["assigned_zone"] = self._device.assignedZones[0]["name"] + attrs["assigned_zone"] = self._device.data["assignedZones"][0]["name"] - # noqa; pylint: disable=protected-access - last_comms = self._device._raw_json["childValues"]["lastComms"]["val"] + # pylint: disable=protected-access + last_comms = self._device._raw["childValues"]["lastComms"]["val"] attrs["last_comms"] = utc_from_timestamp(last_comms).isoformat() return {**attrs} -class GeniusIssue(Entity): +class GeniusIssue(GeniusEntity): """Representation of a Genius Hub sensor.""" - def __init__(self, client, level): + def __init__(self, hub, level) -> None: """Initialize the sensor.""" - self._hub = client.hub + super().__init__() + + self._hub = hub self._name = GH_LEVEL_MAPPING[level] self._level = level self._issues = [] - async def async_added_to_hass(self): - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self): - self.async_schedule_update_ha_state(force_refresh=True) - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - - @property - def state(self): + def state(self) -> str: """Return the number of issues.""" return len(self._issues) @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" return {"{}_list".format(self._level): self._issues} - async def async_update(self): + async def async_update(self) -> Awaitable[None]: """Process the sensor's state data.""" self._issues = [ i["description"] for i in self._hub.issues if i["level"] == self._level diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index 9e27ec5f190..1086160e77c 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -1,5 +1,5 @@ """Support for Genius Hub water_heater devices.""" -import logging +from typing import Any, Awaitable, Dict, Optional, List from homeassistant.components.water_heater import ( WaterHeaterDevice, @@ -7,16 +7,12 @@ from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, ) from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN +from . import DOMAIN, GeniusEntity STATE_AUTO = "auto" STATE_MANUAL = "manual" -_LOGGER = logging.getLogger(__name__) - GH_HEATERS = ["hot water temperature"] GH_SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE @@ -48,96 +44,81 @@ async def async_setup_platform( client = hass.data[DOMAIN]["client"] entities = [ - GeniusWaterHeater(client, z) - for z in client.hub.zone_objs - if z.type in GH_HEATERS + GeniusWaterHeater(z) for z in client.zone_objs if z.data["type"] in GH_HEATERS ] async_add_entities(entities) -class GeniusWaterHeater(WaterHeaterDevice): +class GeniusWaterHeater(GeniusEntity, WaterHeaterDevice): """Representation of a Genius Hub water_heater device.""" - def __init__(self, client, boiler): + def __init__(self, boiler) -> None: """Initialize the water_heater device.""" - self._client = client - self._boiler = boiler + super().__init__() + self._boiler = boiler self._operation_list = list(HA_OPMODE_TO_GH) - async def async_added_to_hass(self): - """Run when entity about to be added.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self): - self.async_schedule_update_ha_state(force_refresh=True) - @property - def name(self): + def name(self) -> str: """Return the name of the water_heater device.""" return self._boiler.name @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return the device state attributes.""" - tmp = self._boiler.__dict__.items() - return {"status": {k: v for k, v in tmp if k in GH_STATE_ATTRS}} + return { + "status": { + k: v for k, v in self._boiler.data.items() if k in GH_STATE_ATTRS + } + } @property - def should_poll(self) -> bool: - """Return False as the geniushub devices should not be polled.""" - return False - - @property - def current_temperature(self): + def current_temperature(self) -> Optional[float]: """Return the current temperature.""" - try: - return self._boiler.temperature - except AttributeError: - return None + return self._boiler.data.get("temperature") @property - def target_temperature(self): + def target_temperature(self) -> float: """Return the temperature we try to reach.""" - return self._boiler.setpoint + return self._boiler.data["setpoint"] @property - def min_temp(self): + def min_temp(self) -> float: """Return max valid temperature that can be set.""" return GH_MIN_TEMP @property - def max_temp(self): + def max_temp(self) -> float: """Return max valid temperature that can be set.""" return GH_MAX_TEMP @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def supported_features(self): + def supported_features(self) -> int: """Return the list of supported features.""" return GH_SUPPORT_FLAGS @property - def operation_list(self): + def operation_list(self) -> List[str]: """Return the list of available operation modes.""" return self._operation_list @property - def current_operation(self): + def current_operation(self) -> str: """Return the current operation mode.""" - return GH_STATE_TO_HA[self._boiler.mode] + return GH_STATE_TO_HA[self._boiler.data["mode"]] - async def async_set_operation_mode(self, operation_mode): + async def async_set_operation_mode(self, operation_mode) -> Awaitable[None]: """Set a new operation mode for this boiler.""" await self._boiler.set_mode(HA_OPMODE_TO_GH[operation_mode]) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs) -> Awaitable[None]: """Set a new target temperature for this boiler.""" temperature = kwargs[ATTR_TEMPERATURE] await self._boiler.set_override(temperature, 3600) # 1 hour diff --git a/homeassistant/components/geo_location/__init__.py b/homeassistant/components/geo_location/__init__.py index 869f96901c1..3c270f2c521 100644 --- a/homeassistant/components/geo_location/__init__.py +++ b/homeassistant/components/geo_location/__init__.py @@ -11,6 +11,9 @@ from homeassistant.helpers.config_validation import ( # noqa 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__) ATTR_DISTANCE = "distance" diff --git a/homeassistant/components/geonetnz_quakes/.translations/en.json b/homeassistant/components/geonetnz_quakes/.translations/en.json new file mode 100644 index 00000000000..4143efcdf96 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "identifier_exists": "Location already registered" + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + }, + "title": "Fill in your filter details." + } + }, + "title": "GeoNet NZ Quakes" + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/__init__.py b/homeassistant/components/geonetnz_quakes/__init__.py new file mode 100644 index 00000000000..e786b413029 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/__init__.py @@ -0,0 +1,101 @@ +"""The GeoNet NZ Quakes integration.""" +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, +) +from homeassistant.helpers import config_validation as cv + +from .config_flow import configured_instances +from .const import ( + CONF_MINIMUM_MAGNITUDE, + CONF_MMI, + DEFAULT_MINIMUM_MAGNITUDE, + DEFAULT_MMI, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + FEED, +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_MMI, default=DEFAULT_MMI): vol.All( + vol.Coerce(int), vol.Range(min=-1, max=8) + ), + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): vol.Coerce(float), + vol.Optional( + CONF_MINIMUM_MAGNITUDE, default=DEFAULT_MINIMUM_MAGNITUDE + ): vol.All(vol.Coerce(float), vol.Range(min=0)), + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass, config): + """Set up the GeoNet NZ Quakes component.""" + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + + latitude = conf.get(CONF_LATITUDE, hass.config.latitude) + longitude = conf.get(CONF_LONGITUDE, hass.config.longitude) + mmi = conf[CONF_MMI] + scan_interval = conf[CONF_SCAN_INTERVAL] + + identifier = f"{latitude}, {longitude}" + if identifier in configured_instances(hass): + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_LATITUDE: latitude, + CONF_LONGITUDE: longitude, + CONF_RADIUS: conf[CONF_RADIUS], + CONF_MINIMUM_MAGNITUDE: conf[CONF_MINIMUM_MAGNITUDE], + CONF_MMI: mmi, + CONF_SCAN_INTERVAL: scan_interval, + }, + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the GeoNet NZ Quakes component as config entry.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][FEED] = {} + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "geo_location") + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an GeoNet NZ Quakes component config entry.""" + manager = hass.data[DOMAIN][FEED].pop(config_entry.entry_id) + await manager.async_stop() + + await hass.config_entries.async_forward_entry_unload(config_entry, "geo_location") + + return True diff --git a/homeassistant/components/geonetnz_quakes/config_flow.py b/homeassistant/components/geonetnz_quakes/config_flow.py new file mode 100644 index 00000000000..bd93f08c72b --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/config_flow.py @@ -0,0 +1,94 @@ +"""Config flow to configure the GeoNet NZ Quakes integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv + +from .const import ( + CONF_MMI, + DEFAULT_MMI, + DEFAULT_RADIUS, + DEFAULT_SCAN_INTERVAL, + DOMAIN, + DEFAULT_MINIMUM_MAGNITUDE, + CONF_MINIMUM_MAGNITUDE, +) + +_LOGGER = logging.getLogger(__name__) + + +@callback +def configured_instances(hass): + """Return a set of configured GeoNet NZ Quakes instances.""" + return set( + f"{entry.data[CONF_LATITUDE]}, {entry.data[CONF_LONGITUDE]}" + for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +@config_entries.HANDLERS.register(DOMAIN) +class GeonetnzQuakesFlowHandler(config_entries.ConfigFlow): + """Handle a GeoNet NZ Quakes config flow.""" + + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def _show_form(self, errors=None): + """Show the form to the user.""" + data_schema = vol.Schema( + { + vol.Optional(CONF_MMI, default=DEFAULT_MMI): vol.All( + vol.Coerce(int), vol.Range(min=-1, max=8) + ), + vol.Optional(CONF_RADIUS, default=DEFAULT_RADIUS): cv.positive_int, + } + ) + + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors or {} + ) + + 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.""" + _LOGGER.debug("User input: %s", user_input) + if not user_input: + return await self._show_form() + + latitude = user_input.get(CONF_LATITUDE, self.hass.config.latitude) + user_input[CONF_LATITUDE] = latitude + longitude = user_input.get(CONF_LONGITUDE, self.hass.config.longitude) + user_input[CONF_LONGITUDE] = longitude + + identifier = f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}" + if identifier in configured_instances(self.hass): + return await self._show_form({"base": "identifier_exists"}) + + if self.hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL: + user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_IMPERIAL + else: + user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC + + scan_interval = user_input.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + + minimum_magnitude = user_input.get( + CONF_MINIMUM_MAGNITUDE, DEFAULT_MINIMUM_MAGNITUDE + ) + user_input[CONF_MINIMUM_MAGNITUDE] = minimum_magnitude + + return self.async_create_entry(title=identifier, data=user_input) diff --git a/homeassistant/components/geonetnz_quakes/const.py b/homeassistant/components/geonetnz_quakes/const.py new file mode 100644 index 00000000000..d06e85ee2cb --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/const.py @@ -0,0 +1,14 @@ +"""Define constants for the GeoNet NZ Quakes integration.""" +from datetime import timedelta + +DOMAIN = "geonetnz_quakes" + +CONF_MINIMUM_MAGNITUDE = "minimum_magnitude" +CONF_MMI = "mmi" + +FEED = "feed" + +DEFAULT_MINIMUM_MAGNITUDE = 0.0 +DEFAULT_MMI = 3 +DEFAULT_RADIUS = 50.0 +DEFAULT_SCAN_INTERVAL = timedelta(minutes=5) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py new file mode 100644 index 00000000000..9d4be94e3aa --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -0,0 +1,284 @@ +"""Geolocation support for GeoNet NZ Quakes Feeds.""" +from datetime import timedelta +import logging +from typing import Optional + +from aio_geojson_geonetnz_quakes import GeonetnzQuakesFeedManager + +from homeassistant.components.geo_location import GeolocationEvent +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_SCAN_INTERVAL, + CONF_UNIT_SYSTEM, + CONF_UNIT_SYSTEM_IMPERIAL, + LENGTH_KILOMETERS, + LENGTH_MILES, + ATTR_TIME, +) +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM + +from .const import CONF_MINIMUM_MAGNITUDE, CONF_MMI, DOMAIN, FEED + +_LOGGER = logging.getLogger(__name__) + +ATTR_DEPTH = "depth" +ATTR_EXTERNAL_ID = "external_id" +ATTR_LOCALITY = "locality" +ATTR_MAGNITUDE = "magnitude" +ATTR_MMI = "mmi" +ATTR_PUBLICATION_DATE = "publication_date" +ATTR_QUALITY = "quality" + +DEFAULT_FILTER_TIME_INTERVAL = timedelta(days=7) + +SIGNAL_DELETE_ENTITY = "geonetnz_quakes_delete_{}" +SIGNAL_UPDATE_ENTITY = "geonetnz_quakes_update_{}" + +SOURCE = "geonetnz_quakes" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the GeoNet NZ Quakes Feed platform.""" + radius = entry.data[CONF_RADIUS] + unit_system = entry.data[CONF_UNIT_SYSTEM] + if unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + radius = METRIC_SYSTEM.length(radius, LENGTH_MILES) + manager = GeonetnzQuakesFeedEntityManager( + hass, + async_add_entities, + entry.data[CONF_SCAN_INTERVAL], + entry.data[CONF_LATITUDE], + entry.data[CONF_LONGITUDE], + entry.data[CONF_MMI], + radius, + unit_system, + entry.data[CONF_MINIMUM_MAGNITUDE], + ) + hass.data[DOMAIN][FEED][entry.entry_id] = manager + await manager.async_init() + + +class GeonetnzQuakesFeedEntityManager: + """Feed Entity Manager for GeoNet NZ Quakes feed.""" + + def __init__( + self, + hass, + async_add_entities, + scan_interval, + latitude, + longitude, + mmi, + radius_in_km, + unit_system, + minimum_magnitude, + ): + """Initialize the Feed Entity Manager.""" + self._hass = hass + coordinates = (latitude, longitude) + websession = aiohttp_client.async_get_clientsession(hass) + self._feed_manager = GeonetnzQuakesFeedManager( + websession, + self._generate_entity, + self._update_entity, + self._remove_entity, + coordinates, + mmi=mmi, + filter_radius=radius_in_km, + filter_minimum_magnitude=minimum_magnitude, + filter_time=DEFAULT_FILTER_TIME_INTERVAL, + ) + self._async_add_entities = async_add_entities + self._scan_interval = timedelta(seconds=scan_interval) + self._unit_system = unit_system + self._track_time_remove_callback = None + + async def async_init(self): + """Schedule regular updates based on configured time interval.""" + + async def update(event_time): + """Update.""" + await self.async_update() + + await self.async_update() + self._track_time_remove_callback = async_track_time_interval( + self._hass, update, self._scan_interval + ) + _LOGGER.debug("Feed entity manager initialized") + + async def async_update(self): + """Refresh data.""" + await self._feed_manager.update() + _LOGGER.debug("Feed entity manager updated") + + async def async_stop(self): + """Stop this feed entity manager from refreshing.""" + if self._track_time_remove_callback: + self._track_time_remove_callback() + _LOGGER.debug("Feed entity manager stopped") + + def get_entry(self, external_id): + """Get feed entry by external id.""" + return self._feed_manager.feed_entries.get(external_id) + + async def _generate_entity(self, external_id): + """Generate new entity.""" + new_entity = GeonetnzQuakesEvent(self, external_id, self._unit_system) + # Add new entities to HA. + self._async_add_entities([new_entity], True) + + async def _update_entity(self, external_id): + """Update entity.""" + async_dispatcher_send(self._hass, SIGNAL_UPDATE_ENTITY.format(external_id)) + + async def _remove_entity(self, external_id): + """Remove entity.""" + async_dispatcher_send(self._hass, SIGNAL_DELETE_ENTITY.format(external_id)) + + +class GeonetnzQuakesEvent(GeolocationEvent): + """This represents an external event with GeoNet NZ Quakes feed data.""" + + def __init__(self, feed_manager, external_id, unit_system): + """Initialize entity with data from feed entry.""" + self._feed_manager = feed_manager + self._external_id = external_id + self._unit_system = unit_system + self._title = None + self._distance = None + self._latitude = None + self._longitude = None + self._attribution = None + self._depth = None + self._locality = None + self._magnitude = None + self._mmi = None + self._quality = None + self._time = None + self._remove_signal_delete = None + self._remove_signal_update = None + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._remove_signal_delete = async_dispatcher_connect( + self.hass, + SIGNAL_DELETE_ENTITY.format(self._external_id), + self._delete_callback, + ) + self._remove_signal_update = async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._external_id), + self._update_callback, + ) + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + self._remove_signal_delete() + self._remove_signal_update() + + @callback + def _delete_callback(self): + """Remove this entity.""" + self.hass.async_create_task(self.async_remove()) + + @callback + def _update_callback(self): + """Call update method.""" + self.async_schedule_update_ha_state(True) + + @property + def should_poll(self): + """No polling needed for GeoNet NZ Quakes feed location events.""" + return False + + async def async_update(self): + """Update this entity from the data held in the feed manager.""" + _LOGGER.debug("Updating %s", self._external_id) + feed_entry = self._feed_manager.get_entry(self._external_id) + if feed_entry: + self._update_from_feed(feed_entry) + + def _update_from_feed(self, feed_entry): + """Update the internal state from the provided feed entry.""" + self._title = feed_entry.title + # Convert distance if not metric system. + if self._unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + self._distance = IMPERIAL_SYSTEM.length( + feed_entry.distance_to_home, LENGTH_KILOMETERS + ) + else: + self._distance = feed_entry.distance_to_home + self._latitude = feed_entry.coordinates[0] + self._longitude = feed_entry.coordinates[1] + self._attribution = feed_entry.attribution + self._depth = feed_entry.depth + self._locality = feed_entry.locality + self._magnitude = feed_entry.magnitude + self._mmi = feed_entry.mmi + self._quality = feed_entry.quality + self._time = feed_entry.time + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:pulse" + + @property + def source(self) -> str: + """Return source value of this external event.""" + return SOURCE + + @property + def name(self) -> Optional[str]: + """Return the name of the entity.""" + return self._title + + @property + def distance(self) -> Optional[float]: + """Return distance value of this external event.""" + return self._distance + + @property + def latitude(self) -> Optional[float]: + """Return latitude value of this external event.""" + return self._latitude + + @property + def longitude(self) -> Optional[float]: + """Return longitude value of this external event.""" + return self._longitude + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + if self._unit_system == CONF_UNIT_SYSTEM_IMPERIAL: + return LENGTH_MILES + return LENGTH_KILOMETERS + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + attributes = {} + for key, value in ( + (ATTR_EXTERNAL_ID, self._external_id), + (ATTR_ATTRIBUTION, self._attribution), + (ATTR_DEPTH, self._depth), + (ATTR_LOCALITY, self._locality), + (ATTR_MAGNITUDE, self._magnitude), + (ATTR_MMI, self._mmi), + (ATTR_QUALITY, self._quality), + (ATTR_TIME, self._time), + ): + if value or isinstance(value, bool): + attributes[key] = value + return attributes diff --git a/homeassistant/components/geonetnz_quakes/manifest.json b/homeassistant/components/geonetnz_quakes/manifest.json new file mode 100644 index 00000000000..c84a4152582 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "geonetnz_quakes", + "name": "GeoNet NZ Quakes", + "config_flow": true, + "documentation": "https://www.home-assistant.io/components/geonetnz_quakes", + "requirements": [ + "aio_geojson_geonetnz_quakes==0.9" + ], + "dependencies": [], + "codeowners": [ + "@exxamalte" + ] +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/strings.json b/homeassistant/components/geonetnz_quakes/strings.json new file mode 100644 index 00000000000..6ec915eb68d --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "title": "GeoNet NZ Quakes", + "step": { + "user": { + "title": "Fill in your filter details.", + "data": { + "radius": "Radius", + "mmi": "MMI" + } + } + }, + "error": { + "identifier_exists": "Location already registered" + } + } +} diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 54c5806101b..cf064b3bfa7 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -256,6 +256,6 @@ class GoogleCloudTTSProvider(Provider): except asyncio.TimeoutError as ex: _LOGGER.error("Timeout for Google Cloud TTS call: %s", ex) except Exception as ex: # pylint: disable=broad-except - _LOGGER.exception("Error occured during Google Cloud TTS call: %s", ex) + _LOGGER.exception("Error occurred during Google Cloud TTS call: %s", ex) return None, None diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 0887aa19bfb..2149e40e504 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -52,7 +52,7 @@ class GoogleMapsScanner: self.see = see self.username = config[CONF_USERNAME] self.max_gps_accuracy = config[CONF_MAX_GPS_ACCURACY] - self.scan_interval = timedelta(seconds=config.get(CONF_SCAN_INTERVAL, 60)) + self.scan_interval = config.get(CONF_SCAN_INTERVAL) or timedelta(60) credfile = "{}.{}".format( hass.config.path(CREDENTIALS_FILE), slugify(self.username) diff --git a/homeassistant/components/googlehome/__init__.py b/homeassistant/components/googlehome/__init__.py deleted file mode 100644 index 01e17708fb3..00000000000 --- a/homeassistant/components/googlehome/__init__.py +++ /dev/null @@ -1,118 +0,0 @@ -"""Support Google Home units.""" -import logging - -import asyncio -import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_HOST -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "googlehome" -CLIENT = "googlehome_client" - -NAME = "GoogleHome" - -CONF_DEVICE_TYPES = "device_types" -CONF_RSSI_THRESHOLD = "rssi_threshold" -CONF_TRACK_ALARMS = "track_alarms" -CONF_TRACK_DEVICES = "track_devices" - -DEVICE_TYPES = [1, 2, 3] -DEFAULT_RSSI_THRESHOLD = -70 - -DEVICE_CONFIG = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_DEVICE_TYPES, default=DEVICE_TYPES): vol.All( - cv.ensure_list, [vol.In(DEVICE_TYPES)] - ), - vol.Optional(CONF_RSSI_THRESHOLD, default=DEFAULT_RSSI_THRESHOLD): vol.Coerce( - int - ), - vol.Optional(CONF_TRACK_ALARMS, default=False): cv.boolean, - vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, - } -) - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_CONFIG])} - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass, config): - """Set up the Google Home component.""" - hass.data[DOMAIN] = {} - hass.data[CLIENT] = GoogleHomeClient(hass) - - for device in config[DOMAIN][CONF_DEVICES]: - hass.data[DOMAIN][device["host"]] = {} - if device[CONF_TRACK_DEVICES]: - hass.async_create_task( - discovery.async_load_platform( - hass, "device_tracker", DOMAIN, device, config - ) - ) - - if device[CONF_TRACK_ALARMS]: - hass.async_create_task( - discovery.async_load_platform(hass, "sensor", DOMAIN, device, config) - ) - - return True - - -class GoogleHomeClient: - """Handle all communication with the Google Home unit.""" - - def __init__(self, hass): - """Initialize the Google Home Client.""" - self.hass = hass - self._connected = None - - async def update_info(self, host): - """Update data from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home info for %s", host) - session = async_get_clientsession(self.hass) - - device_info = await Cast(host, self.hass.loop, session).info() - device_info_data = await device_info.get_device_info() - self._connected = bool(device_info_data) - - self.hass.data[DOMAIN][host]["info"] = device_info_data - - async def update_bluetooth(self, host): - """Update bluetooth from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home bluetooth for %s", host) - session = async_get_clientsession(self.hass) - - bluetooth = await Cast(host, self.hass.loop, session).bluetooth() - await bluetooth.scan_for_devices() - await asyncio.sleep(5) - bluetooth_data = await bluetooth.get_scan_result() - - self.hass.data[DOMAIN][host]["bluetooth"] = bluetooth_data - - async def update_alarms(self, host): - """Update alarms from Google Home.""" - from googledevices.api.connect import Cast - - _LOGGER.debug("Updating Google Home bluetooth for %s", host) - session = async_get_clientsession(self.hass) - - assistant = await Cast(host, self.hass.loop, session).assistant() - alarms_data = await assistant.get_alarms() - - self.hass.data[DOMAIN][host]["alarms"] = alarms_data diff --git a/homeassistant/components/googlehome/device_tracker.py b/homeassistant/components/googlehome/device_tracker.py deleted file mode 100644 index 58350afa430..00000000000 --- a/homeassistant/components/googlehome/device_tracker.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Google Home Bluetooth tacker.""" -from datetime import timedelta -import logging - -from homeassistant.components.device_tracker import DeviceScanner -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util import slugify - -from . import CLIENT, DOMAIN as GOOGLEHOME_DOMAIN, NAME - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=10) - - -async def async_setup_scanner(hass, config, async_see, discovery_info=None): - """Validate the configuration and return a Google Home scanner.""" - if discovery_info is None: - _LOGGER.warning("To use this you need to configure the 'googlehome' component") - return False - scanner = GoogleHomeDeviceScanner( - hass, hass.data[CLIENT], discovery_info, async_see - ) - return await scanner.async_init() - - -class GoogleHomeDeviceScanner(DeviceScanner): - """This class queries a Google Home unit.""" - - def __init__(self, hass, client, config, async_see): - """Initialize the scanner.""" - self.async_see = async_see - self.hass = hass - self.rssi = config["rssi_threshold"] - self.device_types = config["device_types"] - self.host = config["host"] - self.client = client - - async def async_init(self): - """Further initialize connection to Google Home.""" - await self.client.update_info(self.host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] - info = data.get("info", {}) - connected = bool(info) - if connected: - await self.async_update() - async_track_time_interval( - self.hass, self.async_update, DEFAULT_SCAN_INTERVAL - ) - return connected - - async def async_update(self, now=None): - """Ensure the information from Google Home is up to date.""" - _LOGGER.debug("Checking Devices on %s", self.host) - await self.client.update_bluetooth(self.host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self.host] - info = data.get("info") - bluetooth = data.get("bluetooth") - if info is None or bluetooth is None: - return - google_home_name = info.get("name", NAME) - - for device in bluetooth: - if ( - device["device_type"] not in self.device_types - or device["rssi"] < self.rssi - ): - continue - - name = "{} {}".format(self.host, device["mac_address"]) - - attributes = {} - attributes["btle_mac_address"] = device["mac_address"] - attributes["ghname"] = google_home_name - attributes["rssi"] = device["rssi"] - attributes["source_type"] = "bluetooth" - if device["name"]: - attributes["name"] = device["name"] - - await self.async_see(dev_id=slugify(name), attributes=attributes) diff --git a/homeassistant/components/googlehome/manifest.json b/homeassistant/components/googlehome/manifest.json deleted file mode 100644 index 107e7d634f0..00000000000 --- a/homeassistant/components/googlehome/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "googlehome", - "name": "Googlehome", - "documentation": "https://www.home-assistant.io/components/googlehome", - "requirements": [ - "googledevices==1.0.2" - ], - "dependencies": [], - "codeowners": [ - "@ludeeus" - ] -} diff --git a/homeassistant/components/googlehome/sensor.py b/homeassistant/components/googlehome/sensor.py deleted file mode 100644 index 6a578e14f5a..00000000000 --- a/homeassistant/components/googlehome/sensor.py +++ /dev/null @@ -1,91 +0,0 @@ -"""Support for Google Home alarm sensor.""" -from datetime import timedelta -import logging - -from homeassistant.const import DEVICE_CLASS_TIMESTAMP -from homeassistant.helpers.entity import Entity -import homeassistant.util.dt as dt_util - -from . import CLIENT, DOMAIN as GOOGLEHOME_DOMAIN, NAME - -SCAN_INTERVAL = timedelta(seconds=10) - -_LOGGER = logging.getLogger(__name__) - -ICON = "mdi:alarm" - -SENSOR_TYPES = {"timer": "Timer", "alarm": "Alarm"} - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the googlehome sensor platform.""" - if discovery_info is None: - _LOGGER.warning("To use this you need to configure the 'googlehome' component") - return - - await hass.data[CLIENT].update_info(discovery_info["host"]) - data = hass.data[GOOGLEHOME_DOMAIN][discovery_info["host"]] - info = data.get("info", {}) - - devices = [] - for condition in SENSOR_TYPES: - device = GoogleHomeAlarm( - hass.data[CLIENT], condition, discovery_info, info.get("name", NAME) - ) - devices.append(device) - - async_add_entities(devices, True) - - -class GoogleHomeAlarm(Entity): - """Representation of a GoogleHomeAlarm.""" - - def __init__(self, client, condition, config, name): - """Initialize the GoogleHomeAlarm sensor.""" - self._host = config["host"] - self._client = client - self._condition = condition - self._name = None - self._state = None - self._available = True - self._name = "{} {}".format(name, SENSOR_TYPES[self._condition]) - - async def async_update(self): - """Update the data.""" - await self._client.update_alarms(self._host) - data = self.hass.data[GOOGLEHOME_DOMAIN][self._host] - - alarms = data.get("alarms")[self._condition] - if not alarms: - self._available = False - return - self._available = True - time_date = dt_util.utc_from_timestamp( - min(element["fire_time"] for element in alarms) / 1000 - ) - self._state = time_date.isoformat() - - @property - def state(self): - """Return the state.""" - return self._state - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def device_class(self): - """Return the device class.""" - return DEVICE_CLASS_TIMESTAMP - - @property - def available(self): - """Return the availability state.""" - return self._available - - @property - def icon(self): - """Return the icon.""" - return ICON diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index fc10fa2f737..75b45471982 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -34,7 +34,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.util.async_ import run_coroutine_threadsafe -from .reproduce_state import async_reproduce_states # noqa DOMAIN = "group" diff --git a/homeassistant/components/group/reproduce_state.py b/homeassistant/components/group/reproduce_state.py index f2170c4df16..827e9bb1dcb 100644 --- a/homeassistant/components/group/reproduce_state.py +++ b/homeassistant/components/group/reproduce_state.py @@ -3,10 +3,8 @@ from typing import Iterable, Optional from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.loader import bind_hass -@bind_hass async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None ) -> None: diff --git a/homeassistant/components/hangouts/.translations/es-419.json b/homeassistant/components/hangouts/.translations/es-419.json index 951a30f1826..3a297eb15ea 100644 --- a/homeassistant/components/hangouts/.translations/es-419.json +++ b/homeassistant/components/hangouts/.translations/es-419.json @@ -9,6 +9,9 @@ }, "step": { "2fa": { + "data": { + "2fa": "Pin 2FA" + }, "title": "Autenticaci\u00f3n de 2 factores" }, "user": { diff --git a/homeassistant/components/heos/.translations/es-419.json b/homeassistant/components/heos/.translations/es-419.json index 12ed8cc457a..66c02884a7e 100644 --- a/homeassistant/components/heos/.translations/es-419.json +++ b/homeassistant/components/heos/.translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_setup": "Solo puede configurar una sola conexi\u00f3n Heos, ya que ser\u00e1 compatible con todos los dispositivos de la red." + }, "title": "Heos" } } \ No newline at end of file diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index a5450253be0..20ed7930a4f 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from typing import Dict -from pyheos import CommandError, Heos, const as heos_const +from pyheos import Heos, HeosError, const as heos_const import voluptuous as vol from homeassistant.components.media_player.const import DOMAIN as MEDIA_PLAYER_DOMAIN @@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): try: await controller.connect(auto_reconnect=True) # Auto reconnect only operates if initial connection was successful. - except (asyncio.TimeoutError, ConnectionError, CommandError) as error: + except HeosError as error: await controller.disconnect() _LOGGER.debug("Unable to connect to controller %s: %s", host, error) raise ConfigEntryNotReady @@ -93,13 +93,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): host, ) inputs = await controller.get_input_sources() - except (asyncio.TimeoutError, ConnectionError, CommandError) as error: + except HeosError as error: await controller.disconnect() - _LOGGER.debug( - "Unable to retrieve players and sources: %s", - error, - exc_info=isinstance(error, CommandError), - ) + _LOGGER.debug("Unable to retrieve players and sources: %s", error) raise ConfigEntryNotReady controller_manager = ControllerManager(hass, controller) @@ -187,7 +183,7 @@ class ControllerManager: # Retrieve latest players and refresh status data = await self.controller.load_players() self.update_ids(data[heos_const.DATA_MAPPED_IDS]) - except (CommandError, asyncio.TimeoutError, ConnectionError) as ex: + except HeosError as ex: _LOGGER.error("Unable to refresh players: %s", ex) # Update players self._hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_HEOS_UPDATED) @@ -312,21 +308,15 @@ class SourceManager: favorites = await controller.get_favorites() inputs = await controller.get_input_sources() return favorites, inputs - except (asyncio.TimeoutError, ConnectionError, CommandError) as error: + except HeosError as error: if retry_attempts < self.max_retry_attempts: retry_attempts += 1 _LOGGER.debug( - "Error retrieving sources and will " "retry: %s", - error, - exc_info=isinstance(error, CommandError), + "Error retrieving sources and will " "retry: %s", error ) await asyncio.sleep(self.retry_delay) else: - _LOGGER.error( - "Unable to update sources: %s", - error, - exc_info=isinstance(error, CommandError), - ) + _LOGGER.error("Unable to update sources: %s", error) return async def update_sources(event, data=None): diff --git a/homeassistant/components/heos/config_flow.py b/homeassistant/components/heos/config_flow.py index 7c7f57a91d7..1d56478ba3a 100644 --- a/homeassistant/components/heos/config_flow.py +++ b/homeassistant/components/heos/config_flow.py @@ -1,7 +1,5 @@ """Config flow to configure Heos.""" -import asyncio - -from pyheos import Heos +from pyheos import Heos, HeosError import voluptuous as vol from homeassistant import config_entries @@ -59,7 +57,7 @@ class HeosFlowHandler(config_entries.ConfigFlow): await heos.connect() self.hass.data.pop(DATA_DISCOVERED_HOSTS) return await self.async_step_import({CONF_HOST: host}) - except (asyncio.TimeoutError, ConnectionError): + except HeosError: errors[CONF_HOST] = "connection_failure" finally: await heos.disconnect() diff --git a/homeassistant/components/heos/manifest.json b/homeassistant/components/heos/manifest.json index 09833bb729b..eb9ef258a3c 100644 --- a/homeassistant/components/heos/manifest.json +++ b/homeassistant/components/heos/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/heos", "requirements": [ - "pyheos==0.5.2" + "pyheos==0.6.0" ], "ssdp": { "st": [ @@ -15,4 +15,4 @@ "codeowners": [ "@andrewsayre" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/heos/media_player.py b/homeassistant/components/heos/media_player.py index a4094a0c216..40f6113a80d 100644 --- a/homeassistant/components/heos/media_player.py +++ b/homeassistant/components/heos/media_player.py @@ -1,11 +1,10 @@ """Denon HEOS Media Player.""" -import asyncio from functools import reduce, wraps import logging from operator import ior from typing import Sequence -from pyheos import CommandError, const as heos_const +from pyheos import HeosError, const as heos_const from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -83,12 +82,7 @@ def log_command_error(command: str): async def wrapper(*args, **kwargs): try: await func(*args, **kwargs) - except ( - CommandError, - asyncio.TimeoutError, - ConnectionError, - ValueError, - ) as ex: + except (HeosError, ValueError) as ex: _LOGGER.error("Unable to %s: %s", command, ex) return wrapper diff --git a/homeassistant/components/heos/services.py b/homeassistant/components/heos/services.py index 8f3521399e2..ee5df1b483b 100644 --- a/homeassistant/components/heos/services.py +++ b/homeassistant/components/heos/services.py @@ -1,9 +1,8 @@ """Services for the HEOS integration.""" -import asyncio import functools import logging -from pyheos import CommandError, Heos, const +from pyheos import CommandFailedError, Heos, HeosError, const import voluptuous as vol from homeassistant.helpers import config_validation as cv @@ -57,9 +56,9 @@ async def _sign_in_handler(controller, service): password = service.data[ATTR_PASSWORD] try: await controller.sign_in(username, password) - except CommandError as err: + except CommandFailedError as err: _LOGGER.error("Sign in failed: %s", err) - except (asyncio.TimeoutError, ConnectionError) as err: + except HeosError as err: _LOGGER.error("Unable to sign in: %s", err) @@ -70,5 +69,5 @@ async def _sign_out_handler(controller, service): return try: await controller.sign_out() - except (asyncio.TimeoutError, ConnectionError, CommandError) as err: + except HeosError as err: _LOGGER.error("Unable to sign out: %s", err) diff --git a/homeassistant/components/heos/strings.json b/homeassistant/components/heos/strings.json index b210e0ba87f..9a00ac6a4bd 100644 --- a/homeassistant/components/heos/strings.json +++ b/homeassistant/components/heos/strings.json @@ -1,20 +1,21 @@ { - "config": { - "title": "HEOS", - "step": { - "user": { - "title": "Connect to Heos", - "description": "Please enter the host name or IP address of a Heos device (preferably one connected via wire to the network).", - "data": { - "host": "Host" + "config": { + "title": "HEOS", + "step": { + "user": { + "title": "Connect to Heos", + "description": "Please enter the host name or IP address of a Heos device (preferably one connected via wire to the network).", + "data": { + "access_token": "Host", + "host": "Host" + } + } + }, + "error": { + "connection_failure": "Unable to connect to the specified host." + }, + "abort": { + "already_setup": "You can only configure a single Heos connection as it will support all devices on the network." } - } - }, - "error": { - "connection_failure": "Unable to connect to the specified host." - }, - "abort": { - "already_setup": "You can only configure a single Heos connection as it will support all devices on the network." } - } } \ No newline at end of file diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index d402aceaa40..a2285da4e80 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -21,6 +21,9 @@ from homeassistant.const import ATTR_HIDDEN from homeassistant.components.recorder.util import session_scope, execute import homeassistant.helpers.config_validation as cv + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "history" diff --git a/homeassistant/components/homekit_controller/.translations/es-419.json b/homeassistant/components/homekit_controller/.translations/es-419.json index b058e94e25a..9ddf336c060 100644 --- a/homeassistant/components/homekit_controller/.translations/es-419.json +++ b/homeassistant/components/homekit_controller/.translations/es-419.json @@ -15,6 +15,7 @@ "device": "Dispositivo" } } - } + }, + "title": "Accesorio HomeKit" } } \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/.translations/no.json b/homeassistant/components/homekit_controller/.translations/no.json index 8dd293dc7c8..db8b8b035e0 100644 --- a/homeassistant/components/homekit_controller/.translations/no.json +++ b/homeassistant/components/homekit_controller/.translations/no.json @@ -24,7 +24,7 @@ "data": { "pairing_code": "Sammenkoblingskode" }, - "description": "Skriv inn HomeKit sammenkoblingskoden for \u00e5 bruke dette tilbeh\u00f8ret", + "description": "Skriv inn din HomeKit-sammenkoblingskode (i formatet XXX-XX-XXX) for \u00e5 bruke dette tilbeh\u00f8ret", "title": "Koble til HomeKit tilbeh\u00f8r" }, "user": { diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 79636cea9f3..5ae82d0f124 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -1,6 +1,9 @@ """Support for Homekit device discovery.""" import logging +import homekit +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.exceptions import ConfigEntryNotReady @@ -63,9 +66,6 @@ class HomeKitEntity(Entity): def setup(self): """Configure an entity baed on its HomeKit characterstics metadata.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - accessories = self._accessory.accessories get_uuid = CharacteristicsTypes.get_uuid @@ -95,9 +95,6 @@ class HomeKitEntity(Entity): def _setup_characteristic(self, char): """Configure an entity based on a HomeKit characteristics metadata.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - # Build up a list of (aid, iid) tuples to poll on update() self.pollable_characteristics.append((self._aid, char["iid"])) @@ -211,9 +208,6 @@ async def async_setup_entry(hass, entry): async def async_setup(hass, config): """Set up for Homekit devices.""" - # pylint: disable=import-error - import homekit - map_storage = hass.data[ENTITY_MAP] = EntityMapStorage(hass) await map_storage.async_initialize() diff --git a/homeassistant/components/homekit_controller/alarm_control_panel.py b/homeassistant/components/homekit_controller/alarm_control_panel.py index 38ed064f374..bb45a6c33d9 100644 --- a/homeassistant/components/homekit_controller/alarm_control_panel.py +++ b/homeassistant/components/homekit_controller/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for Homekit Alarm Control Panel.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -64,9 +66,6 @@ class HomeKitAlarmControlPanel(HomeKitEntity, AlarmControlPanel): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.SECURITY_SYSTEM_STATE_CURRENT, CharacteristicsTypes.SECURITY_SYSTEM_STATE_TARGET, diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index d295f607d71..194a2b5a42e 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -1,6 +1,8 @@ """Support for Homekit climate devices.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.climate import ( ClimateDevice, DEFAULT_MIN_HUMIDITY, @@ -84,9 +86,6 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.HEATING_COOLING_CURRENT, CharacteristicsTypes.HEATING_COOLING_TARGET, diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 00214282123..008e0f8566d 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -3,6 +3,7 @@ import os import json import logging +import homekit import voluptuous as vol from homeassistant import config_entries @@ -62,8 +63,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize the homekit_controller flow.""" - import homekit # pylint: disable=import-error - self.model = None self.hkid = None self.devices = {} @@ -224,8 +223,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): async def async_step_pair(self, pair_info=None): """Pair with a new HomeKit accessory.""" - import homekit # pylint: disable=import-error - # If async_step_pair is called with no pairing code then we do the M1 # phase of pairing. If this is successful the device enters pairing # mode. @@ -262,7 +259,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # PairVerify M4 - Ed25519 signature verification failed errors["pairing_code"] = "authentication_error" except homekit.UnknownError: - # An error occured on the device whilst performing this + # An error occurred on the device whilst performing this # operation. errors["pairing_code"] = "unknown_error" except homekit.MaxPeersError: diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index c4c00cb384b..1cb2131fb8f 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -3,6 +3,14 @@ import asyncio import datetime import logging +from homekit.exceptions import ( + AccessoryDisconnectedError, + AccessoryNotFoundError, + EncryptionError, +) +from homekit.model.services import ServicesTypes +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.helpers.event import async_track_time_interval from .const import DOMAIN, HOMEKIT_ACCESSORY_DISPATCH, ENTITY_MAP @@ -16,10 +24,6 @@ _LOGGER = logging.getLogger(__name__) def get_accessory_information(accessory): """Obtain the accessory information service of a HomeKit device.""" - # pylint: disable=import-error - from homekit.model.services import ServicesTypes - from homekit.model.characteristics import CharacteristicsTypes - result = {} for service in accessory["services"]: stype = service["type"].upper() @@ -99,6 +103,10 @@ class HKDevice: # this method. self._polling_interval_remover = None + # Never allow concurrent polling of the same accessory or bridge + self._polling_lock = asyncio.Lock() + self._polling_lock_warned = False + def add_pollable_characteristics(self, characteristics): """Add (aid, iid) pairs that we need to poll.""" self.pollable_characteristics.extend(characteristics) @@ -163,9 +171,6 @@ class HKDevice: async def async_refresh_entity_map(self, config_num): """Handle setup of a HomeKit accessory.""" - # pylint: disable=import-error - from homekit.exceptions import AccessoryDisconnectedError - try: async with self.pairing_lock: self.accessories = await self.hass.async_add_executor_job( @@ -205,8 +210,6 @@ class HKDevice: self._add_new_entities(self.listeners) def _add_new_entities(self, callbacks): - from homekit.model.services import ServicesTypes - for accessory in self.accessories: aid = accessory["aid"] for service in accessory["services"]: @@ -225,8 +228,6 @@ class HKDevice: def async_load_platforms(self): """Load any platforms needed by this HomeKit device.""" - from homekit.model.services import ServicesTypes - for accessory in self.accessories: for service in accessory["services"]: stype = ServicesTypes.get_short(service["type"].upper()) @@ -246,33 +247,47 @@ class HKDevice: async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" - # pylint: disable=import-error - from homekit.exceptions import ( - AccessoryDisconnectedError, - AccessoryNotFoundError, - EncryptionError, - ) - if not self.pollable_characteristics: _LOGGER.debug("HomeKit connection not polling any characteristics.") return - _LOGGER.debug("Starting HomeKit controller update") + if self._polling_lock.locked(): + if not self._polling_lock_warned: + _LOGGER.warning( + "HomeKit controller update skipped as previous poll still in flight" + ) + self._polling_lock_warned = True + return - try: - new_values_dict = await self.get_characteristics( - self.pollable_characteristics + if self._polling_lock_warned: + _LOGGER.info( + "HomeKit controller no longer detecting back pressure - not skipping poll" ) - except AccessoryNotFoundError: - # Not only did the connection fail, but also the accessory is not - # visible on the network. - self.async_set_unavailable() - return - except (AccessoryDisconnectedError, EncryptionError): - # Temporary connection failure. Device is still available but our - # connection was dropped. - return + self._polling_lock_warned = False + async with self._polling_lock: + _LOGGER.debug("Starting HomeKit controller update") + + try: + new_values_dict = await self.get_characteristics( + self.pollable_characteristics + ) + except AccessoryNotFoundError: + # Not only did the connection fail, but also the accessory is not + # visible on the network. + self.async_set_unavailable() + return + except (AccessoryDisconnectedError, EncryptionError): + # Temporary connection failure. Device is still available but our + # connection was dropped. + return + + self.process_new_events(new_values_dict) + + _LOGGER.debug("Finished HomeKit controller update") + + def process_new_events(self, new_values_dict): + """Process events from accessory into HA state.""" self.available = True for (aid, cid), value in new_values_dict.items(): @@ -281,8 +296,6 @@ class HKDevice: self.hass.helpers.dispatcher.async_dispatcher_send(self.signal_state_updated) - _LOGGER.debug("Finished HomeKit controller update") - async def get_characteristics(self, *args, **kwargs): """Read latest state from homekit accessory.""" async with self.pairing_lock: @@ -298,10 +311,30 @@ class HKDevice: chars.append((row["aid"], row["iid"], row["value"])) async with self.pairing_lock: - await self.hass.async_add_executor_job( + results = await self.hass.async_add_executor_job( self.pairing.put_characteristics, chars ) + # Feed characteristics back into HA and update the current state + # results will only contain failures, so anythin in characteristics + # but not in results was applied successfully - we can just have HA + # reflect the change immediately. + + new_entity_state = {} + for row in characteristics: + key = (row["aid"], row["iid"]) + + # If the key was returned by put_characteristics() then the + # change didnt work + if key in results: + continue + + # Otherwise it was accepted and we can apply the change to + # our state + new_entity_state[key] = {"value": row["value"]} + + self.process_new_events(new_entity_state) + @property def unique_id(self): """ diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index 924d27218a2..6aa5dc93662 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -21,6 +21,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { "lock-mechanism": "lock", "contact": "binary_sensor", "motion": "binary_sensor", + "carbon-dioxide": "sensor", "humidity": "sensor", "light": "sensor", "temperature": "sensor", diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index c15e0c092ac..7f70b0cfac0 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -1,6 +1,8 @@ """Support for Homekit covers.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, @@ -76,9 +78,6 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.DOOR_STATE_CURRENT, CharacteristicsTypes.DOOR_STATE_TARGET, @@ -154,9 +153,6 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.POSITION_STATE, CharacteristicsTypes.POSITION_CURRENT, diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 534a8c7cd18..fe2a0e9bc97 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -1,6 +1,8 @@ """Support for Homekit lights.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -50,9 +52,6 @@ class HomeKitLight(HomeKitEntity, Light): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.ON, CharacteristicsTypes.BRIGHTNESS, diff --git a/homeassistant/components/homekit_controller/lock.py b/homeassistant/components/homekit_controller/lock.py index 4ca118acee6..53f7bb5dfd5 100644 --- a/homeassistant/components/homekit_controller/lock.py +++ b/homeassistant/components/homekit_controller/lock.py @@ -1,6 +1,8 @@ """Support for HomeKit Controller locks.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_LOCKED, STATE_UNLOCKED @@ -46,9 +48,6 @@ class HomeKitLock(HomeKitEntity, LockDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [ CharacteristicsTypes.LOCK_MECHANISM_CURRENT_STATE, CharacteristicsTypes.LOCK_MECHANISM_TARGET_STATE, diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 16109f08c10..596b697bede 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -1,4 +1,6 @@ """Support for Homekit sensors.""" +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.const import TEMP_CELSIUS from . import KNOWN_DEVICES, HomeKitEntity @@ -6,39 +8,11 @@ from . import KNOWN_DEVICES, HomeKitEntity HUMIDITY_ICON = "mdi:water-percent" TEMP_C_ICON = "mdi:thermometer" BRIGHTNESS_ICON = "mdi:brightness-6" +CO2_ICON = "mdi:periodic-table-co2" UNIT_PERCENT = "%" UNIT_LUX = "lux" - - -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 covers.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] - - def async_add_service(aid, service): - devtype = service["stype"] - info = {"aid": aid, "iid": service["iid"]} - if devtype == "humidity": - async_add_entities([HomeKitHumiditySensor(conn, info)], True) - return True - - if devtype == "temperature": - async_add_entities([HomeKitTemperatureSensor(conn, info)], True) - return True - - if devtype == "light": - async_add_entities([HomeKitLightSensor(conn, info)], True) - return True - - return False - - conn.add_listener(async_add_service) +UNIT_CO2 = "ppm" class HomeKitHumiditySensor(HomeKitEntity): @@ -51,9 +25,6 @@ class HomeKitHumiditySensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [CharacteristicsTypes.RELATIVE_HUMIDITY_CURRENT] @property @@ -90,9 +61,6 @@ class HomeKitTemperatureSensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [CharacteristicsTypes.TEMPERATURE_CURRENT] @property @@ -129,9 +97,6 @@ class HomeKitLightSensor(HomeKitEntity): def get_characteristic_types(self): """Define the homekit characteristics the entity is tracking.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [CharacteristicsTypes.LIGHT_LEVEL_CURRENT] @property @@ -156,3 +121,68 @@ class HomeKitLightSensor(HomeKitEntity): def state(self): """Return the current light level in lux.""" return self._state + + +class HomeKitCarbonDioxideSensor(HomeKitEntity): + """Representation of a Homekit Carbon Dioxide sensor.""" + + def __init__(self, *args): + """Initialise the entity.""" + super().__init__(*args) + self._state = None + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.CARBON_DIOXIDE_LEVEL] + + @property + def name(self): + """Return the name of the device.""" + return "{} {}".format(super().name, "CO2") + + @property + def icon(self): + """Return the sensor icon.""" + return CO2_ICON + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return UNIT_CO2 + + def _update_carbon_dioxide_level(self, value): + self._state = value + + @property + def state(self): + """Return the current CO2 level in ppm.""" + return self._state + + +ENTITY_TYPES = { + "humidity": HomeKitHumiditySensor, + "temperature": HomeKitTemperatureSensor, + "light": HomeKitLightSensor, + "carbon-dioxide": HomeKitCarbonDioxideSensor, +} + + +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 sensors.""" + 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/switch.py b/homeassistant/components/homekit_controller/switch.py index 7fdc6a7082f..7eedda1b191 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -1,6 +1,8 @@ """Support for Homekit switches.""" import logging +from homekit.model.characteristics import CharacteristicsTypes + from homeassistant.components.switch import SwitchDevice from . import KNOWN_DEVICES, HomeKitEntity @@ -41,9 +43,6 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" - # pylint: disable=import-error - from homekit.model.characteristics import CharacteristicsTypes - return [CharacteristicsTypes.ON, CharacteristicsTypes.OUTLET_IN_USE] def _update_on(self, value): diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 008055b649a..1a2f642f91c 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -69,7 +69,7 @@ class HMThermostat(HMDevice, ClimateDevice): if self.target_temperature <= self._hmdevice.OFF_VALUE + 0.5: return HVAC_MODE_OFF if "MANU_MODE" in self._hmdevice.ACTIONNODE: - if self._hm_controll_mode == self._hmdevice.MANU_MODE: + if self._hm_control_mode == self._hmdevice.MANU_MODE: return HVAC_MODE_HEAT return HVAC_MODE_AUTO @@ -95,7 +95,7 @@ class HMThermostat(HMDevice, ClimateDevice): return "boost" # Get the name of the mode - mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_controll_mode] + mode = HM_ATTRIBUTE_SUPPORT[HM_CONTROL_MODE][1][self._hm_control_mode] mode = mode.lower() # Filter HVAC states @@ -173,7 +173,7 @@ class HMThermostat(HMDevice, ClimateDevice): return 0.5 @property - def _hm_controll_mode(self): + def _hm_control_mode(self): """Return Control mode.""" if HMIP_CONTROL_MODE in self._data: return self._data[HMIP_CONTROL_MODE] diff --git a/homeassistant/components/homematicip_cloud/.translations/hr.json b/homeassistant/components/homematicip_cloud/.translations/hr.json new file mode 100644 index 00000000000..648dbfe73f9 --- /dev/null +++ b/homeassistant/components/homematicip_cloud/.translations/hr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Do\u0161lo je do nepoznate pogre\u0161ke." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 7bb7718f0b3..97746f3f472 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -38,7 +38,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -309,7 +309,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - attr = super().device_state_attributes + attr = {ATTR_MODEL_TYPE: self._device.modelType} if self._device.motionDetected: attr[ATTR_MOTIONDETECTED] = True diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 021c264f63f..b086eaa29c7 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -11,12 +11,35 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" +ATTR_ID = "id" # RSSI HAP -> Device ATTR_RSSI_DEVICE = "rssi_device" # RSSI Device -> HAP ATTR_RSSI_PEER = "rssi_peer" ATTR_SABOTAGE = "sabotage" ATTR_GROUP_MEMBER_UNREACHABLE = "group_member_unreachable" +ATTR_DEVICE_OVERHEATED = "device_overheated" +ATTR_DEVICE_OVERLOADED = "device_overloaded" +ATTR_DEVICE_UNTERVOLTAGE = "device_undervoltage" + +DEVICE_ATTRIBUTE_ICONS = { + "lowBat": "mdi:battery-outline", + "sabotage": "mdi:alert", + "deviceOverheated": "mdi:alert", + "deviceOverloaded": "mdi:alert", + "deviceUndervoltage": "mdi:alert", +} + +DEVICE_ATTRIBUTES = { + "modelType": ATTR_MODEL_TYPE, + "id": ATTR_ID, + "sabotage": ATTR_SABOTAGE, + "rssiDeviceValue": ATTR_RSSI_DEVICE, + "rssiPeerValue": ATTR_RSSI_PEER, + "deviceOverheated": ATTR_DEVICE_OVERHEATED, + "deviceOverloaded": ATTR_DEVICE_OVERLOADED, + "deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE, +} class HomematicipGenericDevice(Entity): @@ -84,20 +107,20 @@ class HomematicipGenericDevice(Entity): @property def icon(self) -> Optional[str]: """Return the icon.""" - if hasattr(self._device, "lowBat") and self._device.lowBat: - return "mdi:battery-outline" - if hasattr(self._device, "sabotage") and self._device.sabotage: - return "mdi:alert" + for attr, icon in DEVICE_ATTRIBUTE_ICONS.items(): + if getattr(self._device, attr, None): + return icon + return None @property def device_state_attributes(self): """Return the state attributes of the generic device.""" - attr = {ATTR_MODEL_TYPE: self._device.modelType} - if hasattr(self._device, "sabotage") and self._device.sabotage: - attr[ATTR_SABOTAGE] = self._device.sabotage - if hasattr(self._device, "rssiDeviceValue") and self._device.rssiDeviceValue: - attr[ATTR_RSSI_DEVICE] = self._device.rssiDeviceValue - if hasattr(self._device, "rssiPeerValue") and self._device.rssiPeerValue: - attr[ATTR_RSSI_PEER] = self._device.rssiPeerValue - return attr + state_attr = {} + if isinstance(self._device, AsyncDevice): + for attr, attr_key in DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + return state_attr diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index b3cbf4627d4..c15b3121d3a 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -34,6 +34,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .device import ATTR_MODEL_TYPE _LOGGER = logging.getLogger(__name__) @@ -142,9 +143,14 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): """Return the unit this state is expressed in.""" return "%" + @property + def device_state_attributes(self): + """Return the state attributes of the security zone group.""" + return {ATTR_MODEL_TYPE: self._device.modelType} + class HomematicipHeatingThermostat(HomematicipGenericDevice): - """Represenation of a HomematicIP heating thermostat device.""" + """Representation of a HomematicIP heating thermostat device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize heating thermostat device.""" @@ -173,7 +179,7 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): class HomematicipHumiditySensor(HomematicipGenericDevice): - """Represenation of a HomematicIP Cloud humidity device.""" + """Representation of a HomematicIP Cloud humidity device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the thermometer device.""" @@ -233,7 +239,7 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): class HomematicipIlluminanceSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP Illuminance device.""" + """Representation of a HomematicIP Illuminance device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" @@ -259,7 +265,7 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): class HomematicipPowerSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP power measuring device.""" + """Representation of a HomematicIP power measuring device.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" @@ -272,7 +278,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Represenation of the HomematicIP power comsumption value.""" + """Representation of the HomematicIP power comsumption value.""" return self._device.currentPowerConsumption @property @@ -282,7 +288,7 @@ class HomematicipPowerSensor(HomematicipGenericDevice): class HomematicipWindspeedSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP wind speed sensor.""" + """Representation of a HomematicIP wind speed sensor.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" @@ -290,7 +296,7 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Represenation of the HomematicIP wind speed value.""" + """Representation of the HomematicIP wind speed value.""" return self._device.windSpeed @property @@ -313,7 +319,7 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): class HomematicipTodayRainSensor(HomematicipGenericDevice): - """Represenation of a HomematicIP rain counter of a day sensor.""" + """Representation of a HomematicIP rain counter of a day sensor.""" def __init__(self, home: AsyncHome, device) -> None: """Initialize the device.""" @@ -321,7 +327,7 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): @property def state(self) -> float: - """Represenation of the HomematicIP todays rain value.""" + """Representation of the HomematicIP todays rain value.""" return round(self._device.todayRainCounter, 2) @property diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 6d31c3fc700..5e474dafa07 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -27,6 +27,9 @@ from .real_ip import setup_real_ip from .static import CACHE_HEADERS, CachingStaticResource from .view import HomeAssistantView # noqa + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "http" CONF_API_PASSWORD = "api_password" diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index c65cb6a2e94..4ff581aef02 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -14,6 +14,9 @@ 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__) DATA_API_PASSWORD = "api_password" diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index db8d2ade959..71e7ff38924 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -3,6 +3,7 @@ from collections import defaultdict from datetime import datetime from ipaddress import ip_address import logging +from typing import List, Optional from aiohttp.web import middleware from aiohttp.web_exceptions import HTTPForbidden, HTTPUnauthorized @@ -16,6 +17,9 @@ from homeassistant.util.yaml import dump from .const import KEY_REAL_IP + +# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) KEY_BANNED_IPS = "ha_banned_ips" @@ -155,7 +159,7 @@ async def process_success_login(request): class IpBan: """Represents banned IP address.""" - def __init__(self, ip_ban: str, banned_at: datetime = None) -> None: + def __init__(self, ip_ban: str, banned_at: Optional[datetime] = None) -> None: """Initialize IP Ban object.""" self.ip_address = ip_address(ip_ban) self.banned_at = banned_at or datetime.utcnow() @@ -163,7 +167,7 @@ class IpBan: async def async_load_ip_bans_config(hass: HomeAssistant, path: str): """Load list of banned IPs from config file.""" - ip_list = [] + ip_list: List[IpBan] = [] try: list_ = await hass.async_add_executor_job(load_yaml_config_file, path) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 19fe88c5cde..39ff45fd4e4 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -5,6 +5,9 @@ from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION from homeassistant.const import HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback + +# mypy: allow-untyped-defs, no-check-untyped-defs + ALLOWED_CORS_HEADERS = [ ORIGIN, ACCEPT, diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 8d6ac0b1ceb..634a96aa312 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -4,6 +4,9 @@ import logging import voluptuous as vol + +# mypy: allow-untyped-defs + _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/http/real_ip.py b/homeassistant/components/http/real_ip.py index c38e5d0b592..f327c86a4c1 100644 --- a/homeassistant/components/http/real_ip.py +++ b/homeassistant/components/http/real_ip.py @@ -9,6 +9,9 @@ from homeassistant.core import callback from .const import KEY_REAL_IP +# mypy: allow-untyped-defs + + @callback def setup_real_ip(app, use_x_forwarded_for, trusted_proxies): """Create IP Ban middleware for the app.""" diff --git a/homeassistant/components/http/static.py b/homeassistant/components/http/static.py index f78ce81d884..76844407f7d 100644 --- a/homeassistant/components/http/static.py +++ b/homeassistant/components/http/static.py @@ -6,6 +6,9 @@ from aiohttp.web import FileResponse from aiohttp.web_exceptions import HTTPNotFound, HTTPForbidden from aiohttp.web_urldispatcher import StaticResource + +# mypy: allow-untyped-defs + CACHE_TIME = 31 * 86400 # = 1 month CACHE_HEADERS = {hdrs.CACHE_CONTROL: "public, max-age={}".format(CACHE_TIME)} @@ -39,7 +42,8 @@ class CachingStaticResource(StaticResource): if filepath.is_dir(): return await super()._handle(request) if filepath.is_file(): - return FileResponse( + # type ignore: https://github.com/aio-libs/aiohttp/pull/3976 + return FileResponse( # type: ignore filepath, chunk_size=self._chunk_size, headers=CACHE_HEADERS ) raise HTTPNotFound diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 35e74b7c2c0..66864eba55e 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -2,6 +2,7 @@ import asyncio import json import logging +from typing import List, Optional from aiohttp import web from aiohttp.web_exceptions import ( @@ -22,11 +23,14 @@ from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP _LOGGER = logging.getLogger(__name__) +# mypy: allow-untyped-defs, no-check-untyped-defs + + class HomeAssistantView: """Base view for all views.""" - url = None - extra_urls = [] + url: Optional[str] = None + extra_urls: List[str] = [] # Views inheriting from this class can override this requires_auth = True cors_allowed = False diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c68231ba0e4..2cbc271219b 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -1,12 +1,15 @@ """Support for Huawei LTE routers.""" from datetime import timedelta from functools import reduce +from urllib.parse import urlparse +import ipaddress import logging import operator from typing import Any, Callable import voluptuous as vol import attr +from getmac import get_mac_address from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client from huawei_lte_api.exceptions import ResponseErrorNotSupportedException @@ -55,6 +58,7 @@ class RouterData: """Class for router state.""" client = attr.ib() + mac = attr.ib() device_information = attr.ib(init=False, factory=dict) device_signal = attr.ib(init=False, factory=dict) monitoring_traffic_statistics = attr.ib(init=False, factory=dict) @@ -62,12 +66,6 @@ class RouterData: _subscriptions = attr.ib(init=False, factory=set) - def __attrs_post_init__(self) -> None: - """Fetch device information once, for serial number in @unique_ids.""" - self.subscribe("device_information") - self._update() - self.unsubscribe("device_information") - def __getitem__(self, path: str): """ Get value corresponding to a dotted path. @@ -103,8 +101,8 @@ class RouterData: if debugging or path in self._subscriptions: try: setattr(self, path, func()) - except ResponseErrorNotSupportedException as ex: - _LOGGER.warning("%s not supported by device", path, exc_info=ex) + except ResponseErrorNotSupportedException: + _LOGGER.warning("%s not supported by device", path) self._subscriptions.discard(path) finally: _LOGGER.debug("%s=%s", path, getattr(self, path)) @@ -148,10 +146,24 @@ def _setup_lte(hass, lte_config) -> None: username = lte_config[CONF_USERNAME] password = lte_config[CONF_PASSWORD] + # Get MAC address for use in unique ids. Being able to use something + # from the API would be nice, but all of that seems to be available only + # through authenticated calls (e.g. device_information.SerialNumber), and + # we want this available and the same when unauthenticated too. + host = urlparse(url).hostname + try: + if ipaddress.ip_address(host).version == 6: + mode = "ip6" + else: + mode = "ip" + except ValueError: + mode = "hostname" + mac = get_mac_address(**{mode: host}) + connection = AuthorizedConnection(url, username=username, password=password) client = Client(connection) - data = RouterData(client) + data = RouterData(client, mac) hass.data[DATA_KEY].data[url] = data def cleanup(event): diff --git a/homeassistant/components/huawei_lte/device_tracker.py b/homeassistant/components/huawei_lte/device_tracker.py index 878a819aaae..697b2a3ed3c 100644 --- a/homeassistant/components/huawei_lte/device_tracker.py +++ b/homeassistant/components/huawei_lte/device_tracker.py @@ -1,4 +1,5 @@ """Support for device tracking of Huawei LTE routers.""" +import logging from typing import Any, Dict, List, Optional import attr @@ -9,9 +10,12 @@ from homeassistant.components.device_tracker import PLATFORM_SCHEMA, DeviceScann from homeassistant.const import CONF_URL from . import DATA_KEY, RouterData + +_LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Optional(CONF_URL): cv.url}) -HOSTS_PATH = "wlan_host_list.Hosts" +HOSTS_PATH = "wlan_host_list.Hosts.Host" def get_scanner(hass, config): @@ -32,11 +36,12 @@ class HuaweiLteScanner(DeviceScanner): def scan_devices(self) -> List[str]: """Scan for devices.""" self.data.update() - self._hosts = { - x["MacAddress"]: x - for x in self.data[HOSTS_PATH + ".Host"] - if x.get("MacAddress") - } + try: + self._hosts = { + x["MacAddress"]: x for x in self.data[HOSTS_PATH] if x.get("MacAddress") + } + except KeyError: + _LOGGER.debug("%s not in data", HOSTS_PATH) return list(self._hosts) def get_device_name(self, device: str) -> Optional[str]: diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index bfdc6f167aa..85077511768 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -1,8 +1,9 @@ { "domain": "huawei_lte", - "name": "Huawei lte", + "name": "Huawei LTE", "documentation": "https://www.home-assistant.io/components/huawei_lte", "requirements": [ + "getmac==0.8.1", "huawei-lte-api==1.2.0" ], "dependencies": [], diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 02ccff82c52..da78dc7d8cf 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -11,6 +11,7 @@ from homeassistant.components.sensor import ( PLATFORM_SCHEMA, DEVICE_CLASS_SIGNAL_STRENGTH, ) +from homeassistant.helpers import entity_registry from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv @@ -101,7 +102,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Huawei LTE sensor devices.""" data = hass.data[DATA_KEY].get_data(config) sensors = [] @@ -111,7 +112,24 @@ def setup_platform(hass, config, add_entities, discovery_info): data.subscribe(path) sensors.append(HuaweiLteSensor(data, path, SENSOR_META.get(path, {}))) - add_entities(sensors, True) + # Pre-0.97 unique id migration. Old ones used the device serial number + # (see comments in HuaweiLteData._setup_lte for more info), as well as + # had a bug that joined the path str with periods, not the path components, + # resulting e.g. *_device_signal.sinr to end up as + # *_d.e.v.i.c.e._.s.i.g.n.a.l...s.i.n.r + entreg = await entity_registry.async_get_registry(hass) + for entid, ent in entreg.entities.items(): + if ent.platform != "huawei_lte": + continue + for sensor in sensors: + oldsuf = ".".join(sensor.path) + if ent.unique_id.endswith(f"_{oldsuf}"): + entreg.async_update_entity(entid, new_unique_id=sensor.unique_id) + _LOGGER.debug( + "Updated entity %s unique id to %s", entid, sensor.unique_id + ) + + async_add_entities(sensors, True) def format_default(value): @@ -119,7 +137,9 @@ def format_default(value): unit = None if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB - match = re.match(r"(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value)) + match = re.match( + r"([>=<]*)(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) + ) if match: try: value = float(match.group("value")) @@ -134,7 +154,7 @@ class HuaweiLteSensor(Entity): """Huawei LTE sensor entity.""" data = attr.ib(type=RouterData) - path = attr.ib(type=list) + path = attr.ib(type=str) meta = attr.ib(type=dict) _state = attr.ib(init=False, default=STATE_UNKNOWN) @@ -143,9 +163,7 @@ class HuaweiLteSensor(Entity): @property def unique_id(self) -> str: """Return unique ID for sensor.""" - return "{}_{}".format( - self.data["device_information.SerialNumber"], ".".join(self.path) - ) + return "{}-{}".format(self.data.mac, self.path) @property def name(self) -> str: @@ -187,7 +205,7 @@ class HuaweiLteSensor(Entity): try: value = self.data[self.path] except KeyError: - _LOGGER.warning("%s not in data", self.path) + _LOGGER.debug("%s not in data", self.path) value = None formatter = self.meta.get("formatter") diff --git a/homeassistant/components/hue/.translations/es-419.json b/homeassistant/components/hue/.translations/es-419.json index 8efc9101d9a..48a2ff233da 100644 --- a/homeassistant/components/hue/.translations/es-419.json +++ b/homeassistant/components/hue/.translations/es-419.json @@ -6,6 +6,7 @@ "cannot_connect": "No se puede conectar al puente", "discover_timeout": "Incapaz de descubrir puentes Hue", "no_bridges": "No se descubrieron puentes Philips Hue", + "not_hue_bridge": "No es un puente Hue", "unknown": "Se produjo un error desconocido" }, "error": { diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json index ddb647c18ed..55b308b2373 100644 --- a/homeassistant/components/hue/.translations/fr.json +++ b/homeassistant/components/hue/.translations/fr.json @@ -7,6 +7,7 @@ "cannot_connect": "Connexion au pont impossible", "discover_timeout": "D\u00e9tection de ponts Philips Hue impossible", "no_bridges": "Aucun pont Philips Hue n'a \u00e9t\u00e9 d\u00e9couvert", + "not_hue_bridge": "Pas de pont Hue", "unknown": "Une erreur inconnue s'est produite" }, "error": { diff --git a/homeassistant/components/hue/.translations/hr.json b/homeassistant/components/hue/.translations/hr.json new file mode 100644 index 00000000000..16a1b19ff8e --- /dev/null +++ b/homeassistant/components/hue/.translations/hr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "linking": "Do\u0161lo je do nepoznate pogre\u0161ke u povezivanju.", + "register_failed": "Registracija nije uspjela. Poku\u0161ajte ponovo" + }, + "step": { + "init": { + "data": { + "host": "Host" + } + } + }, + "title": "Philips Hue" + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index d11b38dd69c..6a654744397 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -166,7 +166,7 @@ async def get_bridge(hass, host, username=None): with async_timeout.timeout(10): # Create username if we don't have one if not username: - await bridge.create_user("home-assistant") + await bridge.create_user(f"home-assistant#{hass.config.location_name}") # Initialize bridge (and validate our username) await bridge.initialize() diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 829ea814d27..0b0e3723b13 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -44,8 +44,7 @@ def _find_username_from_config(hass, filename): return None -@config_entries.HANDLERS.register(DOMAIN) -class HueFlowHandler(config_entries.ConfigFlow): +class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Hue config flow.""" VERSION = 1 @@ -62,7 +61,7 @@ class HueFlowHandler(config_entries.ConfigFlow): async def async_step_init(self, user_input=None): """Handle a flow start.""" if user_input is not None: - self.host = user_input["host"] + self.host = self.context["host"] = user_input["host"] return await self.async_step_link() websession = aiohttp_client.async_get_clientsession(self.hass) @@ -141,10 +140,11 @@ class HueFlowHandler(config_entries.ConfigFlow): if "HASS Bridge" in discovery_info.get("name", ""): return self.async_abort(reason="already_configured") - # pylint: disable=unsupported-assignment-operation host = self.context["host"] = discovery_info.get("host") - if any(host == flow["context"]["host"] for flow in self._async_in_progress()): + 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): @@ -166,10 +166,11 @@ class HueFlowHandler(config_entries.ConfigFlow): async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" - # pylint: disable=unsupported-assignment-operation host = self.context["host"] = homekit_info.get("host") - if any(host == flow["context"]["host"] for flow in self._async_in_progress()): + 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): @@ -192,7 +193,7 @@ class HueFlowHandler(config_entries.ConfigFlow): and create an entry. Otherwise we will delegate to `link` step which will ask user to link the bridge. """ - host = import_info["host"] + host = self.context["host"] = import_info["host"] path = import_info.get("path") if path is not None: diff --git a/homeassistant/components/ifttt/.translations/hr.json b/homeassistant/components/ifttt/.translations/hr.json new file mode 100644 index 00000000000..077956287b3 --- /dev/null +++ b/homeassistant/components/ifttt/.translations/hr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "IFTTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/image_processing/__init__.py b/homeassistant/components/image_processing/__init__.py index 1f106e12dcd..b1c167a4175 100644 --- a/homeassistant/components/image_processing/__init__.py +++ b/homeassistant/components/image_processing/__init__.py @@ -14,6 +14,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.util.async_ import run_callback_threadsafe + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "image_processing" diff --git a/homeassistant/components/input_boolean/reproduce_state.py b/homeassistant/components/input_boolean/reproduce_state.py new file mode 100644 index 00000000000..b8bc18edfac --- /dev/null +++ b/homeassistant/components/input_boolean/reproduce_state.py @@ -0,0 +1,57 @@ +"""Reproduce an input boolean state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + STATE_OFF, + ATTR_ENTITY_ID, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_states( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce input boolean states.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in (STATE_ON, STATE_OFF): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + if cur_state.state == state.state: + return + + service = SERVICE_TURN_ON if state.state == STATE_ON else SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, + service, + {ATTR_ENTITY_ID: state.entity_id}, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce component states.""" + await asyncio.gather( + *(_async_reproduce_states(hass, state, context) for state in states) + ) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 006e0fe9a41..d24b70c4be0 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -16,6 +16,9 @@ from homeassistant.core import callback 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__) ATTR_SOURCE_ID = "source" diff --git a/homeassistant/components/ipma/config_flow.py b/homeassistant/components/ipma/config_flow.py index c0bc383abc0..d1532066f68 100644 --- a/homeassistant/components/ipma/config_flow.py +++ b/homeassistant/components/ipma/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure IPMA component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.helpers.config_validation as cv @@ -9,7 +9,7 @@ from .const import DOMAIN, HOME_LOCATION_NAME @config_entries.HANDLERS.register(DOMAIN) -class IpmaFlowHandler(data_entry_flow.FlowHandler): +class IpmaFlowHandler(config_entries.ConfigFlow): """Config flow for IPMA component.""" VERSION = 1 diff --git a/homeassistant/components/iqvia/.translations/es-419.json b/homeassistant/components/iqvia/.translations/es-419.json new file mode 100644 index 00000000000..b107e1bb696 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" + }, + "step": { + "user": { + "data": { + "zip_code": "C\u00f3digo postal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/id.json b/homeassistant/components/iqvia/.translations/id.json new file mode 100644 index 00000000000..a93f9aac26f --- /dev/null +++ b/homeassistant/components/iqvia/.translations/id.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zip_code": "Kode Pos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 6104d4415f9..357bfca607a 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/iqvia", "requirements": [ - "numpy==1.16.4", + "numpy==1.17.0", "pyiqvia==0.2.1" ], "dependencies": [], diff --git a/homeassistant/components/islamic_prayer_times/sensor.py b/homeassistant/components/islamic_prayer_times/sensor.py index 44a27963971..88cbd2cb431 100644 --- a/homeassistant/components/islamic_prayer_times/sensor.py +++ b/homeassistant/components/islamic_prayer_times/sensor.py @@ -89,6 +89,7 @@ async def schedule_future_update(hass, sensors, midnight_time, prayer_times_data calculated midnight = 1:35AM (after traditional midnight) update time: 1:36AM. + """ _LOGGER.debug("Scheduling next update for Islamic prayer times") diff --git a/homeassistant/components/keba/__init__.py b/homeassistant/components/keba/__init__.py new file mode 100644 index 00000000000..5a9a49a005a --- /dev/null +++ b/homeassistant/components/keba/__init__.py @@ -0,0 +1,229 @@ +"""Support for KEBA charging stations.""" +import asyncio +import logging + +from keba_kecontact.connection import KebaKeContact +import voluptuous as vol + +from homeassistant.const import CONF_HOST +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "keba" +SUPPORTED_COMPONENTS = ["binary_sensor", "sensor", "lock"] + +CONF_RFID = "rfid" +CONF_FS = "failsafe" +CONF_FS_TIMEOUT = "failsafe_timeout" +CONF_FS_FALLBACK = "failsafe_fallback" +CONF_FS_PERSIST = "failsafe_persist" +CONF_FS_INTERVAL = "refresh_interval" + +MAX_POLLING_INTERVAL = 5 # in seconds +MAX_FAST_POLLING_COUNT = 4 + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_RFID, default="00845500"): cv.string, + vol.Optional(CONF_FS, default=False): cv.boolean, + vol.Optional(CONF_FS_TIMEOUT, default=30): cv.positive_int, + vol.Optional(CONF_FS_FALLBACK, default=6): cv.positive_int, + vol.Optional(CONF_FS_PERSIST, default=0): cv.positive_int, + vol.Optional(CONF_FS_INTERVAL, default=5): cv.positive_int, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +_SERVICE_MAP = { + "request_data": "request_data", + "set_energy": "async_set_energy", + "set_current": "async_set_current", + "authorize": "async_start", + "deauthorize": "async_stop", + "enable": "async_enable_ev", + "disable": "async_disable_ev", + "set_failsafe": "async_set_failsafe", +} + + +async def async_setup(hass, config): + """Check connectivity and version of KEBA charging station.""" + host = config[DOMAIN][CONF_HOST] + rfid = config[DOMAIN][CONF_RFID] + refresh_interval = config[DOMAIN][CONF_FS_INTERVAL] + keba = KebaHandler(hass, host, rfid, refresh_interval) + hass.data[DOMAIN] = keba + + # Wait for KebaHandler setup complete (initial values loaded) + if not await keba.setup(): + _LOGGER.error("Could not find a charging station at %s", host) + return False + + # 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 + persist = config[DOMAIN][CONF_FS_PERSIST] if failsafe else 0 + try: + hass.loop.create_task(keba.set_failsafe(timeout, fallback, persist)) + except ValueError as ex: + _LOGGER.warning("Could not set failsafe mode %s", ex) + + # Register services to hass + async def execute_service(call): + """Execute a service to KEBA charging station. + + This must be a member function as we need access to the keba + object here. + """ + function_name = _SERVICE_MAP[call.service] + function_call = getattr(keba, function_name) + await function_call(call.data) + + for service in _SERVICE_MAP: + hass.services.async_register(DOMAIN, service, execute_service) + + # Load components + for domain in SUPPORTED_COMPONENTS: + hass.async_create_task( + discovery.async_load_platform(hass, domain, DOMAIN, {}, config) + ) + + # Start periodic polling of charging station data + keba.start_periodic_request() + + return True + + +class KebaHandler(KebaKeContact): + """Representation of a KEBA charging station connection.""" + + def __init__(self, hass, host, rfid, refresh_interval): + """Constructor.""" + super().__init__(host, self.hass_callback) + + self._update_listeners = [] + self._hass = hass + self.rfid = rfid + self.device_name = "keba_wallbox_" + + # Ensure at least MAX_POLLING_INTERVAL seconds delay + self._refresh_interval = max(MAX_POLLING_INTERVAL, refresh_interval) + self._fast_polling_count = MAX_FAST_POLLING_COUNT + self._polling_task = None + + def start_periodic_request(self): + """Start periodic data polling.""" + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + async def _periodic_request(self): + """Send periodic update requests.""" + await self.request_data() + + if self._fast_polling_count < MAX_FAST_POLLING_COUNT: + self._fast_polling_count += 1 + _LOGGER.debug("Periodic data request executed, now wait for 2 seconds") + await asyncio.sleep(2) + else: + _LOGGER.debug( + "Periodic data request executed, now wait for %s seconds", + self._refresh_interval, + ) + await asyncio.sleep(self._refresh_interval) + + _LOGGER.debug("Periodic data request rescheduled") + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + async def setup(self, loop=None): + """Initialize KebaHandler object.""" + await super().setup(loop) + + # 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')}" + return True + + return False + + def hass_callback(self, data): + """Handle component notification via callback.""" + + # Inform entities about updated values + for listener in self._update_listeners: + listener() + + _LOGGER.debug("Notifying %d listeners", len(self._update_listeners)) + + def _set_fast_polling(self): + _LOGGER.debug("Fast polling enabled") + self._fast_polling_count = 0 + self._polling_task.cancel() + self._polling_task = self._hass.loop.create_task(self._periodic_request()) + + def add_update_listener(self, listener): + """Add a listener for update notifications.""" + self._update_listeners.append(listener) + + # initial data is already loaded, thus update the component + listener() + + async def async_set_energy(self, param): + """Set energy target in async way.""" + try: + energy = param["energy"] + await self.set_energy(energy) + self._set_fast_polling() + except (KeyError, ValueError) as ex: + _LOGGER.warning("Energy value is not correct. %s", ex) + + async def async_set_current(self, param): + """Set current maximum in async way.""" + try: + current = param["current"] + await self.set_current(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) + + async def async_start(self, param=None): + """Authorize EV in async way.""" + await self.start(self.rfid) + self._set_fast_polling() + + async def async_stop(self, param=None): + """De-authorize EV in async way.""" + await self.stop(self.rfid) + self._set_fast_polling() + + async def async_enable_ev(self, param=None): + """Enable EV in async way.""" + await self.enable(True) + self._set_fast_polling() + + async def async_disable_ev(self, param=None): + """Disable EV in async way.""" + await self.enable(False) + self._set_fast_polling() + + async def async_set_failsafe(self, param=None): + """Set failsafe mode in async way.""" + try: + timout = param[CONF_FS_TIMEOUT] + fallback = param[CONF_FS_FALLBACK] + persist = param[CONF_FS_PERSIST] + await self.set_failsafe(timout, fallback, persist) + self._set_fast_polling() + except (KeyError, ValueError) as ex: + _LOGGER.warning( + "failsafe_timeout, failsafe_fallback and/or " + "failsafe_persist value are not correct. %s", + ex, + ) diff --git a/homeassistant/components/keba/binary_sensor.py b/homeassistant/components/keba/binary_sensor.py new file mode 100644 index 00000000000..8c0503a2020 --- /dev/null +++ b/homeassistant/components/keba/binary_sensor.py @@ -0,0 +1,108 @@ +"""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_POWER, + DEVICE_CLASS_SAFETY, +) + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + 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), + ] + async_add_entities(sensors) + + +class KebaBinarySensor(BinarySensorDevice): + """Representation of a binary sensor of a KEBA charging station.""" + + def __init__(self, keba, key, sensor_name, device_class): + """Initialize the KEBA Sensor.""" + self._key = key + self._keba = keba + self._name = sensor_name + self._device_class = device_class + self._is_on = None + self._attributes = {} + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._is_on + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return self._attributes + + async def async_update(self): + """Get latest cached states from the device.""" + if self._key == "Online": + self._is_on = self._keba.get_value(self._key) + + elif self._key == "Plug": + self._is_on = self._keba.get_value("Plug_plugged") + self._attributes["plugged_on_wallbox"] = self._keba.get_value( + "Plug_wallbox" + ) + self._attributes["plug_locked"] = self._keba.get_value("Plug_locked") + self._attributes["plugged_on_EV"] = self._keba.get_value("Plug_EV") + + elif self._key == "State": + self._is_on = self._keba.get_value("State_on") + self._attributes["status"] = self._keba.get_value("State_details") + self._attributes["max_charging_rate"] = str( + self._keba.get_value("Max curr") + ) + + elif self._key == "Tmo FS": + self._is_on = not self._keba.get_value("FS_on") + self._attributes["failsafe_timeout"] = str(self._keba.get_value("Tmo FS")) + self._attributes["fallback_current"] = str(self._keba.get_value("Curr FS")) + elif self._key == "Authreq": + self._is_on = self._keba.get_value(self._key) == 0 + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/lock.py b/homeassistant/components/keba/lock.py new file mode 100644 index 00000000000..3a65e44cd6f --- /dev/null +++ b/homeassistant/components/keba/lock.py @@ -0,0 +1,69 @@ +"""Support for KEBA charging station switch.""" +import logging + +from homeassistant.components.lock import LockDevice + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + keba = hass.data[DOMAIN] + + sensors = [KebaLock(keba, "Authentication")] + async_add_entities(sensors) + + +class KebaLock(LockDevice): + """The entity class for KEBA charging stations switch.""" + + def __init__(self, keba, name): + """Initialize the KEBA switch.""" + self._keba = keba + self._name = name + self._state = True + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def is_locked(self): + """Return true if lock is locked.""" + return self._state + + async def async_lock(self, **kwargs): + """Lock wallbox.""" + await self._keba.async_stop() + + async def async_unlock(self, **kwargs): + """Unlock wallbox.""" + await self._keba.async_start() + + async def async_update(self): + """Attempt to retrieve on off state from the switch.""" + self._state = self._keba.get_value("Authreq") == 1 + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/manifest.json b/homeassistant/components/keba/manifest.json new file mode 100644 index 00000000000..9e959f35c9f --- /dev/null +++ b/homeassistant/components/keba/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "keba", + "name": "Keba Charging Station", + "documentation": "https://www.home-assistant.io/components/keba", + "requirements": ["keba-kecontact==0.2.0"], + "dependencies": [], + "codeowners": [ + "@dannerph" + ] +} diff --git a/homeassistant/components/keba/sensor.py b/homeassistant/components/keba/sensor.py new file mode 100644 index 00000000000..f46b2f0cf90 --- /dev/null +++ b/homeassistant/components/keba/sensor.py @@ -0,0 +1,109 @@ +"""Support for KEBA charging station sensors.""" +import logging + +from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.helpers.entity import Entity +from homeassistant.const import DEVICE_CLASS_POWER + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the KEBA charging station platform.""" + if discovery_info is None: + return + + keba = hass.data[DOMAIN] + + sensors = [ + KebaSensor(keba, "Curr user", "Max current", "mdi:flash", "A"), + KebaSensor( + keba, "Setenergy", "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 + ), + KebaSensor(keba, "E total", "Total Energy", "mdi:gauge", ENERGY_KILO_WATT_HOUR), + ] + async_add_entities(sensors) + + +class KebaSensor(Entity): + """The entity class for KEBA charging stations sensors.""" + + def __init__(self, keba, key, name, icon, unit, device_class=None): + """Initialize the KEBA Sensor.""" + self._key = key + self._keba = keba + self._name = name + self._device_class = device_class + self._icon = icon + self._unit = unit + self._state = None + self._attributes = {} + + @property + def should_poll(self): + """Deactivate polling. Data updated by KebaHandler.""" + return False + + @property + def unique_id(self): + """Return the unique ID of the binary sensor.""" + return f"{self._keba.device_name}_{self._name}" + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._device_class + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Get the unit of measurement.""" + return self._unit + + @property + def device_state_attributes(self): + """Return the state attributes of the binary sensor.""" + return self._attributes + + async def async_update(self): + """Get latest cached states from the device.""" + self._state = self._keba.get_value(self._key) + + if self._key == "P": + self._attributes["power_factor"] = self._keba.get_value("PF") + self._attributes["voltage_u1"] = str(self._keba.get_value("U1")) + self._attributes["voltage_u2"] = str(self._keba.get_value("U2")) + self._attributes["voltage_u3"] = str(self._keba.get_value("U3")) + self._attributes["current_i1"] = str(self._keba.get_value("I1")) + self._attributes["current_i2"] = str(self._keba.get_value("I2")) + self._attributes["current_i3"] = str(self._keba.get_value("I3")) + elif self._key == "Curr user": + self._attributes["max_current_hardware"] = self._keba.get_value("Curr HW") + + def update_callback(self): + """Schedule a state update.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Add update callback after being added to hass.""" + self._keba.add_update_listener(self.update_callback) diff --git a/homeassistant/components/keba/services.yaml b/homeassistant/components/keba/services.yaml new file mode 100644 index 00000000000..3422d6cf034 --- /dev/null +++ b/homeassistant/components/keba/services.yaml @@ -0,0 +1,56 @@ +# Describes the format for available services for KEBA charging staitons + +request_data: + description: > + Request new data from the charging station. + +authorize: + description: > + Authorizes a charging process with the predefined RFID tag of the configuration file. + +deauthorize: + description: > + Deauthorizes the running charging process with the predefined RFID tag of the configuration file. + +set_energy: + description: Sets the energy target after which the charging process stops. + fields: + energy: + description: > + The energy target to stop charging in kWh. Setting 0 disables the limit. + example: 10.0 + +set_current: + description: Sets the maximum current for charging processes. + fields: + current: + description: > + The maximum current used for the charging process in A. Allowed are values between + 6 A and 63 A. Invalid values are discardedand the default is set to 6 A. + The value is also depending on the DIP-switchsettings and the used cable of the + charging station + example: 16 +enable: + description: > + Starts a charging process if charging station is authorized. + +disable: + description: > + Stops the charging process if charging station is authorized. + +set_failsafe: + description: > + Set the failsafe mode of the charging station. If all parameters are 0, the failsafe mode will be disabled. + fields: + failsafe_timeout: + description: > + Timeout in seconds after which the failsafe mode is triggered, if set_current was not executed during this time. + example: 30 + failsafe_fallback: + description: > + Fallback current in A to be set after timeout. + example: 6 + failsafe_persist: + description: > + If failsafe_persist is 0, the failsafe option is only until charging station reboot. If failsafe_persist is 1, the failsafe option will survive a reboot. + example: 0 diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 79b11c129af..5b751bac17c 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -1,5 +1,9 @@ -group_write: - description: Turn a light on. +send: + description: "Send arbitrary data directly to the KNX bus." fields: - address: {description: Group address(es) to write to., example: 1/1/0} - data: {description: KNX data to send., example: 1} + address: + description: "Group address(es) to write to." + example: "1/1/0" + payload: + description: "Payload to send to the bus. Integers are treated as DPT 1/2/3 payloads. For DPTs > 6 bits send a list. Each value represents 1 octet (0-255). Pad with 0 to DPT byte length." + example: "[0, 4]" diff --git a/homeassistant/components/kodi/__init__.py b/homeassistant/components/kodi/__init__.py index cbe20384103..5bbffc5df1d 100644 --- a/homeassistant/components/kodi/__init__.py +++ b/homeassistant/components/kodi/__init__.py @@ -1 +1,91 @@ """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 + + +SERVICE_ADD_MEDIA = "add_to_playlist" +SERVICE_CALL_METHOD = "call_method" + +ATTR_MEDIA_TYPE = "media_type" +ATTR_MEDIA_NAME = "media_name" +ATTR_MEDIA_ARTIST_NAME = "artist_name" +ATTR_MEDIA_ID = "media_id" +ATTR_METHOD = "method" + +MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) + +KODI_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( + { + vol.Required(ATTR_MEDIA_TYPE): cv.string, + vol.Optional(ATTR_MEDIA_ID): cv.string, + vol.Optional(ATTR_MEDIA_NAME): cv.string, + vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string, + } +) +KODI_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( + {vol.Required(ATTR_METHOD): cv.string}, extra=vol.ALLOW_EXTRA +) + +SERVICE_TO_METHOD = { + SERVICE_ADD_MEDIA: { + "method": "async_add_media_to_playlist", + "schema": KODI_ADD_MEDIA_SCHEMA, + }, + SERVICE_CALL_METHOD: { + "method": "async_call_method", + "schema": KODI_CALL_METHOD_SCHEMA, + }, +} + + +async def async_setup(hass, config): + """Set up the Kodi integration.""" + if any( + ((CONF_PLATFORM, DOMAIN) in cfg.items() for cfg in config.get(MP_DOMAIN, [])) + ): + # Register the Kodi media_player services + async def async_service_handler(service): + """Map services to methods on MediaPlayerDevice.""" + method = SERVICE_TO_METHOD.get(service.service) + if not method: + return + + params = { + key: value for key, value in service.data.items() if key != "entity_id" + } + entity_ids = service.data.get("entity_id") + if entity_ids: + target_players = [ + player + for player in hass.data[DOMAIN].values() + if player.entity_id in entity_ids + ] + else: + target_players = hass.data[DOMAIN].values() + + update_tasks = [] + for player in target_players: + await getattr(player, method["method"])(**params) + + for player in target_players: + if player.should_poll: + update_coro = player.async_update_ha_state(True) + update_tasks.append(update_coro) + + if update_tasks: + await asyncio.wait(update_tasks) + + for service in SERVICE_TO_METHOD: + schema = SERVICE_TO_METHOD[service]["schema"] + hass.services.async_register( + DOMAIN, service, async_service_handler, schema=schema + ) + + # Return boolean to indicate that initialization was successful. + return True diff --git a/homeassistant/components/kodi/const.py b/homeassistant/components/kodi/const.py new file mode 100644 index 00000000000..7cb93f0d283 --- /dev/null +++ b/homeassistant/components/kodi/const.py @@ -0,0 +1,2 @@ +"""Constants for the Kodi platform.""" +DOMAIN = "kodi" diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index de50a2ef4de..14ef0292ecc 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -1,5 +1,4 @@ """Support for interfacing with the XBMC/Kodi JSON-RPC API.""" -import asyncio from collections import OrderedDict from functools import wraps import logging @@ -10,9 +9,10 @@ import urllib import aiohttp 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.const import ( - DOMAIN, MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -34,7 +34,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PASSWORD, @@ -134,42 +133,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) -SERVICE_ADD_MEDIA = "kodi_add_to_playlist" -SERVICE_CALL_METHOD = "kodi_call_method" - -DATA_KODI = "kodi" - -ATTR_MEDIA_TYPE = "media_type" -ATTR_MEDIA_NAME = "media_name" -ATTR_MEDIA_ARTIST_NAME = "artist_name" -ATTR_MEDIA_ID = "media_id" -ATTR_METHOD = "method" - -MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) - -MEDIA_PLAYER_ADD_MEDIA_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( - { - vol.Required(ATTR_MEDIA_TYPE): cv.string, - vol.Optional(ATTR_MEDIA_ID): cv.string, - vol.Optional(ATTR_MEDIA_NAME): cv.string, - vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string, - } -) -MEDIA_PLAYER_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( - {vol.Required(ATTR_METHOD): cv.string}, extra=vol.ALLOW_EXTRA -) - -SERVICE_TO_METHOD = { - SERVICE_ADD_MEDIA: { - "method": "async_add_media_to_playlist", - "schema": MEDIA_PLAYER_ADD_MEDIA_SCHEMA, - }, - SERVICE_CALL_METHOD: { - "method": "async_call_method", - "schema": MEDIA_PLAYER_CALL_METHOD_SCHEMA, - }, -} - def _check_deprecated_turn_off(hass, turn_off_action): """Create an equivalent script for old turn off actions.""" @@ -205,8 +168,8 @@ def _check_deprecated_turn_off(hass, turn_off_action): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Kodi platform.""" - if DATA_KODI not in hass.data: - hass.data[DATA_KODI] = dict() + if DOMAIN not in hass.data: + hass.data[DOMAIN] = dict() unique_id = None # Is this a manual configuration? @@ -231,14 +194,14 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Only add a device once, so discovered devices do not override manual # config. ip_addr = socket.gethostbyname(host) - if ip_addr in hass.data[DATA_KODI]: + if ip_addr in hass.data[DOMAIN]: return # If we got an unique id, check that it does not exist already. # This is necessary as netdisco does not deterministally return the same # advertisement when the service is offered over multiple IP addresses. if unique_id is not None: - for device in hass.data[DATA_KODI].values(): + for device in hass.data[DOMAIN].values(): if device.unique_id == unique_id: return @@ -258,49 +221,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= unique_id=unique_id, ) - hass.data[DATA_KODI][ip_addr] = entity + hass.data[DOMAIN][ip_addr] = entity async_add_entities([entity], update_before_add=True) - async def async_service_handler(service): - """Map services to methods on MediaPlayerDevice.""" - method = SERVICE_TO_METHOD.get(service.service) - if not method: - return - - params = { - key: value for key, value in service.data.items() if key != "entity_id" - } - entity_ids = service.data.get("entity_id") - if entity_ids: - target_players = [ - player - for player in hass.data[DATA_KODI].values() - if player.entity_id in entity_ids - ] - else: - target_players = hass.data[DATA_KODI].values() - - update_tasks = [] - for player in target_players: - await getattr(player, method["method"])(**params) - - for player in target_players: - if player.should_poll: - update_coro = player.async_update_ha_state(True) - update_tasks.append(update_coro) - - if update_tasks: - await asyncio.wait(update_tasks) - - if hass.services.has_service(DOMAIN, SERVICE_ADD_MEDIA): - return - - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] - hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema - ) - def cmd(func): """Catch command exceptions.""" diff --git a/homeassistant/components/kodi/services.yaml b/homeassistant/components/kodi/services.yaml index e69de29bb2d..01dde6a249c 100644 --- a/homeassistant/components/kodi/services.yaml +++ b/homeassistant/components/kodi/services.yaml @@ -0,0 +1,30 @@ +# Describes the format for available Kodi services + +add_to_playlist: + description: Add music to the default playlist (i.e. playlistid=0). + fields: + entity_id: + description: Name(s) of the Kodi entities where to add the media. + example: 'media_player.living_room_kodi' + media_type: + description: Media type identifier. It must be one of SONG or ALBUM. + example: ALBUM + media_id: + description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library. + example: 123456 + media_name: + description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist. + example: 'Highway to Hell' + artist_name: + description: Optional artist name for filtering media. + example: 'AC/DC' + +call_method: + description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.' + fields: + entity_id: + description: Name(s) of the Kodi entities where to run the API method. + example: 'media_player.living_room_kodi' + method: + description: Name of the Kodi JSONRPC API method to be called. + example: 'VideoLibrary.GetRecentlyAddedEpisodes' diff --git a/homeassistant/components/lacrosse/manifest.json b/homeassistant/components/lacrosse/manifest.json index 4716b3cb548..99dd4889213 100644 --- a/homeassistant/components/lacrosse/manifest.json +++ b/homeassistant/components/lacrosse/manifest.json @@ -3,7 +3,7 @@ "name": "Lacrosse", "documentation": "https://www.home-assistant.io/components/lacrosse", "requirements": [ - "pylacrosse==0.3.1" + "pylacrosse==0.4.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/launch_library/sensor.py b/homeassistant/components/launch_library/sensor.py index 71246872663..14a75704312 100644 --- a/homeassistant/components/launch_library/sensor.py +++ b/homeassistant/components/launch_library/sensor.py @@ -49,7 +49,7 @@ class LaunchLibrarySensor(Entity): """Get the latest data.""" await self.launches.get_launches() if self.launches.launches is None: - _LOGGER.error("No data recieved") + _LOGGER.error("No data received") return try: data = self.launches.launches[0] diff --git a/homeassistant/components/life360/.translations/es-419.json b/homeassistant/components/life360/.translations/es-419.json new file mode 100644 index 00000000000..3f9bfab3304 --- /dev/null +++ b/homeassistant/components/life360/.translations/es-419.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Life360" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/fr.json b/homeassistant/components/life360/.translations/fr.json index 8bb298a0a84..cb4682fc937 100644 --- a/homeassistant/components/life360/.translations/fr.json +++ b/homeassistant/components/life360/.translations/fr.json @@ -4,6 +4,9 @@ "invalid_credentials": "Informations d'identification invalides", "user_already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" }, + "create_entry": { + "default": "Pour d\u00e9finir les options avanc\u00e9es, voir [Documentation de Life360]( {docs_url} )." + }, "error": { "invalid_credentials": "Informations d'identification invalides", "invalid_username": "Nom d'utilisateur invalide", @@ -14,8 +17,11 @@ "data": { "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "description": "Pour d\u00e9finir des options avanc\u00e9es, voir [Documentation Life360]({docs_url}).\nVous pouvez le faire avant d'ajouter des comptes.", + "title": "Informations sur le compte Life360" } - } + }, + "title": "Life360" } } \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/hr.json b/homeassistant/components/life360/.translations/hr.json new file mode 100644 index 00000000000..5cf8cbef17f --- /dev/null +++ b/homeassistant/components/life360/.translations/hr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "invalid_credentials": "Neva\u017ee\u0107e vjerodajnice", + "user_already_configured": "Ra\u010dun je ve\u0107 konfiguriran" + }, + "create_entry": { + "default": "Da biste postavili napredne opcije, pogledajte [Life360 dokumentacija] ( {docs_url} )." + }, + "error": { + "invalid_credentials": "Neva\u017ee\u0107e vjerodajnice", + "invalid_username": "Neispravno korisni\u010dko ime", + "user_already_configured": "Ra\u010dun je ve\u0107 konfiguriran" + }, + "step": { + "user": { + "data": { + "password": "Lozinka", + "username": "Korisni\u010dko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/id.json b/homeassistant/components/life360/.translations/id.json new file mode 100644 index 00000000000..2bb7a1cca68 --- /dev/null +++ b/homeassistant/components/life360/.translations/id.json @@ -0,0 +1,7 @@ +{ + "config": { + "error": { + "invalid_username": "Nama pengguna tidak valid" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/pl.json b/homeassistant/components/life360/.translations/pl.json index b1523da188c..15aabaa6308 100644 --- a/homeassistant/components/life360/.translations/pl.json +++ b/homeassistant/components/life360/.translations/pl.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "invalid_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce", - "user_already_configured": "Konto jest ju\u017c skonfigurowane." + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", + "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." }, "create_entry": { "default": "Aby skonfigurowa\u0107 zaawansowane ustawienia, zapoznaj si\u0119 z [dokumentacj\u0105 Life360]({docs_url})." }, "error": { - "invalid_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", "invalid_username": "Nieprawid\u0142owa nazwa u\u017cytkownika", - "user_already_configured": "Konto jest ju\u017c skonfigurowane." + "user_already_configured": "Konto zosta\u0142o ju\u017c skonfigurowane." }, "step": { "user": { diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index 9bd33981f37..be84d276422 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -2,7 +2,7 @@ from collections import OrderedDict import logging -from life360 import LoginError +from life360 import Life360Error, LoginError import voluptuous as vol from homeassistant import config_entries @@ -54,6 +54,11 @@ class Life360ConfigFlow(config_entries.ConfigFlow): errors[CONF_USERNAME] = "invalid_username" except LoginError: errors["base"] = "invalid_credentials" + except Life360Error as error: + _LOGGER.error( + "Unexpected error communicating with Life360 server: %s", error + ) + errors["base"] = "unexpected" else: if self._username in self.configured_usernames: errors["base"] = "user_already_configured" @@ -88,6 +93,11 @@ class Life360ConfigFlow(config_entries.ConfigFlow): except LoginError: _LOGGER.error("Invalid credentials for %s", username) return self.async_abort(reason="invalid_credentials") + except Life360Error as error: + _LOGGER.error( + "Unexpected error communicating with Life360 server: %s", error + ) + return self.async_abort(reason="unexpected") return self.async_create_entry( title="{} (from configuration)".format(username), data={ diff --git a/homeassistant/components/life360/manifest.json b/homeassistant/components/life360/manifest.json index 079344af6a6..9eae371070a 100644 --- a/homeassistant/components/life360/manifest.json +++ b/homeassistant/components/life360/manifest.json @@ -8,6 +8,6 @@ "@pnbruckner" ], "requirements": [ - "life360==4.0.1" + "life360==4.1.1" ] } diff --git a/homeassistant/components/life360/strings.json b/homeassistant/components/life360/strings.json index cff3f39e5d5..419f3650333 100644 --- a/homeassistant/components/life360/strings.json +++ b/homeassistant/components/life360/strings.json @@ -14,7 +14,8 @@ "error": { "invalid_username": "Invalid username", "invalid_credentials": "Invalid credentials", - "user_already_configured": "Account has already been configured" + "user_already_configured": "Account has already been configured", + "unexpected": "Unexpected error communicating with Life360 server" }, "create_entry": { "default": "To set advanced options, see [Life360 documentation]({docs_url})." diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 7ac46d1237d..c70a209a35a 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -4,6 +4,7 @@ import csv from datetime import timedelta import logging import os +from typing import Dict, Optional, Tuple import voluptuous as vol @@ -29,6 +30,9 @@ from homeassistant.helpers import intent 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) @@ -344,7 +348,7 @@ async def async_unload_entry(hass, entry): class Profiles: """Representation of available color profiles.""" - _all = None + _all: Optional[Dict[str, Tuple[float, float, int]]] = None @classmethod async def load_profiles(cls, hass): diff --git a/homeassistant/components/light/device_automation.py b/homeassistant/components/light/device_automation.py index 26ddef16aae..ed75b5f906f 100644 --- a/homeassistant/components/light/device_automation.py +++ b/homeassistant/components/light/device_automation.py @@ -14,6 +14,9 @@ import homeassistant.helpers.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 + CONF_TURN_OFF = "turn_off" CONF_TURN_ON = "turn_on" diff --git a/homeassistant/components/linksys_ap/device_tracker.py b/homeassistant/components/linksys_ap/device_tracker.py index df24a409b98..d40de718f90 100644 --- a/homeassistant/components/linksys_ap/device_tracker.py +++ b/homeassistant/components/linksys_ap/device_tracker.py @@ -30,6 +30,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(hass, config): """Validate the configuration and return a Linksys AP scanner.""" + _LOGGER.warning( + "The linksys_ap integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + try: return LinksysAPDeviceScanner(config[DOMAIN]) except ConnectionError: diff --git a/homeassistant/components/linksys_ap/manifest.json b/homeassistant/components/linksys_ap/manifest.json index ccad7298d6b..31fafe17edd 100644 --- a/homeassistant/components/linksys_ap/manifest.json +++ b/homeassistant/components/linksys_ap/manifest.json @@ -3,7 +3,7 @@ "name": "Linksys ap", "documentation": "https://www.home-assistant.io/components/linksys_ap", "requirements": [ - "beautifulsoup4==4.7.1" + "beautifulsoup4==4.8.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/linky/sensor.py b/homeassistant/components/linky/sensor.py index aa5e6516e2e..98aca67d8ea 100644 --- a/homeassistant/components/linky/sensor.py +++ b/homeassistant/components/linky/sensor.py @@ -152,7 +152,7 @@ class LinkySensor(Entity): } def update(self): - """Retreive the new data for the sensor.""" + """Retrieve the new data for the sensor.""" data = self.__account.data[self._scale][self.__when] self.__consumption = data[CONSUMPTION] self.__time = data[TIME] diff --git a/homeassistant/components/lock/__init__.py b/homeassistant/components/lock/__init__.py index a2205ccbf46..503bd3a8c78 100644 --- a/homeassistant/components/lock/__init__.py +++ b/homeassistant/components/lock/__init__.py @@ -25,6 +25,9 @@ from homeassistant.const import ( ) from homeassistant.components import group + +# mypy: allow-untyped-defs, no-check-untyped-defs + ATTR_CHANGED_BY = "changed_by" DOMAIN = "lock" diff --git a/homeassistant/components/logi_circle/.translations/es-419.json b/homeassistant/components/logi_circle/.translations/es-419.json new file mode 100644 index 00000000000..2393908e281 --- /dev/null +++ b/homeassistant/components/logi_circle/.translations/es-419.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una sola cuenta de Logi Circle.", + "external_error": "Se produjo una excepci\u00f3n de otro flujo.", + "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Logi Circle." + }, + "error": { + "auth_error": "Autorizaci\u00f3n de API fallida." + }, + "step": { + "auth": { + "title": "Autenticar con Logi Circle" + }, + "user": { + "data": { + "flow_impl": "Proveedor" + }, + "title": "Proveedor de autenticaci\u00f3n" + } + }, + "title": "Logi Circle" + } +} \ No newline at end of file diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 27b6a2da59f..153f6b5aea6 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,7 +3,8 @@ "name": "Luci", "documentation": "https://www.home-assistant.io/components/luci", "requirements": [ - "openwrt-luci-rpc==1.1.0" + "openwrt-luci-rpc==1.1.0", + "packaging==19.1" ], "dependencies": [], "codeowners": ["@fbradyirl"] diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index a29c7faa06a..26d6c21f3a9 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/luftdaten", "requirements": [ - "luftdaten==0.6.2" + "luftdaten==0.6.3" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 1ef9a9374f7..9f4d81df044 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -40,7 +40,13 @@ def setup(hass, base_config): hass.data[LUTRON_BUTTONS] = [] hass.data[LUTRON_CONTROLLER] = None - hass.data[LUTRON_DEVICES] = {"light": [], "cover": [], "switch": [], "scene": []} + hass.data[LUTRON_DEVICES] = { + "light": [], + "cover": [], + "switch": [], + "scene": [], + "binary_sensor": [], + } config = base_config.get(DOMAIN) hass.data[LUTRON_CONTROLLER] = Lutron( @@ -76,9 +82,13 @@ def setup(hass, base_config): ) hass.data[LUTRON_BUTTONS].append(LutronButton(hass, keypad, button)) + if area.occupancy_group is not None: + hass.data[LUTRON_DEVICES]["binary_sensor"].append( + (area.name, area.occupancy_group) + ) - for component in ("light", "cover", "switch", "scene"): - discovery.load_platform(hass, component, DOMAIN, None, base_config) + for component in ("light", "cover", "switch", "scene", "binary_sensor"): + discovery.load_platform(hass, component, DOMAIN, {}, base_config) return True diff --git a/homeassistant/components/lutron/binary_sensor.py b/homeassistant/components/lutron/binary_sensor.py new file mode 100644 index 00000000000..a86d56c325f --- /dev/null +++ b/homeassistant/components/lutron/binary_sensor.py @@ -0,0 +1,56 @@ +"""Support for Lutron Powr Savr occupancy sensors.""" +from pylutron import OccupancyGroup + +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, + DEVICE_CLASS_OCCUPANCY, +) + +from . import LUTRON_CONTROLLER, LUTRON_DEVICES, LutronDevice + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Lutron occupancy sensors.""" + if discovery_info is None: + return + devs = [] + for (area_name, device) in hass.data[LUTRON_DEVICES]["binary_sensor"]: + dev = LutronOccupancySensor(area_name, device, hass.data[LUTRON_CONTROLLER]) + devs.append(dev) + + add_entities(devs) + + +class LutronOccupancySensor(LutronDevice, BinarySensorDevice): + """Representation of a Lutron Occupancy Group. + + The Lutron integration API reports "occupancy groups" rather than + individual sensors. If two sensors are in the same room, they're + reported as a single occupancy group. + """ + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + # Error cases will end up treated as unoccupied. + return self._lutron_device.state == OccupancyGroup.State.OCCUPIED + + @property + def device_class(self): + """Return that this is an occupancy sensor.""" + return DEVICE_CLASS_OCCUPANCY + + @property + def name(self): + """Return the name of the device.""" + # The default LutronDevice naming would create 'Kitchen Occ Kitchen', + # but since there can only be one OccupancyGroup per area we go + # with something shorter. + return f"{self._area_name} Occupancy" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attr = {} + attr["lutron_integration_id"] = self._lutron_device.id + return attr diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 138d4882446..cae2fc5cfdd 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -34,7 +34,6 @@ class LutronLight(LutronDevice, Light): def __init__(self, area_name, lutron_device, controller): """Initialize the light.""" self._prev_brightness = None - self._is_on = False super().__init__(area_name, lutron_device, controller) @property @@ -78,6 +77,5 @@ class LutronLight(LutronDevice, Light): def update(self): """Call when forcing a refresh of the device.""" - self._is_on = self._lutron_device.level > 0 if self._prev_brightness is None: self._prev_brightness = to_hass_level(self._lutron_device.level) diff --git a/homeassistant/components/lutron/manifest.json b/homeassistant/components/lutron/manifest.json index ab7ca3d919f..bece55ae09d 100644 --- a/homeassistant/components/lutron/manifest.json +++ b/homeassistant/components/lutron/manifest.json @@ -3,7 +3,7 @@ "name": "Lutron", "documentation": "https://www.home-assistant.io/components/lutron", "requirements": [ - "pylutron==0.2.1" + "pylutron==0.2.2" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/lutron/switch.py b/homeassistant/components/lutron/switch.py index 7aa5334cbf2..604f19fc2ae 100644 --- a/homeassistant/components/lutron/switch.py +++ b/homeassistant/components/lutron/switch.py @@ -23,7 +23,7 @@ class LutronSwitch(LutronDevice, SwitchDevice): def __init__(self, area_name, lutron_device, controller): """Initialize the switch.""" - self._is_on = False + self._prev_state = None super().__init__(area_name, lutron_device, controller) def turn_on(self, **kwargs): @@ -48,4 +48,5 @@ class LutronSwitch(LutronDevice, SwitchDevice): def update(self): """Call when forcing a refresh of the device.""" - self._is_on = self._lutron_device.level > 0 + if self._prev_state is None: + self._prev_state = self._lutron_device.level > 0 diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index d4b7a59e79c..3b9ccae1681 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -55,9 +55,8 @@ class LW12WiFi(Light): def __init__(self, name, lw12_light): """Initialise LW-12 WiFi LED Controller. - Args: - name: Friendly name for this platform to use. - lw12_light: Instance of the LW12 controller. + :param name: Friendly name for this platform to use. + :param lw12_light: Instance of the LW12 controller. """ self._light = lw12_light self._name = name diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py index 08853a4edce..1252036e1b2 100644 --- a/homeassistant/components/mailbox/__init__.py +++ b/homeassistant/components/mailbox/__init__.py @@ -16,6 +16,9 @@ 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__) DOMAIN = "mailbox" diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index cfb4353b698..4005e51e373 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -3,7 +3,7 @@ "name": "Mastodon", "documentation": "https://www.home-assistant.io/components/mastodon", "requirements": [ - "Mastodon.py==1.4.5" + "Mastodon.py==1.4.6" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 2c7d63eac2c..f93fe5e77a9 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/components/media_extractor", "requirements": [ - "youtube_dl==2019.07.16" + "youtube_dl==2019.08.13" ], "dependencies": [ "media_player" diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 844f4e22089..791dacb7024 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -96,7 +96,9 @@ from .const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from .reproduce_state import async_reproduce_states # noqa + + +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) _RND = SystemRandom() diff --git a/homeassistant/components/media_player/reproduce_state.py b/homeassistant/components/media_player/reproduce_state.py index 8180a6f358b..4eba4657d95 100644 --- a/homeassistant/components/media_player/reproduce_state.py +++ b/homeassistant/components/media_player/reproduce_state.py @@ -19,7 +19,6 @@ from homeassistant.const import ( ) from homeassistant.core import Context, State from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.loader import bind_hass from .const import ( ATTR_MEDIA_VOLUME_LEVEL, @@ -37,6 +36,9 @@ from .const import ( ) +# mypy: allow-incomplete-defs, allow-untyped-defs + + async def _async_reproduce_states( hass: HomeAssistantType, state: State, context: Optional[Context] = None ) -> None: @@ -89,7 +91,6 @@ async def _async_reproduce_states( ) -@bind_hass async def async_reproduce_states( hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None ) -> None: diff --git a/homeassistant/components/media_player/services.yaml b/homeassistant/components/media_player/services.yaml index d7f636d070a..5421085c308 100644 --- a/homeassistant/components/media_player/services.yaml +++ b/homeassistant/components/media_player/services.yaml @@ -232,35 +232,6 @@ soundtouch_remove_zone_slave: description: Name of slaves entities to remove from the existing zone. example: 'media_player.soundtouch_bedroom' -kodi_add_to_playlist: - description: Add music to the default playlist (i.e. playlistid=0). - fields: - entity_id: - description: Name(s) of the Kodi entities where to add the media. - example: 'media_player.living_room_kodi' - media_type: - description: Media type identifier. It must be one of SONG or ALBUM. - example: ALBUM - media_id: - description: Unique Id of the media entry to add (`songid` or albumid`). If not defined, `media_name` and `artist_name` are needed to search the Kodi music library. - example: 123456 - media_name: - description: Optional media name for filtering media. Can be 'ALL' when `media_type` is 'ALBUM' and `artist_name` is specified, to add all songs from one artist. - example: 'Highway to Hell' - artist_name: - description: Optional artist name for filtering media. - example: 'AC/DC' - -kodi_call_method: - description: 'Call a Kodi JSONRPC API method with optional parameters. Results of the Kodi API call will be redirected in a Home Assistant event: `kodi_call_method_result`.' - fields: - entity_id: - description: Name(s) of the Kodi entities where to run the API method. - example: 'media_player.living_room_kodi' - method: - description: Name of the Kodi JSONRPC API method to be called. - example: 'VideoLibrary.GetRecentlyAddedEpisodes' - squeezebox_call_method: description: 'Call a Squeezebox JSON/RPC API method.' fields: diff --git a/homeassistant/components/met/.translations/es-419.json b/homeassistant/components/met/.translations/es-419.json new file mode 100644 index 00000000000..d744de150d2 --- /dev/null +++ b/homeassistant/components/met/.translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "El nombre ya existe" + }, + "step": { + "user": { + "data": { + "elevation": "Elevaci\u00f3n", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre" + }, + "description": "Meteorologisk institutt", + "title": "Ubicaci\u00f3n" + } + }, + "title": "Met.no" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/fr.json b/homeassistant/components/met/.translations/fr.json index d9996c8308c..7100cb5e4a7 100644 --- a/homeassistant/components/met/.translations/fr.json +++ b/homeassistant/components/met/.translations/fr.json @@ -11,8 +11,10 @@ "longitude": "Longitude", "name": "Nom" }, + "description": "Institut m\u00e9t\u00e9orologique", "title": "Emplacement" } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/hr.json b/homeassistant/components/met/.translations/hr.json new file mode 100644 index 00000000000..6505229355c --- /dev/null +++ b/homeassistant/components/met/.translations/hr.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "name_exists": "Ime ve\u0107 postoji" + }, + "step": { + "user": { + "data": { + "elevation": "Elevacija", + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina", + "name": "Ime" + }, + "description": "Meteorolo\u0161ki institutt", + "title": "Lokacija" + } + }, + "title": "Met.no" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/id.json b/homeassistant/components/met/.translations/id.json new file mode 100644 index 00000000000..12854e4ed61 --- /dev/null +++ b/homeassistant/components/met/.translations/id.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "elevation": "Ketinggian", + "name": "Nama" + }, + "title": "Lokasi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index 61b66b794e1..d44142213bf 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -11,7 +11,7 @@ "longitude": "D\u0142ugo\u015b\u0107 geograficzna", "name": "Nazwa" }, - "description": "Meteorologisk institutt", + "description": "Instytut Meteorologiczny", "title": "Lokalizacja" } }, diff --git a/homeassistant/components/met/config_flow.py b/homeassistant/components/met/config_flow.py index 9088d958cf0..795ba57d988 100644 --- a/homeassistant/components/met/config_flow.py +++ b/homeassistant/components/met/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure Met component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -17,8 +17,7 @@ def configured_instances(hass): ) -@config_entries.HANDLERS.register(DOMAIN) -class MetFlowHandler(data_entry_flow.FlowHandler): +class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Met component.""" VERSION = 1 diff --git a/homeassistant/components/meteo_france/__init__.py b/homeassistant/components/meteo_france/__init__.py index ab3ec45867b..d6460fd6e5a 100644 --- a/homeassistant/components/meteo_france/__init__.py +++ b/homeassistant/components/meteo_france/__init__.py @@ -121,7 +121,7 @@ def setup(hass, config): weather_alert_client.update_data() except VigilanceMeteoError as exp: _LOGGER.error( - "Unexpected error when creating the" "vigilance_meteoFrance proxy: %s ", + "Unexpected error when creating the vigilance_meteoFrance proxy: %s ", exp, ) else: @@ -152,7 +152,7 @@ def setup(hass, config): if CONF_MONITORED_CONDITIONS in location: monitored_conditions = location[CONF_MONITORED_CONDITIONS] - _LOGGER.debug("meteo_france sensor platfrom loaded for %s", city) + _LOGGER.debug("meteo_france sensor platform loaded for %s", city) load_platform( hass, "sensor", diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index ce21e0f7d77..9a861d13c2e 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -1,5 +1,5 @@ """Support for Meteo-France weather service.""" -from datetime import datetime, timedelta +from datetime import timedelta import logging from homeassistant.components.weather import ( @@ -9,6 +9,7 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TIME, WeatherEntity, ) +import homeassistant.util.dt as dt_util from homeassistant.const import TEMP_CELSIUS from . import ATTRIBUTION, CONDITION_CLASSES, CONF_CITY, DATA_METEO_FRANCE @@ -83,8 +84,9 @@ class MeteoFranceWeather(WeatherEntity): @property def forecast(self): """Return the forecast.""" - reftime = datetime.now().replace(hour=12, minute=00) + reftime = dt_util.utcnow().replace(hour=12, minute=0, second=0, microsecond=0) reftime += timedelta(hours=24) + _LOGGER.debug("reftime used for %s forecast: %s", self._data["name"], reftime) forecast_data = [] for key in self._data["forecast"]: value = self._data["forecast"][key] diff --git a/homeassistant/components/mikrotik/__init__.py b/homeassistant/components/mikrotik/__init__.py index 0fe5a1c70b1..aacd3c65b3e 100644 --- a/homeassistant/components/mikrotik/__init__.py +++ b/homeassistant/components/mikrotik/__init__.py @@ -1 +1,201 @@ """The mikrotik component.""" +import logging +import ssl + +import voluptuous as vol +import librouteros +from librouteros.login import login_plain, login_token + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_USERNAME, + CONF_PORT, + CONF_SSL, + CONF_METHOD, +) +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, + DOMAIN, + HOSTS, + MTK_LOGIN_PLAIN, + MTK_LOGIN_TOKEN, + DEFAULT_ENCODING, + IDENTITY, + CONF_TRACK_DEVICES, + CONF_ENCODING, + CONF_ARP_PING, + CONF_LOGIN_METHOD, + MIKROTIK_SERVICES, +) + +_LOGGER = logging.getLogger(__name__) + +MTK_DEFAULT_API_PORT = "8728" +MTK_DEFAULT_API_SSL_PORT = "8729" + +MIKROTIK_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_METHOD): cv.string, + vol.Optional(CONF_LOGIN_METHOD): vol.Any(MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN), + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, + vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean, + vol.Optional(CONF_ARP_PING, default=False): cv.boolean, + } + ) +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [MIKROTIK_SCHEMA])}, extra=vol.ALLOW_EXTRA +) + + +def setup(hass, config): + """Set up the Mikrotik component.""" + hass.data[DOMAIN] = {HOSTS: {}} + + for device in config[DOMAIN]: + host = device[CONF_HOST] + use_ssl = device.get(CONF_SSL) + user = device.get(CONF_USERNAME) + password = device.get(CONF_PASSWORD, "") + login = device.get(CONF_LOGIN_METHOD) + encoding = device.get(CONF_ENCODING) + track_devices = device.get(CONF_TRACK_DEVICES) + + if CONF_PORT in device: + port = device.get(CONF_PORT) + else: + if use_ssl: + port = MTK_DEFAULT_API_SSL_PORT + else: + port = MTK_DEFAULT_API_PORT + + if login == MTK_LOGIN_PLAIN: + login_method = (login_plain,) + elif login == MTK_LOGIN_TOKEN: + login_method = (login_token,) + else: + login_method = (login_plain, login_token) + + try: + api = MikrotikClient( + host, use_ssl, port, user, password, login_method, encoding + ) + api.connect_to_device() + hass.data[DOMAIN][HOSTS][host] = {"config": device, "api": api} + except ( + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ConnectionError, + ) as api_error: + _LOGGER.error("Mikrotik %s error %s", host, api_error) + continue + + if track_devices: + hass.data[DOMAIN][HOSTS][host][DEVICE_TRACKER] = True + load_platform(hass, DEVICE_TRACKER, DOMAIN, None, config) + + if not hass.data[DOMAIN][HOSTS]: + return False + return True + + +class MikrotikClient: + """Handle all communication with the Mikrotik API.""" + + def __init__(self, host, use_ssl, port, user, password, login_method, encoding): + """Initialize the Mikrotik Client.""" + self._host = host + self._use_ssl = use_ssl + self._port = port + self._user = user + self._password = password + self._login_method = login_method + self._encoding = encoding + self._ssl_wrapper = None + self.hostname = None + self._client = None + self._connected = False + + def connect_to_device(self): + """Connect to Mikrotik device.""" + self._connected = False + _LOGGER.debug("[%s] Connecting to Mikrotik device", self._host) + + kwargs = { + "encoding": self._encoding, + "login_methods": self._login_method, + "port": self._port, + } + + if self._use_ssl: + if self._ssl_wrapper is None: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + self._ssl_wrapper = ssl_context.wrap_socket + kwargs["ssl_wrapper"] = self._ssl_wrapper + + try: + self._client = librouteros.connect( + self._host, self._user, self._password, **kwargs + ) + self._connected = True + except ( + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + librouteros.exceptions.ConnectionError, + ) as api_error: + _LOGGER.error("Mikrotik %s: %s", self._host, api_error) + self._client = None + return False + + self.hostname = self.get_hostname() + _LOGGER.info("Mikrotik Connected to %s (%s)", self.hostname, self._host) + return self._connected + + def get_hostname(self): + """Return device host name.""" + data = self.command(MIKROTIK_SERVICES[IDENTITY]) + return data[0][NAME] if data else None + + def connected(self): + """Return connected boolean.""" + return self._connected + + def command(self, cmd, params=None): + """Retrieve data from Mikrotik API.""" + if not self._connected or not self._client: + if not self.connect_to_device(): + return None + try: + if params: + response = self._client(cmd=cmd, **params) + else: + response = self._client(cmd=cmd) + except (librouteros.exceptions.ConnectionError,) as api_error: + _LOGGER.error("Mikrotik %s connection error %s", self._host, api_error) + self.connect_to_device() + return None + except ( + librouteros.exceptions.TrapError, + librouteros.exceptions.MultiTrapError, + ) as api_error: + _LOGGER.error( + "Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s", + self._host, + cmd, + api_error, + ) + return None + return response if response else None diff --git a/homeassistant/components/mikrotik/const.py b/homeassistant/components/mikrotik/const.py new file mode 100644 index 00000000000..bd26b02fe1b --- /dev/null +++ b/homeassistant/components/mikrotik/const.py @@ -0,0 +1,50 @@ +"""Constants used in the Mikrotik components.""" + +DOMAIN = "mikrotik" +MIKROTIK = DOMAIN +HOSTS = "hosts" +MTK_LOGIN_PLAIN = "plain" +MTK_LOGIN_TOKEN = "token" + +CONF_ARP_PING = "arp_ping" +CONF_TRACK_DEVICES = "track_devices" +CONF_LOGIN_METHOD = "login_method" +CONF_ENCODING = "encoding" +DEFAULT_ENCODING = "utf-8" + +NAME = "name" +INFO = "info" +IDENTITY = "identity" +ARP = "arp" +DHCP = "dhcp" +WIRELESS = "wireless" +CAPSMAN = "capsman" + +MIKROTIK_SERVICES = { + INFO: "/system/routerboard/getall", + IDENTITY: "/system/identity/getall", + ARP: "/ip/arp/getall", + DHCP: "/ip/dhcp-server/lease/getall", + WIRELESS: "/interface/wireless/registration-table/getall", + CAPSMAN: "/caps-man/registration-table/getall", +} + +ATTR_DEVICE_TRACKER = [ + "comment", + "mac-address", + "ssid", + "interface", + "host-name", + "last-seen", + "rx-signal", + "signal-strength", + "tx-ccq", + "signal-to-noise", + "wmm-enabled", + "authentication-type", + "encryption", + "tx-rate-set", + "rx-rate", + "tx-rate", + "uptime", +] diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 3260ac1ab2c..6c3fb559cba 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -1,251 +1,190 @@ """Support for Mikrotik routers as device tracker.""" import logging -import ssl - -import voluptuous as vol - -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, + DOMAIN as DEVICE_TRACKER, DeviceScanner, ) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - CONF_PORT, - CONF_SSL, - CONF_METHOD, +from homeassistant.util import slugify +from homeassistant.const import CONF_METHOD +from .const import ( + HOSTS, + MIKROTIK, + CONF_ARP_PING, + MIKROTIK_SERVICES, + CAPSMAN, + WIRELESS, + DHCP, + ARP, + ATTR_DEVICE_TRACKER, ) _LOGGER = logging.getLogger(__name__) -MTK_DEFAULT_API_PORT = "8728" -MTK_DEFAULT_API_SSL_PORT = "8729" - -CONF_LOGIN_METHOD = "login_method" -MTK_LOGIN_PLAIN = "plain" -MTK_LOGIN_TOKEN = "token" - -CONF_ENCODING = "encoding" -DEFAULT_ENCODING = "utf-8" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_METHOD): cv.string, - vol.Optional(CONF_LOGIN_METHOD): vol.Any(MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN), - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string, - } -) - def get_scanner(hass, config): - """Validate the configuration and return MTikScanner.""" - scanner = MikrotikScanner(config[DOMAIN]) + """Validate the configuration and return MikrotikScanner.""" + for host in hass.data[MIKROTIK][HOSTS]: + if DEVICE_TRACKER not in hass.data[MIKROTIK][HOSTS][host]: + continue + hass.data[MIKROTIK][HOSTS][host].pop(DEVICE_TRACKER, None) + api = hass.data[MIKROTIK][HOSTS][host]["api"] + config = hass.data[MIKROTIK][HOSTS][host]["config"] + hostname = api.get_hostname() + scanner = MikrotikScanner(api, host, hostname, config) return scanner if scanner.success_init else None class MikrotikScanner(DeviceScanner): - """This class queries a Mikrotik router.""" + """This class queries a Mikrotik device.""" - def __init__(self, config): + def __init__(self, api, host, hostname, config): """Initialize the scanner.""" - self.last_results = {} - - self.host = config[CONF_HOST] - self.ssl = config[CONF_SSL] - try: - self.port = config[CONF_PORT] - except KeyError: - if self.ssl: - self.port = MTK_DEFAULT_API_SSL_PORT - else: - self.port = MTK_DEFAULT_API_PORT - self.username = config[CONF_USERNAME] - self.password = config[CONF_PASSWORD] - self.login_method = config.get(CONF_LOGIN_METHOD) + self.api = api + self.config = config + self.host = host + self.hostname = hostname self.method = config.get(CONF_METHOD) - self.encoding = config[CONF_ENCODING] + self.arp_ping = config.get(CONF_ARP_PING) + self.dhcp = None + self.devices_arp = {} + self.devices_dhcp = {} + self.device_tracker = None + self.success_init = self.api.connected() - self.connected = False - self.success_init = False - self.client = None - self.wireless_exist = None - self.success_init = self.connect_to_device() + def get_extra_attributes(self, device): + """ + Get extra attributes of a device. - if self.success_init: - _LOGGER.info("Start polling Mikrotik (%s) router...", self.host) - self._update_info() - else: - _LOGGER.error("Connection to Mikrotik (%s) failed", self.host) + Some known extra attributes that may be returned in the device tuple + include MAC address (mac), network device (dev), IP address + (ip), reachable status (reachable), associated router + (host), hostname if known (hostname) among others. + """ + return self.device_tracker.get(device) or {} - def connect_to_device(self): - """Connect to Mikrotik method.""" - import librouteros - from librouteros.login import login_plain, login_token - - if self.login_method == MTK_LOGIN_PLAIN: - login_method = (login_plain,) - elif self.login_method == MTK_LOGIN_TOKEN: - login_method = (login_token,) - else: - login_method = (login_plain, login_token) - - try: - kwargs = { - "port": self.port, - "encoding": self.encoding, - "login_methods": login_method, - } - - if self.ssl: - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - kwargs["ssl_wrapper"] = ssl_context.wrap_socket - self.client = librouteros.connect( - self.host, self.username, self.password, **kwargs - ) - - try: - routerboard_info = self.client(cmd="/system/routerboard/getall") - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ): - routerboard_info = None - raise - - if routerboard_info: - _LOGGER.info( - "Connected to Mikrotik %s with IP %s", - routerboard_info[0].get("model", "Router"), - self.host, - ) - - self.connected = True - - try: - self.capsman_exist = self.client(cmd="/caps-man/interface/getall") - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ): - self.capsman_exist = False - - if not self.capsman_exist: - _LOGGER.info( - "Mikrotik %s: Not a CAPSman controller. Trying " - "local interfaces", - self.host, - ) - - try: - self.wireless_exist = self.client(cmd="/interface/wireless/getall") - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ): - self.wireless_exist = False - - if ( - not self.wireless_exist - and not self.capsman_exist - or self.method == "ip" - ): - _LOGGER.info( - "Mikrotik %s: Wireless adapters not found. Try to " - "use DHCP lease table as presence tracker source. " - "Please decrease lease time as much as possible", - self.host, - ) - if self.method: - _LOGGER.info( - "Mikrotik %s: Manually selected polling method %s", - self.host, - self.method, - ) - - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ) as api_error: - _LOGGER.error("Connection error: %s", api_error) - return self.connected + def get_device_name(self, device): + """Get name for a device.""" + host = self.device_tracker.get(device, {}) + return host.get("host_name") def scan_devices(self): """Scan for new devices and return a list with found device MACs.""" - import librouteros + self.update_device_tracker() + return list(self.device_tracker) - try: - self._update_info() - except ( - librouteros.exceptions.TrapError, - librouteros.exceptions.MultiTrapError, - librouteros.exceptions.ConnectionError, - ) as api_error: - _LOGGER.error("Connection error: %s", api_error) - self.connect_to_device() - return [device for device in self.last_results] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - return self.last_results.get(device) - - def _update_info(self): - """Retrieve latest information from the Mikrotik box.""" + def get_method(self): + """Determine the device tracker polling method.""" if self.method: - devices_tracker = self.method + _LOGGER.debug( + "Mikrotik %s: Manually selected polling method %s", + self.host, + self.method, + ) + return self.method + + capsman = self.api.command(MIKROTIK_SERVICES[CAPSMAN]) + if not capsman: + _LOGGER.debug( + "Mikrotik %s: Not a CAPsMAN controller. " + "Trying local wireless interfaces", + (self.host), + ) else: - if self.capsman_exist: - devices_tracker = "capsman" - elif self.wireless_exist: - devices_tracker = "wireless" - else: - devices_tracker = "ip" + return CAPSMAN - _LOGGER.debug( - "Loading %s devices from Mikrotik (%s) ...", devices_tracker, self.host - ) + wireless = self.api.command(MIKROTIK_SERVICES[WIRELESS]) + if not wireless: + _LOGGER.info( + "Mikrotik %s: Wireless adapters not found. Try to " + "use DHCP lease table as presence tracker source. " + "Please decrease lease time as much as possible", + self.host, + ) + return DHCP - device_names = self.client(cmd="/ip/dhcp-server/lease/getall") - if devices_tracker == "capsman": - devices = self.client(cmd="/caps-man/registration-table/getall") - elif devices_tracker == "wireless": - devices = self.client(cmd="/interface/wireless/registration-table/getall") - else: - devices = device_names + return WIRELESS - if device_names is None and devices is None: - return False + def update_device_tracker(self): + """Update device_tracker from Mikrotik API.""" + self.device_tracker = {} + if not self.method: + self.method = self.get_method() - mac_names = { - device.get("mac-address"): device.get("host-name") - for device in device_names - if device.get("mac-address") + data = self.api.command(MIKROTIK_SERVICES[self.method]) + if data is None: + return + + if self.method != DHCP: + dhcp = self.api.command(MIKROTIK_SERVICES[DHCP]) + if dhcp is not None: + self.devices_dhcp = load_mac(dhcp) + + arp = self.api.command(MIKROTIK_SERVICES[ARP]) + self.devices_arp = load_mac(arp) + + for device in data: + mac = device.get("mac-address") + if self.method == DHCP: + if "active-address" not in device: + continue + + if self.arp_ping and self.devices_arp: + if mac not in self.devices_arp: + continue + ip_address = self.devices_arp[mac]["address"] + interface = self.devices_arp[mac]["interface"] + if not self.do_arp_ping(ip_address, interface): + continue + + attrs = {} + if mac in self.devices_dhcp and "host-name" in self.devices_dhcp[mac]: + hostname = self.devices_dhcp[mac].get("host-name") + if hostname: + attrs["host_name"] = hostname + + if self.devices_arp and mac in self.devices_arp: + attrs["ip_address"] = self.devices_arp[mac].get("address") + + for attr in ATTR_DEVICE_TRACKER: + if attr in device and device[attr] is not None: + attrs[slugify(attr)] = device[attr] + attrs["scanner_type"] = self.method + attrs["scanner_host"] = self.host + attrs["scanner_hostname"] = self.hostname + self.device_tracker[mac] = attrs + + def do_arp_ping(self, ip_address, interface): + """Attempt to arp ping MAC address via interface.""" + params = { + "arp-ping": "yes", + "interval": "100ms", + "count": 3, + "interface": interface, + "address": ip_address, } + cmd = "/ping" + data = self.api.command(cmd, params) + if data is not None: + status = 0 + for result in data: + if "status" in result: + _LOGGER.debug( + "Mikrotik %s arp_ping error: %s", self.host, result["status"] + ) + status += 1 + if status == len(data): + return None + return data - if devices_tracker in ("wireless", "capsman"): - self.last_results = { - device.get("mac-address"): mac_names.get(device.get("mac-address")) - for device in devices - } - else: - self.last_results = { - device.get("mac-address"): mac_names.get(device.get("mac-address")) - for device in device_names - if device.get("active-address") - } - return True +def load_mac(devices=None): + """Load dictionary using MAC address as key.""" + if not devices: + return None + mac_devices = {} + for device in devices: + if "mac-address" in device: + mac = device.pop("mac-address") + mac_devices[mac] = device + return mac_devices diff --git a/homeassistant/components/mikrotik/manifest.json b/homeassistant/components/mikrotik/manifest.json index caa9733f241..92869856545 100644 --- a/homeassistant/components/mikrotik/manifest.json +++ b/homeassistant/components/mikrotik/manifest.json @@ -3,7 +3,7 @@ "name": "Mikrotik", "documentation": "https://www.home-assistant.io/components/mikrotik", "requirements": [ - "librouteros==2.2.0" + "librouteros==2.3.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/minio/__init__.py b/homeassistant/components/minio/__init__.py new file mode 100644 index 00000000000..cede3a7aad5 --- /dev/null +++ b/homeassistant/components/minio/__init__.py @@ -0,0 +1,265 @@ +"""Minio component.""" +import logging +import os +import threading +from queue import Queue +from typing import List + +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 + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "minio" +CONF_HOST = "host" +CONF_PORT = "port" +CONF_ACCESS_KEY = "access_key" +CONF_SECRET_KEY = "secret_key" +CONF_SECURE = "secure" +CONF_LISTEN = "listen" +CONF_LISTEN_BUCKET = "bucket" +CONF_LISTEN_PREFIX = "prefix" +CONF_LISTEN_SUFFIX = "suffix" +CONF_LISTEN_EVENTS = "events" + +ATTR_BUCKET = "bucket" +ATTR_KEY = "key" +ATTR_FILE_PATH = "file_path" + +DEFAULT_LISTEN_PREFIX = "" +DEFAULT_LISTEN_SUFFIX = ".*" +DEFAULT_LISTEN_EVENTS = "s3:ObjectCreated:*" + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Required(CONF_PORT): cv.port, + vol.Required(CONF_ACCESS_KEY): cv.string, + vol.Required(CONF_SECRET_KEY): cv.string, + vol.Required(CONF_SECURE): cv.boolean, + vol.Optional(CONF_LISTEN, default=[]): vol.All( + cv.ensure_list, + [ + vol.Schema( + { + vol.Required(CONF_LISTEN_BUCKET): cv.string, + vol.Optional( + CONF_LISTEN_PREFIX, default=DEFAULT_LISTEN_PREFIX + ): cv.string, + vol.Optional( + CONF_LISTEN_SUFFIX, default=DEFAULT_LISTEN_SUFFIX + ): cv.string, + vol.Optional( + CONF_LISTEN_EVENTS, default=DEFAULT_LISTEN_EVENTS + ): cv.string, + } + ) + ], + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +BUCKET_KEY_SCHEMA = vol.Schema( + {vol.Required(ATTR_BUCKET): cv.template, vol.Required(ATTR_KEY): cv.template} +) + +BUCKET_KEY_FILE_SCHEMA = BUCKET_KEY_SCHEMA.extend( + {vol.Required(ATTR_FILE_PATH): cv.template} +) + + +def setup(hass, config): + """Set up MinioClient and event listeners.""" + conf = config[DOMAIN] + + host = conf[CONF_HOST] + port = conf[CONF_PORT] + access_key = conf[CONF_ACCESS_KEY] + secret_key = conf[CONF_SECRET_KEY] + secure = conf[CONF_SECURE] + + queue_listener = QueueListener(hass) + queue = queue_listener.queue + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, queue_listener.start_handler) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, queue_listener.stop_handler) + + def _setup_listener(listener_conf): + bucket = listener_conf[CONF_LISTEN_BUCKET] + prefix = listener_conf[CONF_LISTEN_PREFIX] + suffix = listener_conf[CONF_LISTEN_SUFFIX] + events = listener_conf[CONF_LISTEN_EVENTS] + + minio_listener = MinioListener( + queue, + get_minio_endpoint(host, port), + access_key, + secret_key, + secure, + bucket, + prefix, + suffix, + events, + ) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, minio_listener.start_handler) + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, minio_listener.stop_handler) + + for listen_conf in conf[CONF_LISTEN]: + _setup_listener(listen_conf) + + minio_client = create_minio_client( + get_minio_endpoint(host, port), access_key, secret_key, secure + ) + + def _render_service_value(service, key): + value = service.data[key] + value.hass = hass + return value.async_render() + + def put_file(service): + """Upload file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + file_path = _render_service_value(service, ATTR_FILE_PATH) + + if not hass.config.is_allowed_path(file_path): + _LOGGER.error("Invalid file_path %s", file_path) + return + + minio_client.fput_object(bucket, key, file_path) + + def get_file(service): + """Download file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + file_path = _render_service_value(service, ATTR_FILE_PATH) + + if not hass.config.is_allowed_path(file_path): + _LOGGER.error("Invalid file_path %s", file_path) + return + + minio_client.fget_object(bucket, key, file_path) + + def remove_file(service): + """Delete file service.""" + bucket = _render_service_value(service, ATTR_BUCKET) + key = _render_service_value(service, ATTR_KEY) + + minio_client.remove_object(bucket, key) + + hass.services.register(DOMAIN, "put", put_file, schema=BUCKET_KEY_FILE_SCHEMA) + hass.services.register(DOMAIN, "get", get_file, schema=BUCKET_KEY_FILE_SCHEMA) + hass.services.register(DOMAIN, "remove", remove_file, schema=BUCKET_KEY_SCHEMA) + + return True + + +def get_minio_endpoint(host: str, port: int) -> str: + """Create minio endpoint from host and port.""" + return "{}:{}".format(host, port) + + +class QueueListener(threading.Thread): + """Forward events from queue into HASS event bus.""" + + def __init__(self, hass): + """Create queue.""" + super().__init__() + self._hass = hass + self._queue = Queue() + + def run(self): + """Listen to queue events, and forward them to HASS event bus.""" + _LOGGER.info("Running QueueListener") + while True: + event = self._queue.get() + if event is None: + break + + _, file_name = os.path.split(event[ATTR_KEY]) + + _LOGGER.debug( + "Sending event %s, %s, %s", + event["event_name"], + event[ATTR_BUCKET], + event[ATTR_KEY], + ) + self._hass.bus.fire(DOMAIN, {"file_name": file_name, **event}) + + @property + def queue(self): + """Return wrapped queue.""" + return self._queue + + def stop(self): + """Stop run by putting None into queue and join the thread.""" + _LOGGER.info("Stopping QueueListener") + self._queue.put(None) + self.join() + _LOGGER.info("Stopped QueueListener") + + def start_handler(self, _): + """Start handler helper method.""" + self.start() + + def stop_handler(self, _): + """Stop handler helper method.""" + self.stop() + + +class MinioListener: + """MinioEventThread wrapper with helper methods.""" + + def __init__( + self, + queue: Queue, + endpoint: str, + access_key: str, + secret_key: str, + secure: bool, + bucket_name: str, + prefix: str, + suffix: str, + events: List[str], + ): + """Create Listener.""" + self._queue = queue + self._endpoint = endpoint + self._access_key = access_key + self._secret_key = secret_key + self._secure = secure + self._bucket_name = bucket_name + self._prefix = prefix + self._suffix = suffix + self._events = events + self._minio_event_thread = None + + def start_handler(self, _): + """Create and start the event thread.""" + self._minio_event_thread = MinioEventThread( + self._queue, + self._endpoint, + self._access_key, + self._secret_key, + self._secure, + self._bucket_name, + self._prefix, + self._suffix, + self._events, + ) + self._minio_event_thread.start() + + def stop_handler(self, _): + """Issue stop and wait for thread to join.""" + if self._minio_event_thread is not None: + self._minio_event_thread.stop() diff --git a/homeassistant/components/minio/manifest.json b/homeassistant/components/minio/manifest.json new file mode 100644 index 00000000000..2b2f84836ea --- /dev/null +++ b/homeassistant/components/minio/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "minio", + "name": "Minio", + "documentation": "https://www.home-assistant.io/components/minio", + "requirements": [ + "minio==4.0.9" + ], + "dependencies": [], + "codeowners": [ + "@tkislan" + ] +} diff --git a/homeassistant/components/minio/minio_helper.py b/homeassistant/components/minio/minio_helper.py new file mode 100644 index 00000000000..bd7b15d27d4 --- /dev/null +++ b/homeassistant/components/minio/minio_helper.py @@ -0,0 +1,209 @@ +"""Minio helper methods.""" +import time +from collections.abc import Iterable +import json +import logging +import re +import threading +from queue import Queue +from typing import Iterator, List +from urllib.parse import unquote + +from minio import Minio +from urllib3.exceptions import HTTPError + +_LOGGER = logging.getLogger(__name__) + +_METADATA_RE = re.compile("x-amz-meta-(.*)", re.IGNORECASE) + + +def normalize_metadata(metadata: dict) -> dict: + """Normalize object metadata by stripping the prefix.""" + new_metadata = {} + for meta_key, meta_value in metadata.items(): + match = _METADATA_RE.match(meta_key) + if not match: + continue + + new_metadata[match.group(1).lower()] = meta_value + + return new_metadata + + +def create_minio_client( + endpoint: str, access_key: str, secret_key: str, secure: bool +) -> Minio: + """Create Minio client.""" + return Minio(endpoint, access_key, secret_key, secure) + + +def get_minio_notification_response( + minio_client, bucket_name: str, prefix: str, suffix: str, events: List[str] +): + """Start listening to minio events. Copied from minio-py.""" + query = {"prefix": prefix, "suffix": suffix, "events": events} + # pylint: disable=protected-access + return minio_client._url_open( + "GET", bucket_name=bucket_name, query=query, preload_content=False + ) + + +class MinioEventStreamIterator(Iterable): + """Iterator wrapper over notification http response stream.""" + + def __iter__(self) -> Iterator: + """Return self.""" + return self + + def __init__(self, response): + """Init.""" + self._response = response + self._stream = response.stream() + + def __next__(self): + """Get next not empty line.""" + while True: + line = next(self._stream) + if line.strip(): + event = json.loads(line.decode("utf-8")) + if event["Records"] is not None: + return event + + def close(self): + """Close the response.""" + self._response.close() + + +class MinioEventThread(threading.Thread): + """Thread wrapper around minio notification blocking stream.""" + + def __init__( + self, + queue: Queue, + endpoint: str, + access_key: str, + secret_key: str, + secure: bool, + bucket_name: str, + prefix: str, + suffix: str, + events: List[str], + ): + """Copy over all Minio client options.""" + super().__init__() + self._queue = queue + self._endpoint = endpoint + self._access_key = access_key + self._secret_key = secret_key + self._secure = secure + self._bucket_name = bucket_name + self._prefix = prefix + self._suffix = suffix + self._events = events + self._event_stream_it = None + self._should_stop = False + + def __enter__(self): + """Start the thread.""" + self.start() + + def __exit__(self, exc_type, exc_val, exc_tb): + """Stop and join the thread.""" + self.stop() + + def run(self): + """Create MinioClient and run the loop.""" + _LOGGER.info("Running MinioEventThread") + + self._should_stop = False + + minio_client = create_minio_client( + self._endpoint, self._access_key, self._secret_key, self._secure + ) + + while not self._should_stop: + _LOGGER.info("Connecting to minio event stream") + response = None + try: + response = get_minio_notification_response( + minio_client, + self._bucket_name, + self._prefix, + self._suffix, + self._events, + ) + + self._event_stream_it = MinioEventStreamIterator(response) + + self._iterate_event_stream(self._event_stream_it, minio_client) + except json.JSONDecodeError: + if response: + response.close() + except HTTPError as error: + _LOGGER.error("Failed to connect to Minio endpoint: %s", error) + + # Wait before attempting to connect again. + time.sleep(1) + except AttributeError: + # When response is closed, iterator will fail to access + # the underlying socket descriptor. + break + + def _iterate_event_stream(self, event_stream_it, minio_client): + for event in event_stream_it: + for event_name, bucket, key, metadata in iterate_objects(event): + presigned_url = "" + try: + presigned_url = minio_client.presigned_get_object(bucket, key) + # Fail gracefully. If for whatever reason this stops working, + # it shouldn't prevent it from firing events. + # pylint: disable=broad-except + except Exception as error: + _LOGGER.error("Failed to generate presigned url: %s", error) + + queue_entry = { + "event_name": event_name, + "bucket": bucket, + "key": key, + "presigned_url": presigned_url, + "metadata": metadata, + } + _LOGGER.debug("Queue entry, %s", queue_entry) + self._queue.put(queue_entry) + + def stop(self): + """Cancel event stream and join the thread.""" + _LOGGER.debug("Stopping event thread") + self._should_stop = True + if self._event_stream_it is not None: + self._event_stream_it.close() + self._event_stream_it = None + + _LOGGER.debug("Joining event thread") + self.join() + _LOGGER.debug("Event thread joined") + + +def iterate_objects(event): + """ + Iterate over file records of notification event. + + Most of the time it should still be only one record. + """ + records = event.get("Records", []) + + for record in records: + event_name = record.get("eventName") + bucket = record.get("s3", {}).get("bucket", {}).get("name") + key = record.get("s3", {}).get("object", {}).get("key") + metadata = normalize_metadata( + record.get("s3", {}).get("object", {}).get("userMetadata", {}) + ) + + if not bucket or not key: + _LOGGER.warning("Invalid bucket and/or key, %s, %s", bucket, key) + continue + + key = unquote(key) + + yield event_name, bucket, key, metadata diff --git a/homeassistant/components/minio/services.yaml b/homeassistant/components/minio/services.yaml new file mode 100644 index 00000000000..8fb8a267c3b --- /dev/null +++ b/homeassistant/components/minio/services.yaml @@ -0,0 +1,35 @@ +get: + description: Download file from Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg + file_path: + description: File path on local filesystem. + example: /data/camera_files/snapshot.jpg + +put: + description: Upload file to Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg + file_path: + description: File path on local filesystem. + example: /data/camera_files/snapshot.jpg + +remove: + description: Delete file from Minio. + fields: + bucket: + description: Bucket to use. + example: camera-files + key: + description: Object key of the file. + example: front_camera/2018/01/02/snapshot_12512514.jpg diff --git a/homeassistant/components/mobile_app/.translations/es-419.json b/homeassistant/components/mobile_app/.translations/es-419.json index 417d0627616..271e38147c3 100644 --- a/homeassistant/components/mobile_app/.translations/es-419.json +++ b/homeassistant/components/mobile_app/.translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "install_app": "Abra la aplicaci\u00f3n m\u00f3vil para configurar la integraci\u00f3n con Home Assistant. Consulte [los documentos] ({apps_url}) para obtener una lista de aplicaciones compatibles." + }, "step": { "confirm": { "title": "Aplicaci\u00f3n movil" diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 43c1a89435f..4fc9fb808c6 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -2,6 +2,7 @@ import logging import struct +from typing import Any, Union import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -36,6 +37,26 @@ DATA_TYPE_UINT = "uint" REGISTER_TYPE_HOLDING = "holding" REGISTER_TYPE_INPUT = "input" + +def number(value: Any) -> Union[int, float]: + """Coerce a value to number without losing precision.""" + if isinstance(value, int): + return value + + if isinstance(value, str): + try: + value = int(value) + return value + except (TypeError, ValueError): + pass + + try: + value = float(value) + return value + except (TypeError, ValueError): + raise vol.Invalid("invalid number {}".format(value)) + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_REGISTERS): [ @@ -47,13 +68,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT, DATA_TYPE_CUSTOM] ), vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_OFFSET, default=0): number, vol.Optional(CONF_PRECISION, default=0): cv.positive_int, vol.Optional(CONF_REGISTER_TYPE, default=REGISTER_TYPE_HOLDING): vol.In( [REGISTER_TYPE_HOLDING, REGISTER_TYPE_INPUT] ), vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, - vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), + vol.Optional(CONF_SCALE, default=1): number, vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -207,6 +228,10 @@ class ModbusRegisterSensor(RestoreEntity): return byte_string = b"".join([x.to_bytes(2, byteorder="big") for x in registers]) val = struct.unpack(self._structure, byte_string)[0] - self._value = format( - self._scale * val + self._offset, ".{}f".format(self._precision) - ) + val = self._scale * val + self._offset + if isinstance(val, int): + self._value = str(val) + if self._precision > 0: + self._value += "." + "0" * self._precision + else: + self._value = f"{val:.{self._precision}f}" diff --git a/homeassistant/components/moon/.translations/sensor.es-419.json b/homeassistant/components/moon/.translations/sensor.es-419.json index 71cfab736cb..89823dd2055 100644 --- a/homeassistant/components/moon/.translations/sensor.es-419.json +++ b/homeassistant/components/moon/.translations/sensor.es-419.json @@ -2,11 +2,6 @@ "state": { "first_quarter": "Cuarto creciente", "full_moon": "Luna llena", - "last_quarter": "Cuarto menguante", - "new_moon": "Luna nueva", - "waning_crescent": "Luna menguante", - "waning_gibbous": "Luna menguante gibosa", - "waxing_crescent": "Luna creciente", - "waxing_gibbous": "Luna creciente gibosa" + "last_quarter": "Cuarto menguante" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/hr.json b/homeassistant/components/mqtt/.translations/hr.json new file mode 100644 index 00000000000..b3c82fdd8db --- /dev/null +++ b/homeassistant/components/mqtt/.translations/hr.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "broker": { + "data": { + "password": "Lozinka", + "port": "Port", + "username": "Korisni\u010dko ime" + }, + "title": "MQTT" + } + }, + "title": "MQTT" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 0a93c29e8b0..75552d1d14b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -416,7 +416,7 @@ async def async_subscribe( topic: str, msg_callback: MessageCallbackType, qos: int = DEFAULT_QOS, - encoding: str = "utf-8", + encoding: Optional[str] = "utf-8", ): """Subscribe to an MQTT topic. @@ -431,7 +431,7 @@ async def async_subscribe( ) wrapped_msg_callback = msg_callback - # If we have 3 paramaters with no default value, wrap the callback + # If we have 3 parameters with no default value, wrap the callback if non_default == 3: _LOGGER.warning( "Signature of MQTT msg_callback '%s.%s' is deprecated", @@ -829,7 +829,11 @@ class MQTT: return self.hass.async_add_job(stop) async def async_subscribe( - self, topic: str, msg_callback: MessageCallbackType, qos: int, encoding: str + self, + topic: str, + msg_callback: MessageCallbackType, + qos: int, + encoding: Optional[str] = None, ) -> Callable[[], None]: """Set up a subscription to a topic with the provided qos. diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py new file mode 100644 index 00000000000..2350dfc6634 --- /dev/null +++ b/homeassistant/components/mqtt/abbreviations.py @@ -0,0 +1,180 @@ +"""Abbreviations supported by MQTT discovery.""" + +ABBREVIATIONS = { + "act_t": "action_topic", + "act_tpl": "action_template", + "aux_cmd_t": "aux_command_topic", + "aux_stat_tpl": "aux_state_template", + "aux_stat_t": "aux_state_topic", + "avty_t": "availability_topic", + "away_mode_cmd_t": "away_mode_command_topic", + "away_mode_stat_tpl": "away_mode_state_template", + "away_mode_stat_t": "away_mode_state_topic", + "b_tpl": "blue_template", + "bri_cmd_t": "brightness_command_topic", + "bri_scl": "brightness_scale", + "bri_stat_t": "brightness_state_topic", + "bri_tpl": "brightness_template", + "bri_val_tpl": "brightness_value_template", + "clr_temp_cmd_tpl": "color_temp_command_template", + "bat_lev_t": "battery_level_topic", + "bat_lev_tpl": "battery_level_template", + "chrg_t": "charging_topic", + "chrg_tpl": "charging_template", + "clr_temp_cmd_t": "color_temp_command_topic", + "clr_temp_stat_t": "color_temp_state_topic", + "clr_temp_tpl": "color_temp_template", + "clr_temp_val_tpl": "color_temp_value_template", + "cln_t": "cleaning_topic", + "cln_tpl": "cleaning_template", + "cmd_off_tpl": "command_off_template", + "cmd_on_tpl": "command_on_template", + "cmd_t": "command_topic", + "cmd_tpl": "command_template", + "cod_arm_req": "code_arm_required", + "cod_dis_req": "code_disarm_required", + "curr_temp_t": "current_temperature_topic", + "curr_temp_tpl": "current_temperature_template", + "dev": "device", + "dev_cla": "device_class", + "dock_t": "docked_topic", + "dock_tpl": "docked_template", + "err_t": "error_topic", + "err_tpl": "error_template", + "fanspd_t": "fan_speed_topic", + "fanspd_tpl": "fan_speed_template", + "fanspd_lst": "fan_speed_list", + "flsh_tlng": "flash_time_long", + "flsh_tsht": "flash_time_short", + "fx_cmd_t": "effect_command_topic", + "fx_list": "effect_list", + "fx_stat_t": "effect_state_topic", + "fx_tpl": "effect_template", + "fx_val_tpl": "effect_value_template", + "exp_aft": "expire_after", + "fan_mode_cmd_t": "fan_mode_command_topic", + "fan_mode_stat_tpl": "fan_mode_state_template", + "fan_mode_stat_t": "fan_mode_state_topic", + "frc_upd": "force_update", + "g_tpl": "green_template", + "hold_cmd_t": "hold_command_topic", + "hold_stat_tpl": "hold_state_template", + "hold_stat_t": "hold_state_topic", + "hs_cmd_t": "hs_command_topic", + "hs_stat_t": "hs_state_topic", + "hs_val_tpl": "hs_value_template", + "ic": "icon", + "init": "initial", + "json_attr": "json_attributes", + "json_attr_t": "json_attributes_topic", + "json_attr_tpl": "json_attributes_template", + "max_temp": "max_temp", + "min_temp": "min_temp", + "mode_cmd_t": "mode_command_topic", + "mode_stat_tpl": "mode_state_template", + "mode_stat_t": "mode_state_topic", + "name": "name", + "off_dly": "off_delay", + "on_cmd_type": "on_command_type", + "opt": "optimistic", + "osc_cmd_t": "oscillation_command_topic", + "osc_stat_t": "oscillation_state_topic", + "osc_val_tpl": "oscillation_value_template", + "pl_arm_away": "payload_arm_away", + "pl_arm_home": "payload_arm_home", + "pl_arm_nite": "payload_arm_night", + "pl_avail": "payload_available", + "pl_cln_sp": "payload_clean_spot", + "pl_cls": "payload_close", + "pl_disarm": "payload_disarm", + "pl_hi_spd": "payload_high_speed", + "pl_lock": "payload_lock", + "pl_loc": "payload_locate", + "pl_lo_spd": "payload_low_speed", + "pl_med_spd": "payload_medium_speed", + "pl_not_avail": "payload_not_available", + "pl_off": "payload_off", + "pl_off_spd": "payload_off_speed", + "pl_on": "payload_on", + "pl_open": "payload_open", + "pl_osc_off": "payload_oscillation_off", + "pl_osc_on": "payload_oscillation_on", + "pl_paus": "payload_pause", + "pl_stop": "payload_stop", + "pl_strt": "payload_start", + "pl_stpa": "payload_start_pause", + "pl_ret": "payload_return_to_base", + "pl_toff": "payload_turn_off", + "pl_ton": "payload_turn_on", + "pl_unlk": "payload_unlock", + "pos_clsd": "position_closed", + "pos_open": "position_open", + "pow_cmd_t": "power_command_topic", + "pow_stat_t": "power_state_topic", + "pow_stat_tpl": "power_state_template", + "r_tpl": "red_template", + "ret": "retain", + "rgb_cmd_tpl": "rgb_command_template", + "rgb_cmd_t": "rgb_command_topic", + "rgb_stat_t": "rgb_state_topic", + "rgb_val_tpl": "rgb_value_template", + "send_cmd_t": "send_command_topic", + "send_if_off": "send_if_off", + "set_fan_spd_t": "set_fan_speed_topic", + "set_pos_tpl": "set_position_template", + "set_pos_t": "set_position_topic", + "pos_t": "position_topic", + "spd_cmd_t": "speed_command_topic", + "spd_stat_t": "speed_state_topic", + "spd_val_tpl": "speed_value_template", + "spds": "speeds", + "stat_clsd": "state_closed", + "stat_off": "state_off", + "stat_on": "state_on", + "stat_open": "state_open", + "stat_t": "state_topic", + "stat_tpl": "state_template", + "stat_val_tpl": "state_value_template", + "sup_feat": "supported_features", + "swing_mode_cmd_t": "swing_mode_command_topic", + "swing_mode_stat_tpl": "swing_mode_state_template", + "swing_mode_stat_t": "swing_mode_state_topic", + "temp_cmd_t": "temperature_command_topic", + "temp_hi_cmd_t": "temperature_high_command_topic", + "temp_hi_stat_tpl": "temperature_high_state_template", + "temp_hi_stat_t": "temperature_high_state_topic", + "temp_lo_cmd_t": "temperature_low_command_topic", + "temp_lo_stat_tpl": "temperature_low_state_template", + "temp_lo_stat_t": "temperature_low_state_topic", + "temp_stat_tpl": "temperature_state_template", + "temp_stat_t": "temperature_state_topic", + "tilt_clsd_val": "tilt_closed_value", + "tilt_cmd_t": "tilt_command_topic", + "tilt_inv_stat": "tilt_invert_state", + "tilt_max": "tilt_max", + "tilt_min": "tilt_min", + "tilt_opnd_val": "tilt_opened_value", + "tilt_opt": "tilt_optimistic", + "tilt_status_t": "tilt_status_topic", + "tilt_status_tpl": "tilt_status_template", + "t": "topic", + "uniq_id": "unique_id", + "unit_of_meas": "unit_of_measurement", + "val_tpl": "value_template", + "whit_val_cmd_t": "white_value_command_topic", + "whit_val_scl": "white_value_scale", + "whit_val_stat_t": "white_value_state_topic", + "whit_val_tpl": "white_value_template", + "xy_cmd_t": "xy_command_topic", + "xy_stat_t": "xy_state_topic", + "xy_val_tpl": "xy_value_template", +} + +DEVICE_ABBREVIATIONS = { + "cns": "connections", + "ids": "identifiers", + "name": "name", + "mf": "manufacturer", + "mdl": "model", + "sw": "sw_version", +} diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 032a176932e..66e14ca9a5a 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -102,7 +102,7 @@ TILT_FEATURES = ( def validate_options(value): """Validate options. - If set postion topic is set then get position topic is set as well. + If set position topic is set then get position topic is set as well. """ if CONF_SET_POSITION_TOPIC in value and CONF_GET_POSITION_TOPIC not in value: raise vol.Invalid( diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 1d307599698..d611b8db13e 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -10,6 +10,7 @@ from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType +from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS from .const import ATTR_DISCOVERY_HASH, CONF_STATE_TOPIC _LOGGER = logging.getLogger(__name__) @@ -64,150 +65,6 @@ MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" TOPIC_BASE = "~" -ABBREVIATIONS = { - "aux_cmd_t": "aux_command_topic", - "aux_stat_tpl": "aux_state_template", - "aux_stat_t": "aux_state_topic", - "avty_t": "availability_topic", - "away_mode_cmd_t": "away_mode_command_topic", - "away_mode_stat_tpl": "away_mode_state_template", - "away_mode_stat_t": "away_mode_state_topic", - "b_tpl": "blue_template", - "bri_cmd_t": "brightness_command_topic", - "bri_scl": "brightness_scale", - "bri_stat_t": "brightness_state_topic", - "bri_tpl": "brightness_template", - "bri_val_tpl": "brightness_value_template", - "clr_temp_cmd_tpl": "color_temp_command_template", - "bat_lev_t": "battery_level_topic", - "bat_lev_tpl": "battery_level_template", - "chrg_t": "charging_topic", - "chrg_tpl": "charging_template", - "clr_temp_cmd_t": "color_temp_command_topic", - "clr_temp_stat_t": "color_temp_state_topic", - "clr_temp_val_tpl": "color_temp_value_template", - "cln_t": "cleaning_topic", - "cln_tpl": "cleaning_template", - "cmd_off_tpl": "command_off_template", - "cmd_on_tpl": "command_on_template", - "cmd_t": "command_topic", - "curr_temp_t": "current_temperature_topic", - "curr_temp_tpl": "current_temperature_template", - "dev": "device", - "dev_cla": "device_class", - "dock_t": "docked_topic", - "dock_tpl": "docked_template", - "err_t": "error_topic", - "err_tpl": "error_template", - "fanspd_t": "fan_speed_topic", - "fanspd_tpl": "fan_speed_template", - "fanspd_lst": "fan_speed_list", - "fx_cmd_t": "effect_command_topic", - "fx_list": "effect_list", - "fx_stat_t": "effect_state_topic", - "fx_tpl": "effect_template", - "fx_val_tpl": "effect_value_template", - "exp_aft": "expire_after", - "fan_mode_cmd_t": "fan_mode_command_topic", - "fan_mode_stat_tpl": "fan_mode_state_template", - "fan_mode_stat_t": "fan_mode_state_topic", - "frc_upd": "force_update", - "g_tpl": "green_template", - "hold_cmd_t": "hold_command_topic", - "hold_stat_tpl": "hold_state_template", - "hold_stat_t": "hold_state_topic", - "ic": "icon", - "init": "initial", - "json_attr": "json_attributes", - "json_attr_t": "json_attributes_topic", - "max_temp": "max_temp", - "min_temp": "min_temp", - "mode_cmd_t": "mode_command_topic", - "mode_stat_tpl": "mode_state_template", - "mode_stat_t": "mode_state_topic", - "name": "name", - "on_cmd_type": "on_command_type", - "opt": "optimistic", - "osc_cmd_t": "oscillation_command_topic", - "osc_stat_t": "oscillation_state_topic", - "osc_val_tpl": "oscillation_value_template", - "pl_arm_away": "payload_arm_away", - "pl_arm_home": "payload_arm_home", - "pl_avail": "payload_available", - "pl_cls": "payload_close", - "pl_disarm": "payload_disarm", - "pl_hi_spd": "payload_high_speed", - "pl_lock": "payload_lock", - "pl_lo_spd": "payload_low_speed", - "pl_med_spd": "payload_medium_speed", - "pl_not_avail": "payload_not_available", - "pl_off": "payload_off", - "pl_on": "payload_on", - "pl_open": "payload_open", - "pl_osc_off": "payload_oscillation_off", - "pl_osc_on": "payload_oscillation_on", - "pl_stop": "payload_stop", - "pl_unlk": "payload_unlock", - "pow_cmd_t": "power_command_topic", - "r_tpl": "red_template", - "ret": "retain", - "rgb_cmd_tpl": "rgb_command_template", - "rgb_cmd_t": "rgb_command_topic", - "rgb_stat_t": "rgb_state_topic", - "rgb_val_tpl": "rgb_value_template", - "send_cmd_t": "send_command_topic", - "send_if_off": "send_if_off", - "set_pos_tpl": "set_position_template", - "set_pos_t": "set_position_topic", - "pos_t": "position_topic", - "spd_cmd_t": "speed_command_topic", - "spd_stat_t": "speed_state_topic", - "spd_val_tpl": "speed_value_template", - "spds": "speeds", - "stat_clsd": "state_closed", - "stat_off": "state_off", - "stat_on": "state_on", - "stat_open": "state_open", - "stat_t": "state_topic", - "stat_tpl": "state_template", - "stat_val_tpl": "state_value_template", - "sup_feat": "supported_features", - "swing_mode_cmd_t": "swing_mode_command_topic", - "swing_mode_stat_tpl": "swing_mode_state_template", - "swing_mode_stat_t": "swing_mode_state_topic", - "temp_cmd_t": "temperature_command_topic", - "temp_stat_tpl": "temperature_state_template", - "temp_stat_t": "temperature_state_topic", - "tilt_clsd_val": "tilt_closed_value", - "tilt_cmd_t": "tilt_command_topic", - "tilt_inv_stat": "tilt_invert_state", - "tilt_max": "tilt_max", - "tilt_min": "tilt_min", - "tilt_opnd_val": "tilt_opened_value", - "tilt_status_opt": "tilt_status_optimistic", - "tilt_status_t": "tilt_status_topic", - "t": "topic", - "uniq_id": "unique_id", - "unit_of_meas": "unit_of_measurement", - "val_tpl": "value_template", - "whit_val_cmd_t": "white_value_command_topic", - "whit_val_scl": "white_value_scale", - "whit_val_stat_t": "white_value_state_topic", - "whit_val_tpl": "white_value_template", - "xy_cmd_t": "xy_command_topic", - "xy_stat_t": "xy_state_topic", - "xy_val_tpl": "xy_value_template", -} - -DEVICE_ABBREVIATIONS = { - "cns": "connections", - "ids": "identifiers", - "name": "name", - "mf": "manufacturer", - "mdl": "model", - "sw": "sw_version", -} - def clear_discovery_hash(hass, discovery_hash): """Clear entry in ALREADY_DISCOVERED list.""" diff --git a/homeassistant/components/nest/.translations/hr.json b/homeassistant/components/nest/.translations/hr.json new file mode 100644 index 00000000000..b96a358f2f0 --- /dev/null +++ b/homeassistant/components/nest/.translations/hr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "invalid_code": "Neispravan kod" + }, + "step": { + "init": { + "data": { + "flow_impl": "Pru\u017eatelj usluge" + }, + "title": "Pru\u017eatelj usluge autentifikacije" + }, + "link": { + "data": { + "code": "PIN kod" + } + } + }, + "title": "Nest" + } +} \ No newline at end of file diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 60909ccaee7..e4909ce68fc 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -43,6 +43,7 @@ EVENT_SMS = "netgear_lte_sms" SERVICE_DELETE_SMS = "delete_sms" SERVICE_SET_OPTION = "set_option" SERVICE_CONNECT_LTE = "connect_lte" +SERVICE_DISCONNECT_LTE = "disconnect_lte" ATTR_HOST = "host" ATTR_SMS_ID = "sms_id" @@ -122,6 +123,8 @@ SET_OPTION_SCHEMA = vol.Schema( CONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string}) +DISCONNECT_LTE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST): cv.string}) + @attr.s class ModemData: @@ -199,18 +202,20 @@ async def async_setup(hass, config): await modem_data.modem.set_autoconnect_mode(autoconnect) elif service.service == SERVICE_CONNECT_LTE: await modem_data.modem.connect_lte() + elif service.service == SERVICE_DISCONNECT_LTE: + await modem_data.modem.disconnect_lte() - hass.services.async_register( - DOMAIN, SERVICE_DELETE_SMS, service_handler, schema=DELETE_SMS_SCHEMA - ) + service_schemas = { + SERVICE_DELETE_SMS: DELETE_SMS_SCHEMA, + SERVICE_SET_OPTION: SET_OPTION_SCHEMA, + SERVICE_CONNECT_LTE: CONNECT_LTE_SCHEMA, + SERVICE_DISCONNECT_LTE: DISCONNECT_LTE_SCHEMA, + } - hass.services.async_register( - DOMAIN, SERVICE_SET_OPTION, service_handler, schema=SET_OPTION_SCHEMA - ) - - hass.services.async_register( - DOMAIN, SERVICE_CONNECT_LTE, service_handler, schema=CONNECT_LTE_SCHEMA - ) + for service, schema in service_schemas.items(): + hass.services.async_register( + DOMAIN, service, service_handler, schema=schema + ) netgear_lte_config = config[DOMAIN] diff --git a/homeassistant/components/netgear_lte/manifest.json b/homeassistant/components/netgear_lte/manifest.json index 12cf81f46bf..609ea72cc69 100644 --- a/homeassistant/components/netgear_lte/manifest.json +++ b/homeassistant/components/netgear_lte/manifest.json @@ -3,8 +3,10 @@ "name": "Netgear lte", "documentation": "https://www.home-assistant.io/components/netgear_lte", "requirements": [ - "eternalegypt==0.0.8" + "eternalegypt==0.0.10" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@amelchio" + ] } diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 0555ab1ffe8..4f13662519d 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -1,4 +1,4 @@ -"""Suport for Netgear LTE notifications.""" +"""Support for Netgear LTE notifications.""" import logging import attr diff --git a/homeassistant/components/netgear_lte/services.yaml b/homeassistant/components/netgear_lte/services.yaml index 4ba3afb07b4..564fb914cf9 100644 --- a/homeassistant/components/netgear_lte/services.yaml +++ b/homeassistant/components/netgear_lte/services.yaml @@ -27,3 +27,10 @@ connect_lte: host: description: The modem that should connect. example: 192.168.5.1 + +disconnect_lte: + description: Ask the modem to close the LTE connection. + fields: + host: + description: The modem that should disconnect. + example: 192.168.5.1 diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 409b4d38208..38b7018af6c 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -24,14 +24,12 @@ DOMAIN = "nissan_leaf" DATA_LEAF = "nissan_leaf_data" DATA_BATTERY = "battery" -DATA_LOCATION = "location" DATA_CHARGING = "charging" DATA_PLUGGED_IN = "plugged_in" DATA_CLIMATE = "climate" DATA_RANGE_AC = "range_ac_on" DATA_RANGE_AC_OFF = "range_ac_off" -CONF_NCONNECT = "nissan_connect" CONF_INTERVAL = "update_interval" CONF_CHARGING_INTERVAL = "update_interval_charging" CONF_CLIMATE_INTERVAL = "update_interval_climate" @@ -61,7 +59,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_REGION): vol.In(CONF_VALID_REGIONS), - vol.Optional(CONF_NCONNECT, default=True): cv.boolean, vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): ( vol.All(cv.time_period, vol.Clamp(min=MIN_UPDATE_INTERVAL)) ), @@ -84,7 +81,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -LEAF_COMPONENTS = ["sensor", "switch", "binary_sensor", "device_tracker"] +LEAF_COMPONENTS = ["sensor", "switch", "binary_sensor"] SIGNAL_UPDATE_LEAF = "nissan_leaf_update" @@ -177,8 +174,7 @@ def setup(hass, config): hass.data[DATA_LEAF][leaf.vin] = data_store for component in LEAF_COMPONENTS: - if component != "device_tracker" or car_config[CONF_NCONNECT]: - load_platform(hass, component, DOMAIN, {}, car_config) + load_platform(hass, component, DOMAIN, {}, car_config) async_track_point_in_utc_time( hass, data_store.async_update_data, utcnow() + INITIAL_UPDATE @@ -209,24 +205,20 @@ class LeafDataStore: self.hass = hass self.leaf = leaf self.car_config = car_config - self.nissan_connect = car_config[CONF_NCONNECT] self.force_miles = car_config[CONF_FORCE_MILES] self.data = {} self.data[DATA_CLIMATE] = False self.data[DATA_BATTERY] = 0 self.data[DATA_CHARGING] = False - self.data[DATA_LOCATION] = False self.data[DATA_RANGE_AC] = 0 self.data[DATA_RANGE_AC_OFF] = 0 self.data[DATA_PLUGGED_IN] = False self.next_update = None self.last_check = None self.request_in_progress = False - # Timestamp of last successful response from battery, - # climate or location. + # Timestamp of last successful response from battery or climate. self.last_battery_response = None self.last_climate_response = None - self.last_location_response = None self._remove_listener = None async def async_update_data(self, now): @@ -334,20 +326,6 @@ class LeafDataStore: except CarwingsError: _LOGGER.error("Error fetching climate info") - if self.nissan_connect: - try: - location_response = await self.async_get_location() - - if location_response is None: - _LOGGER.debug("Empty Location Response Received") - self.data[DATA_LOCATION] = None - else: - _LOGGER.debug("Location Response: %s", location_response.__dict__) - self.data[DATA_LOCATION] = location_response - self.last_location_response = utcnow() - except CarwingsError: - _LOGGER.error("Error fetching location info") - self.request_in_progress = False async_dispatcher_send(self.hass, SIGNAL_UPDATE_LEAF) @@ -364,19 +342,6 @@ class LeafDataStore: from pycarwings2 import CarwingsError try: - # First, check nissan servers for the latest data - start_server_info = await self.hass.async_add_executor_job( - self.leaf.get_latest_battery_status - ) - - # Store the date from the nissan servers - start_date = self._extract_start_date(start_server_info) - if start_date is None: - _LOGGER.info("No start date from servers. Aborting") - return None - - _LOGGER.debug("Start server date=%s", start_date) - # Request battery update from the car _LOGGER.debug("Requesting battery update, %s", self.leaf.vin) request = await self.hass.async_add_executor_job(self.leaf.request_update) @@ -393,21 +358,30 @@ class LeafDataStore: ) await asyncio.sleep(PYCARWINGS2_SLEEP) - # Note leaf.get_status_from_update is always returning 0, so - # don't try to use it anymore. - server_info = await self.hass.async_add_executor_job( - self.leaf.get_latest_battery_status + # We don't use the response from get_status_from_update + # apart from knowing that the car has responded saying it + # has given the latest battery status to Nissan. + check_result_info = await self.hass.async_add_executor_job( + self.leaf.get_status_from_update, request ) - latest_date = self._extract_start_date(server_info) - _LOGGER.debug("Latest server date=%s", latest_date) - if latest_date is not None and latest_date != start_date: + if check_result_info is not None: + # Get the latest battery status from Nissan servers. + # This has the SOC in it. + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status + ) return server_info _LOGGER.debug( "%s attempts exceeded return latest data from server", MAX_RESPONSE_ATTEMPTS, ) + # Get the latest data from the nissan servers, even though + # it may be out of date, it's better than nothing. + server_info = await self.hass.async_add_executor_job( + self.leaf.get_latest_battery_status + ) return server_info except CarwingsError: _LOGGER.error("An error occurred getting battery status.") @@ -465,29 +439,6 @@ class LeafDataStore: _LOGGER.debug("Climate result not returned by Nissan servers") return False - async def async_get_location(self): - """Get location from Nissan servers.""" - request = await self.hass.async_add_executor_job(self.leaf.request_location) - for attempt in range(MAX_RESPONSE_ATTEMPTS): - if attempt > 0: - _LOGGER.debug( - "Location data not in yet. (%s) (%s). " "Waiting %s seconds", - self.leaf.vin, - attempt, - PYCARWINGS2_SLEEP, - ) - await asyncio.sleep(PYCARWINGS2_SLEEP) - - location_status = await self.hass.async_add_executor_job( - self.leaf.get_status_from_location, request - ) - - if location_status is not None: - _LOGGER.debug("Location_status=%s", location_status.__dict__) - break - - return location_status - class LeafEntity(Entity): """Base class for Nissan Leaf entity.""" diff --git a/homeassistant/components/nissan_leaf/device_tracker.py b/homeassistant/components/nissan_leaf/device_tracker.py deleted file mode 100644 index 11d18ee5a8e..00000000000 --- a/homeassistant/components/nissan_leaf/device_tracker.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Support for tracking a Nissan Leaf.""" -import logging - -from homeassistant.helpers.dispatcher import dispatcher_connect -from homeassistant.util import slugify - -from . import DATA_LEAF, DATA_LOCATION, SIGNAL_UPDATE_LEAF - -_LOGGER = logging.getLogger(__name__) - -ICON_CAR = "mdi:car" - - -def setup_scanner(hass, config, see, discovery_info=None): - """Set up the Nissan Leaf tracker.""" - if discovery_info is None: - return False - - def see_vehicle(): - """Handle the reporting of the vehicle position.""" - for vin, datastore in hass.data[DATA_LEAF].items(): - host_name = datastore.leaf.nickname - dev_id = "nissan_leaf_{}".format(slugify(host_name)) - if not datastore.data[DATA_LOCATION]: - _LOGGER.debug("No position found for vehicle %s", vin) - return - _LOGGER.debug( - "Updating device_tracker for %s with position %s", - datastore.leaf.nickname, - datastore.data[DATA_LOCATION].__dict__, - ) - attrs = {"updated_on": datastore.last_location_response} - see( - dev_id=dev_id, - host_name=host_name, - gps=( - datastore.data[DATA_LOCATION].latitude, - datastore.data[DATA_LOCATION].longitude, - ), - attributes=attrs, - icon=ICON_CAR, - ) - - dispatcher_connect(hass, SIGNAL_UPDATE_LEAF, see_vehicle) - - return True diff --git a/homeassistant/components/nissan_leaf/manifest.json b/homeassistant/components/nissan_leaf/manifest.json index ab94c01b7c1..70aaa112414 100644 --- a/homeassistant/components/nissan_leaf/manifest.json +++ b/homeassistant/components/nissan_leaf/manifest.json @@ -3,7 +3,7 @@ "name": "Nissan leaf", "documentation": "https://www.home-assistant.io/components/nissan_leaf", "requirements": [ - "pycarwings2==2.8" + "pycarwings2==2.9" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 8483b703a70..773c08808c3 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -2,6 +2,7 @@ import asyncio import logging from functools import partial +from typing import Optional import voluptuous as vol @@ -10,8 +11,12 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.helpers import config_per_platform, discovery +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) # Platform specific data @@ -164,7 +169,7 @@ async def async_setup(hass, config): class BaseNotificationService: """An abstract class for notification services.""" - hass = None + hass: Optional[HomeAssistantType] = None def send_message(self, message, **kwargs): """Send a message. diff --git a/homeassistant/components/notion/.translations/cy.json b/homeassistant/components/notion/.translations/cy.json new file mode 100644 index 00000000000..63b1c613505 --- /dev/null +++ b/homeassistant/components/notion/.translations/cy.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Cyfrinair", + "username": "Enw Defnyddiwr / Cyfeiriad E-bost" + }, + "title": "Llenwch eich gwybodaeth" + } + }, + "title": "Syniad" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/es-419.json b/homeassistant/components/notion/.translations/es-419.json new file mode 100644 index 00000000000..ad2f19b0668 --- /dev/null +++ b/homeassistant/components/notion/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "invalid_credentials": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos", + "no_devices": "No se han encontrado dispositivos en la cuenta." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario/direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Complete su informaci\u00f3n" + } + }, + "title": "Noci\u00f3n" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/hr.json b/homeassistant/components/notion/.translations/hr.json new file mode 100644 index 00000000000..b20317a236a --- /dev/null +++ b/homeassistant/components/notion/.translations/hr.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Korisni\u010dko ime je ve\u0107 registrirano", + "invalid_credentials": "Neispravno korisni\u010dko ime ili lozinka", + "no_devices": "Nisu prona\u0111eni ure\u0111aji na ra\u010dunu" + }, + "step": { + "user": { + "data": { + "password": "Lozinka", + "username": "Korisni\u010dko ime/adresa e-po\u0161te" + }, + "title": "Ispunite svoje podatke" + } + }, + "title": "Pojam" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json index 0c1fe674887..c35de9c535c 100644 --- a/homeassistant/components/notion/.translations/pl.json +++ b/homeassistant/components/notion/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "Nazwa u\u017cytkownika jest ju\u017c zarejestrowana", + "identifier_exists": "Nazwa u\u017cytkownika ju\u017c zarejestrowana", "invalid_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" }, @@ -14,6 +14,6 @@ "title": "Wprowad\u017a swoje dane" } }, - "title": "Notion" + "title": "Poj\u0119cie" } } \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/es-419.json b/homeassistant/components/onboarding/.translations/es-419.json new file mode 100644 index 00000000000..747074436d7 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/es-419.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Habitaci\u00f3n", + "kitchen": "Cocina", + "living_room": "Sala" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/id.json b/homeassistant/components/onboarding/.translations/id.json new file mode 100644 index 00000000000..33e8a88a9ae --- /dev/null +++ b/homeassistant/components/onboarding/.translations/id.json @@ -0,0 +1,5 @@ +{ + "area": { + "kitchen": "Dapur" + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/.translations/vi.json b/homeassistant/components/onboarding/.translations/vi.json new file mode 100644 index 00000000000..652307f69e1 --- /dev/null +++ b/homeassistant/components/onboarding/.translations/vi.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "Ph\u00f2ng ng\u1ee7", + "kitchen": "Ph\u00f2ng b\u1ebfp", + "living_room": "Ph\u00f2ng kh\u00e1ch" + } +} \ No newline at end of file diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index c740582ebc9..68f14846af7 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -3,7 +3,7 @@ "name": "Opencv", "documentation": "https://www.home-assistant.io/components/opencv", "requirements": [ - "numpy==1.16.4", + "numpy==1.17.0", "opencv-python-headless==4.1.0.25" ], "dependencies": [], diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 7e91f78f74c..b20d97dadce 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -30,6 +30,7 @@ from .const import ( ATTR_GW_ID, ATTR_MODE, ATTR_LEVEL, + ATTR_DHW_OVRD, CONF_CLIMATE, CONF_FLOOR_TEMP, CONF_PRECISION, @@ -38,6 +39,7 @@ from .const import ( SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, + SERVICE_SET_HOT_WATER_OVRD, SERVICE_SET_GPIO_MODE, SERVICE_SET_LED_MODE, SERVICE_SET_MAX_MOD, @@ -123,6 +125,16 @@ def register_services(hass): ), } ) + service_set_hot_water_ovrd_schema = vol.Schema( + { + vol.Required(ATTR_GW_ID): vol.All( + cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS]) + ), + vol.Required(ATTR_DHW_OVRD): vol.Any( + vol.Equal("A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1)) + ), + } + ) service_set_gpio_mode_schema = vol.Schema( vol.Any( vol.Schema( @@ -216,6 +228,21 @@ def register_services(hass): service_set_control_setpoint_schema, ) + async def set_dhw_ovrd(call): + """Set the domestic hot water override on the OpenTherm Gateway.""" + gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] + gw_var = gw_vars.OTGW_DHW_OVRD + value = await gw_dev.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD]) + gw_dev.status.update({gw_var: value}) + async_dispatcher_send(hass, gw_dev.update_signal, gw_dev.status) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_HOT_WATER_OVRD, + set_dhw_ovrd, + service_set_hot_water_ovrd_schema, + ) + async def set_device_clock(call): """Set the clock on the OpenTherm Gateway.""" gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]] diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 5b0411a2c72..77b0bf9b313 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -6,6 +6,7 @@ from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS ATTR_GW_ID = "gateway_id" ATTR_MODE = "mode" ATTR_LEVEL = "level" +ATTR_DHW_OVRD = "dhw_override" CONF_CLIMATE = "climate" CONF_FLOOR_TEMP = "floor_temperature" @@ -21,6 +22,7 @@ DEVICE_CLASS_PROBLEM = "problem" SERVICE_RESET_GATEWAY = "reset_gateway" SERVICE_SET_CLOCK = "set_clock" SERVICE_SET_CONTROL_SETPOINT = "set_control_setpoint" +SERVICE_SET_HOT_WATER_OVRD = "set_hot_water_ovrd" SERVICE_SET_GPIO_MODE = "set_gpio_mode" SERVICE_SET_LED_MODE = "set_led_mode" SERVICE_SET_MAX_MOD = "set_max_modulation" diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml index d8fe2c7e406..4e9e727ef5a 100644 --- a/homeassistant/components/opentherm_gw/services.yaml +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -35,6 +35,23 @@ set_control_setpoint: A value of 0 disables the central heating setpoint override. example: '37.5' +set_hot_water_ovrd: + description: > + Set the domestic hot water enable option on the gateway. + fields: + gateway_id: + description: The gateway_id of the OpenTherm Gateway. + example: 'opentherm_gateway' + dhw_override: + description: > + Control the domestic hot water enable option. If the boiler has + been configured to let the room unit control when to keep a + small amount of water preheated, this command can influence + that. + Value should be 0 or 1 to enable the override in off or on + state, or "A" to disable the override. + example: '1' + set_gpio_mode: description: Change the function of the GPIO pins of the gateway. fields: diff --git a/homeassistant/components/openuv/.translations/hr.json b/homeassistant/components/openuv/.translations/hr.json new file mode 100644 index 00000000000..835929d26df --- /dev/null +++ b/homeassistant/components/openuv/.translations/hr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "elevation": "Elevacija", + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index c47d8811e68..2d19ab25fe7 100644 --- a/homeassistant/components/pi_hole/manifest.json +++ b/homeassistant/components/pi_hole/manifest.json @@ -3,7 +3,7 @@ "name": "Pi hole", "documentation": "https://www.home-assistant.io/components/pi_hole", "requirements": [ - "hole==0.3.0" + "hole==0.5.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/plaato/.translations/es-419.json b/homeassistant/components/plaato/.translations/es-419.json new file mode 100644 index 00000000000..d63802984ef --- /dev/null +++ b/homeassistant/components/plaato/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "La instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes de Plaato Airlock.", + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n de webhook en Plaato Airlock. \n\n Complete la siguiente informaci\u00f3n: \n\n - URL: `{webhook_url}` \n - M\u00e9todo: POST \n\n Consulte [la documentaci\u00f3n]({docs_url}) para obtener m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfEst\u00e1 seguro de que deseas configurar Plaato Airlock?", + "title": "Configurar el Webhook de Plaato" + } + }, + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/fr.json b/homeassistant/components/plaato/.translations/fr.json new file mode 100644 index 00000000000..091c680be4c --- /dev/null +++ b/homeassistant/components/plaato/.translations/fr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/hr.json b/homeassistant/components/plaato/.translations/hr.json new file mode 100644 index 00000000000..680571040b1 --- /dev/null +++ b/homeassistant/components/plaato/.translations/hr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Va\u0161 Home Assistant mora biti dostupan s interneta za primanje poruka od Plaato Airlocka.", + "one_instance_allowed": "Potrebna je samo jedna instanca." + }, + "create_entry": { + "default": "Za slanje doga\u0111aja kod ku\u0107nog pomo\u0107nika, morat \u0107ete postaviti zna\u010dajku webhook u Plaato Airlock.\n\nIspunite sljede\u0107e informacije:\n\n-URL: ' {webhook_url} '\n-Metoda: POST\n\nZa dodatne detalje pogledajte [dokumentaciju] ({docs_url})." + }, + "step": { + "user": { + "description": "Jeste li sigurni da \u017eelite postaviti Plaato Airlock?", + "title": "Postavljanje Plaato Webhook" + } + }, + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/pl.json b/homeassistant/components/plaato/.translations/pl.json index aa7eb5f29bc..aac48ee4774 100644 --- a/homeassistant/components/plaato/.translations/pl.json +++ b/homeassistant/components/plaato/.translations/pl.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Czy chcesz skonfigurowa\u0107 Plaato Airlock?", + "description": "Czy na pewno chcesz skonfigurowa\u0107 Airlock Plaato?", "title": "Konfiguracja Plaato Webhook" } }, diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 2579a004e88..98137897149 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -262,7 +262,7 @@ def request_configuration(host, hass, config, add_entities_callback): host, data.get("token"), cv.boolean(data.get("has_ssl")), - cv.boolean(data.get("do_not_verify")), + cv.boolean(data.get("do_not_verify_ssl")), hass, config, add_entities_callback, diff --git a/homeassistant/components/plugwise/__init__.py b/homeassistant/components/plugwise/__init__.py new file mode 100644 index 00000000000..afde7eae5d6 --- /dev/null +++ b/homeassistant/components/plugwise/__init__.py @@ -0,0 +1 @@ +"""Plugwise Climate (current only Anna) component for HomeAssistant.""" diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py new file mode 100644 index 00000000000..b3b4cf3c1d4 --- /dev/null +++ b/homeassistant/components/plugwise/climate.py @@ -0,0 +1,213 @@ +"""Plugwise Climate component for HomeAssistant.""" + +import logging + +import voluptuous as vol +import haanna + +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_IDLE, + HVAC_MODE_AUTO, + HVAC_MODE_OFF, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + TEMP_CELSIUS, +) +from homeassistant.exceptions import PlatformNotReady + +SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + +_LOGGER = logging.getLogger(__name__) + +# Configuration directives +CONF_MIN_TEMP = "min_temp" +CONF_MAX_TEMP = "max_temp" + +# Default directives +DEFAULT_NAME = "Plugwise Thermostat" +DEFAULT_USERNAME = "smile" +DEFAULT_TIMEOUT = 10 +DEFAULT_PORT = 80 +DEFAULT_ICON = "mdi:thermometer" +DEFAULT_MIN_TEMP = 4 +DEFAULT_MAX_TEMP = 30 + +# HVAC modes +ATTR_HVAC_MODES = [HVAC_MODE_AUTO, HVAC_MODE_OFF] + +# Read platform configuration +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, + vol.Optional(CONF_MIN_TEMP, default=DEFAULT_MIN_TEMP): cv.positive_int, + vol.Optional(CONF_MAX_TEMP, default=DEFAULT_MAX_TEMP): cv.positive_int, + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Add the Plugwise (Anna) Thermostate.""" + api = haanna.Haanna( + config[CONF_USERNAME], + config[CONF_PASSWORD], + config[CONF_HOST], + config[CONF_PORT], + ) + try: + api.ping_anna_thermostat() + except OSError: + _LOGGER.debug("Ping failed, retrying later", exc_info=True) + raise PlatformNotReady + devices = [ + ThermostatDevice( + api, config[CONF_NAME], config[CONF_MIN_TEMP], config[CONF_MAX_TEMP] + ) + ] + add_entities(devices, True) + + +class ThermostatDevice(ClimateDevice): + """Representation of an Plugwise thermostat.""" + + def __init__(self, api, name, min_temp, max_temp): + """Set up the Plugwise API.""" + self._api = api + self._min_temp = min_temp + self._max_temp = max_temp + self._name = name + self._domain_objects = None + self._outdoor_temperature = None + self._active_schema = None + self._preset_mode = None + self._hvac_modes = ATTR_HVAC_MODES + + @property + def hvac_action(self): + """Return the current action.""" + if self._api.get_heating_status(self._domain_objects): + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + + @property + def name(self): + """Return the name of the thermostat, if any.""" + return self._name + + @property + def icon(self): + """Return the icon to use in the frontend.""" + return DEFAULT_ICON + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attributes = {} + attributes["outdoor_temperature"] = self._outdoor_temperature + attributes["available_schemas"] = self._api.get_schema_names( + self._domain_objects + ) + attributes["active_schema"] = self._active_schema + return attributes + + def update(self): + """Update the data from the thermostat.""" + _LOGGER.debug("Update called") + self._domain_objects = self._api.get_domain_objects() + self._outdoor_temperature = self._api.get_outdoor_temperature( + self._domain_objects + ) + self._active_schema = self._api.get_active_schema_name(self._domain_objects) + + @property + def hvac_mode(self): + """Return current active hvac state.""" + if self._api.get_schema_state(self._domain_objects): + return HVAC_MODE_AUTO + return HVAC_MODE_OFF + + @property + def preset_mode(self): + """Return the active preset mode.""" + return self._api.get_current_preset(self._domain_objects) + + @property + def preset_modes(self): + """Return the available preset modes list without values.""" + presets = list(self._api.get_presets(self._domain_objects)) + return presets + + @property + def hvac_modes(self): + """Return the available hvac modes list.""" + return self._hvac_modes + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._api.get_temperature(self._domain_objects) + + @property + def min_temp(self): + """Return the minimal temperature possible to set.""" + return self._min_temp + + @property + def max_temp(self): + """Return the maximum temperature possible to set.""" + return self._max_temp + + @property + def target_temperature(self): + """Return the target temperature.""" + return self._api.get_target_temperature(self._domain_objects) + + @property + def temperature_unit(self): + """Return the unit of measured temperature.""" + return TEMP_CELSIUS + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + _LOGGER.debug("Adjusting temperature") + temperature = kwargs.get(ATTR_TEMPERATURE) + if temperature is not None and self._min_temp < temperature < self._max_temp: + _LOGGER.debug("Changing temporary temperature") + self._api.set_temperature(self._domain_objects, temperature) + else: + _LOGGER.error("Invalid temperature requested") + + def set_hvac_mode(self, hvac_mode): + """Set the hvac mode.""" + _LOGGER.debug("Adjusting hvac_mode (i.e. schedule/schema)") + schema_mode = "false" + if hvac_mode == HVAC_MODE_AUTO: + schema_mode = "true" + self._api.set_schema_state( + self._domain_objects, self._active_schema, schema_mode + ) + + def set_preset_mode(self, preset_mode): + """Set the preset mode.""" + _LOGGER.debug("Changing preset mode") + self._preset_mode = preset_mode + self._api.set_preset(self._domain_objects, preset_mode) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json new file mode 100644 index 00000000000..c399232f315 --- /dev/null +++ b/homeassistant/components/plugwise/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "plugwise", + "name": "Plugwise", + "documentation": "https://www.home-assistant.io/components/plugwise", + "dependencies": [], + "codeowners": ["@laetificat","@CoMPaTech"], + "requirements": ["haanna==0.10.1"] +} diff --git a/homeassistant/components/point/.translations/es-419.json b/homeassistant/components/point/.translations/es-419.json index c20e3350272..7436513ba6f 100644 --- a/homeassistant/components/point/.translations/es-419.json +++ b/homeassistant/components/point/.translations/es-419.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_setup": "Solo puede configurar una cuenta Point.", + "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", "external_setup": "Punto configurado con \u00e9xito desde otro flujo." }, "error": { diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index b55fb9f681d..4a0db111b7d 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -17,7 +17,7 @@ _LOGGER = logging.getLogger(__name__) EVENT_MAP = { "off": STATE_ALARM_DISARMED, - "alarm_silenced": STATE_ALARM_ARMED_AWAY, + "alarm_silenced": STATE_ALARM_DISARMED, "alarm_grace_period_expired": STATE_ALARM_TRIGGERED, } @@ -63,11 +63,14 @@ class MinutPointAlarmControl(AlarmControlPanel): """Process new event from the webhook.""" _type = data.get("event", {}).get("type") _device_id = data.get("event", {}).get("device_id") - if _device_id not in self._home["devices"] or _type not in EVENT_MAP: + _changed_by = data.get("event", {}).get("user_id") + if ( + _device_id not in self._home["devices"] and _type not in EVENT_MAP + ) and _type != "alarm_silenced": # alarm_silenced does not have device_id return - _LOGGER.debug("Recieved webhook: %s", _type) - self._home["alarm_status"] = EVENT_MAP[_type] - self._changed_by = _device_id + _LOGGER.debug("Received webhook: %s", _type) + self._home["alarm_status"] = _type + self._changed_by = _changed_by self.async_schedule_update_ha_state() @property diff --git a/homeassistant/components/point/binary_sensor.py b/homeassistant/components/point/binary_sensor.py index a5704a1fc17..a08f7dbedc4 100644 --- a/homeassistant/components/point/binary_sensor.py +++ b/homeassistant/components/point/binary_sensor.py @@ -106,7 +106,7 @@ class MinutPointBinarySensor(MinutPointEntity, BinarySensorDevice): _device_id = data.get("event", {}).get("device_id") if _type not in self._events or _device_id != self.device.device_id: return - _LOGGER.debug("Recieved webhook: %s", _type) + _LOGGER.debug("Received webhook: %s", _type) if _type == self._events[0]: self._is_on = True if _type == self._events[1]: diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 80b68e52885..1ba2c4809b6 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -10,13 +10,16 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, + ATTR_DEVICE_CLASS, CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, TEMP_FAHRENHEIT, + TEMP_CELSIUS, ) from homeassistant.helpers import entityfilter, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.util.temperature import fahrenheit_to_celsius +from homeassistant.helpers.entity_values import EntityValues _LOGGER = logging.getLogger(__name__) @@ -25,6 +28,14 @@ API_ENDPOINT = "/api/prometheus" DOMAIN = "prometheus" CONF_FILTER = "filter" CONF_PROM_NAMESPACE = "namespace" +CONF_COMPONENT_CONFIG = "component_config" +CONF_COMPONENT_CONFIG_GLOB = "component_config_glob" +CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain" +CONF_DEFAULT_METRIC = "default_metric" +CONF_OVERRIDE_METRIC = "override_metric" +COMPONENT_CONFIG_SCHEMA_ENTRY = vol.Schema( + {vol.Optional(CONF_OVERRIDE_METRIC): cv.string} +) CONFIG_SCHEMA = vol.Schema( { @@ -32,6 +43,17 @@ CONFIG_SCHEMA = vol.Schema( { vol.Optional(CONF_FILTER, default={}): entityfilter.FILTER_SCHEMA, vol.Optional(CONF_PROM_NAMESPACE): cv.string, + vol.Optional(CONF_DEFAULT_METRIC): cv.string, + vol.Optional(CONF_OVERRIDE_METRIC): cv.string, + vol.Optional(CONF_COMPONENT_CONFIG, default={}): vol.Schema( + {cv.entity_id: COMPONENT_CONFIG_SCHEMA_ENTRY} + ), + vol.Optional(CONF_COMPONENT_CONFIG_GLOB, default={}): vol.Schema( + {cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY} + ), + vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}): vol.Schema( + {cv.string: COMPONENT_CONFIG_SCHEMA_ENTRY} + ), } ) }, @@ -49,8 +71,22 @@ def setup(hass, config): entity_filter = conf[CONF_FILTER] namespace = conf.get(CONF_PROM_NAMESPACE) climate_units = hass.config.units.temperature_unit + override_metric = conf.get(CONF_OVERRIDE_METRIC) + default_metric = conf.get(CONF_DEFAULT_METRIC) + component_config = EntityValues( + conf[CONF_COMPONENT_CONFIG], + conf[CONF_COMPONENT_CONFIG_DOMAIN], + conf[CONF_COMPONENT_CONFIG_GLOB], + ) + metrics = PrometheusMetrics( - prometheus_client, entity_filter, namespace, climate_units + prometheus_client, + entity_filter, + namespace, + climate_units, + component_config, + override_metric, + default_metric, ) hass.bus.listen(EVENT_STATE_CHANGED, metrics.handle_event) @@ -60,12 +96,32 @@ def setup(hass, config): class PrometheusMetrics: """Model all of the metrics which should be exposed to Prometheus.""" - def __init__(self, prometheus_client, entity_filter, namespace, climate_units): + def __init__( + self, + prometheus_client, + entity_filter, + namespace, + climate_units, + component_config, + override_metric, + default_metric, + ): """Initialize Prometheus Metrics.""" self.prometheus_client = prometheus_client + self._component_config = component_config + self._override_metric = override_metric + self._default_metric = default_metric self._filter = entity_filter + self._sensor_metric_handlers = [ + self._sensor_override_component_metric, + self._sensor_override_metric, + self._sensor_attribute_metric, + self._sensor_default_metric, + self._sensor_fallback_metric, + ] + if namespace: - self.metrics_prefix = "{}_".format(namespace) + self.metrics_prefix = f"{namespace}_" else: self.metrics_prefix = "" self._metrics = {} @@ -84,7 +140,7 @@ class PrometheusMetrics: if not self._filter(state.entity_id): return - handler = "_handle_{}".format(domain) + handler = f"_handle_{domain}" if hasattr(self, handler): getattr(self, handler)(state) @@ -103,7 +159,7 @@ class PrometheusMetrics: try: return self._metrics[metric] except KeyError: - full_metric_name = "{}{}".format(self.metrics_prefix, metric) + full_metric_name = f"{self.metrics_prefix}{metric}" self._metrics[metric] = factory(full_metric_name, documentation, labels) return self._metrics[metric] @@ -229,31 +285,73 @@ class PrometheusMetrics: pass def _handle_sensor(self, state): + unit = self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) - unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - metric = state.entity_id.split(".")[1] + for metric_handler in self._sensor_metric_handlers: + metric = metric_handler(state, unit) + if metric is not None: + break - if "_" not in str(metric): - metric = state.entity_id.replace(".", "_") + if metric is not None: + _metric = self._metric( + metric, self.prometheus_client.Gauge, f"Sensor data measured in {unit}" + ) - try: - int(metric.split("_")[-1]) - metric = "_".join(metric.split("_")[:-1]) - except ValueError: - pass - - _metric = self._metric(metric, self.prometheus_client.Gauge, state.entity_id) - - try: - value = self.state_as_number(state) - if unit == TEMP_FAHRENHEIT: - value = fahrenheit_to_celsius(value) - _metric.labels(**self._labels(state)).set(value) - except ValueError: - pass + try: + value = self.state_as_number(state) + if unit == TEMP_FAHRENHEIT: + value = fahrenheit_to_celsius(value) + _metric.labels(**self._labels(state)).set(value) + except ValueError: + pass self._battery(state) + def _sensor_default_metric(self, state, unit): + """Get default metric.""" + return self._default_metric + + @staticmethod + def _sensor_attribute_metric(state, unit): + """Get metric based on device class attribute.""" + metric = state.attributes.get(ATTR_DEVICE_CLASS) + if metric is not None: + return f"{metric}_{unit}" + return None + + def _sensor_override_metric(self, state, unit): + """Get metric from override in configuration.""" + if self._override_metric: + return self._override_metric + return None + + def _sensor_override_component_metric(self, state, unit): + """Get metric from override in component confioguration.""" + return self._component_config.get(state.entity_id).get(CONF_OVERRIDE_METRIC) + + @staticmethod + def _sensor_fallback_metric(state, unit): + """Get metric from fallback logic for compatability.""" + if unit in (None, ""): + _LOGGER.debug("Unsupported sensor: %s", state.entity_id) + return None + return f"sensor_unit_{unit}" + + @staticmethod + def _unit_string(unit): + """Get a formatted string of the unit.""" + if unit is None: + return + + units = { + TEMP_CELSIUS: "c", + TEMP_FAHRENHEIT: "c", # F should go into C metric + "%": "percent", + } + default = unit.replace("/", "_per_") + default = default.lower() + return units.get(unit, default) + def _handle_switch(self, state): metric = self._metric( "switch_state", self.prometheus_client.Gauge, "State of the switch (0/1)" diff --git a/homeassistant/components/prometheus/manifest.json b/homeassistant/components/prometheus/manifest.json index d9699be6bf7..cab1228aa56 100644 --- a/homeassistant/components/prometheus/manifest.json +++ b/homeassistant/components/prometheus/manifest.json @@ -3,7 +3,7 @@ "name": "Prometheus", "documentation": "https://www.home-assistant.io/components/prometheus", "requirements": [ - "prometheus_client==0.2.0" + "prometheus_client==0.7.1" ], "dependencies": [ "http" diff --git a/homeassistant/components/proximity/__init__.py b/homeassistant/components/proximity/__init__.py index d563314225a..e9b85f79084 100644 --- a/homeassistant/components/proximity/__init__.py +++ b/homeassistant/components/proximity/__init__.py @@ -10,6 +10,9 @@ 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__) ATTR_DIR_OF_TRAVEL = "dir_of_travel" diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index a4a33efa2cd..54d5ebe5f14 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,7 +3,7 @@ "name": "Proxy", "documentation": "https://www.home-assistant.io/components/proxy", "requirements": [ - "pillow==5.4.1" + "pillow==6.1.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/ps4/.translations/es-419.json b/homeassistant/components/ps4/.translations/es-419.json index 093ee552951..0f7066df007 100644 --- a/homeassistant/components/ps4/.translations/es-419.json +++ b/homeassistant/components/ps4/.translations/es-419.json @@ -25,6 +25,12 @@ }, "description": "Ingresa tu informaci\u00f3n de PlayStation 4. Para 'PIN', navegue hasta 'Configuraci\u00f3n' en su consola PlayStation 4. Luego navegue a 'Configuraci\u00f3n de conexi\u00f3n de la aplicaci\u00f3n m\u00f3vil' y seleccione 'Agregar dispositivo'. Ingrese el PIN que se muestra.", "title": "Playstation 4" + }, + "mode": { + "data": { + "mode": "Modo de configuraci\u00f3n" + }, + "title": "Playstation 4" } }, "title": "Playstation 4" diff --git a/homeassistant/components/ps4/.translations/no.json b/homeassistant/components/ps4/.translations/no.json index 132ec5b83ec..3608a5534ab 100644 --- a/homeassistant/components/ps4/.translations/no.json +++ b/homeassistant/components/ps4/.translations/no.json @@ -25,7 +25,7 @@ "name": "Navn", "region": "Region" }, - "description": "Skriv inn PlayStation 4 informasjonen din. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4 konsollen, deretter navigerer du til 'Innstillinger for mobilapp forbindelse' og velger 'Legg til enhet'. Skriv inn PIN-koden som vises.", + "description": "Skriv inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Tast inn PIN-koden som vises. Se [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", "title": "PlayStation 4" }, "mode": { diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 1e6198f1a38..9baf1adbcc2 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -127,7 +127,7 @@ async def async_migrate_entry(hass, entry): DOMAIN, unique_id, suggested_object_id=new_id, - config_entry_id=e_entry.config_entry_id, + config_entry=entry, device_id=e_entry.device_id, ) entry.version = 3 diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 9c5dd4f73b3..e1ec32ddd1f 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -58,11 +58,6 @@ DEFAULT_RETRIES = 2 async def async_setup_entry(hass, config_entry, async_add_entities): """Set up PS4 from a config entry.""" config = config_entry - await async_setup_platform(hass, config, async_add_entities, discovery_info=None) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up PS4 Platform.""" creds = config.data[CONF_TOKEN] device_list = [] for device in config.data["devices"]: @@ -74,6 +69,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(device_list, update_before_add=True) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Not Implemented.""" + pass + + class PS4Device(MediaPlayerDevice): """Representation of a PS4.""" diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 788da6a8d64..715c06aca43 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -9,8 +9,10 @@ import voluptuous as vol from homeassistant.const import SERVICE_RELOAD from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass from homeassistant.util import sanitize_filename +from homeassistant.util.yaml.loader import load_yaml import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -90,10 +92,23 @@ def discover_scripts(hass): continue hass.services.remove(DOMAIN, existing_service) + # Load user-provided service descriptions from python_scripts/services.yaml + services_yaml = os.path.join(path, "services.yaml") + if os.path.exists(services_yaml): + services_dict = load_yaml(services_yaml) + else: + services_dict = {} + for fil in glob.iglob(os.path.join(path, "*.py")): name = os.path.splitext(os.path.basename(fil))[0] hass.services.register(DOMAIN, name, python_script_service_handler) + service_desc = { + "description": services_dict.get(name, {}).get("description", ""), + "fields": services_dict.get(name, {}).get("fields", {}), + } + async_set_service_schema(hass, DOMAIN, name, service_desc) + @bind_hass def execute_script(hass, name, data=None): diff --git a/homeassistant/components/python_script/manifest.json b/homeassistant/components/python_script/manifest.json index 0f88513bb45..610ec92a2b3 100644 --- a/homeassistant/components/python_script/manifest.json +++ b/homeassistant/components/python_script/manifest.json @@ -3,7 +3,7 @@ "name": "Python script", "documentation": "https://www.home-assistant.io/components/python_script", "requirements": [ - "restrictedpython==4.0b8" + "restrictedpython==4.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 96a351ac453..eb8da25bace 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -3,7 +3,7 @@ "name": "Qrcode", "documentation": "https://www.home-assistant.io/components/qrcode", "requirements": [ - "pillow==5.4.1", + "pillow==6.1.0", "pyzbar==0.1.7" ], "dependencies": [], diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 04ed47f2a2a..b65e6bf6044 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -117,7 +117,7 @@ class RachioStandbySwitch(RachioSwitch): @property def unique_id(self) -> str: - """Return a unique id by combinining controller id and purpose.""" + """Return a unique id by combining controller id and purpose.""" return "{}-standby".format(self._controller.controller_id) @property @@ -182,7 +182,7 @@ class RachioZone(RachioSwitch): @property def unique_id(self) -> str: - """Return a unique id by combinining controller id and zone number.""" + """Return a unique id by combining controller id and zone number.""" return "{}-zone-{}".format(self._controller.controller_id, self.zone_id) @property diff --git a/homeassistant/components/rainbird/__init__.py b/homeassistant/components/rainbird/__init__.py index 3ae1c8bf585..1d8ed8e37b1 100644 --- a/homeassistant/components/rainbird/__init__.py +++ b/homeassistant/components/rainbird/__init__.py @@ -29,13 +29,12 @@ def setup(hass, config): from pyrainbird import RainbirdController - controller = RainbirdController() - controller.setConfig(server, password) + controller = RainbirdController(server, password) _LOGGER.debug("Rain Bird Controller set to: %s", server) initial_status = controller.currentIrrigation() - if initial_status == -1: + if initial_status and initial_status["type"] != "CurrentStationsActiveResponse": _LOGGER.error("Error getting state. Possible configuration issues") return False diff --git a/homeassistant/components/rainbird/manifest.json b/homeassistant/components/rainbird/manifest.json index 24113d62534..584ea22afe2 100644 --- a/homeassistant/components/rainbird/manifest.json +++ b/homeassistant/components/rainbird/manifest.json @@ -3,7 +3,7 @@ "name": "Rainbird", "documentation": "https://www.home-assistant.io/components/rainbird", "requirements": [ - "pyrainbird==0.1.6" + "pyrainbird==0.2.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rainbird/sensor.py b/homeassistant/components/rainbird/sensor.py index d59ea3b0fec..2d4549a21d5 100644 --- a/homeassistant/components/rainbird/sensor.py +++ b/homeassistant/components/rainbird/sensor.py @@ -56,7 +56,11 @@ class RainBirdSensor(Entity): """Get the latest data and updates the states.""" _LOGGER.debug("Updating sensor: %s", self._name) if self._sensor_type == "rainsensor": - self._state = self._controller.currentRainSensorState() + result = self._controller.currentRainSensorState() + if result and result["type"] == "CurrentRainSensorStateResponse": + self._state = result["sensorState"] + else: + self._state = None @property def name(self): diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index 94b37c52fb7..a1b82bc1af7 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -70,15 +70,23 @@ class RainBirdSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the switch on.""" - self._rainbird.startIrrigation(int(self._zone), int(self._duration)) + response = self._rainbird.startIrrigation(int(self._zone), int(self._duration)) + if response and response["type"] == "AcknowledgeResponse": + self._state = True def turn_off(self, **kwargs): """Turn the switch off.""" - self._rainbird.stopIrrigation() + response = self._rainbird.stopIrrigation() + if response and response["type"] == "AcknowledgeResponse": + self._state = False def get_device_status(self): """Get the status of the switch from Rain Bird Controller.""" - return self._rainbird.currentIrrigation() == self._zone + response = self._rainbird.currentIrrigation() + if response is None: + return None + if isinstance(response, dict) and "sprinklers" in response: + return response["sprinklers"][self._zone] def update(self): """Update switch status.""" diff --git a/homeassistant/components/raspyrfm/switch.py b/homeassistant/components/raspyrfm/switch.py index 1dbe4ab776d..53cb1dbdcb5 100644 --- a/homeassistant/components/raspyrfm/switch.py +++ b/homeassistant/components/raspyrfm/switch.py @@ -1,4 +1,4 @@ -"""Support for switchs that can be controlled using the RaspyRFM rc module.""" +"""Support for switches that can be controlled using the RaspyRFM rc module.""" import logging import voluptuous as vol diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 67a426232f2..c91b910724c 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,7 +3,7 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/components/recorder", "requirements": [ - "sqlalchemy==1.3.5" + "sqlalchemy==1.3.7" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 99cfe1067e8..3172e614166 100755 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -192,7 +192,7 @@ class PublicTransportData: _LOGGER.debug("API returned error: %s", error) return except (rjpl.rjplConnectionError, rjpl.rjplHTTPError): - _LOGGER.debug("Error occured while connecting to the API") + _LOGGER.debug("Error occurred while connecting to the API") return # Filter result diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index e495874f339..450b1c123c3 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -22,6 +22,9 @@ from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA_BASE, ) + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) ATTR_ACTIVITY = "activity" diff --git a/homeassistant/components/ring/light.py b/homeassistant/components/ring/light.py new file mode 100644 index 00000000000..bd7ea3a3679 --- /dev/null +++ b/homeassistant/components/ring/light.py @@ -0,0 +1,97 @@ +"""This component provides HA switch support for Ring Door Bell/Chimes.""" +import logging +from datetime import datetime, timedelta +from homeassistant.components.light import Light +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback + +from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING + +_LOGGER = logging.getLogger(__name__) + + +# It takes a few seconds for the API to correctly return an update indicating +# that the changes have been made. Once we request a change (i.e. a light +# being turned on) we simply wait for this time delta before we allow +# updates to take place. + +SKIP_UPDATES_DELAY = timedelta(seconds=5) + +ON_STATE = "on" +OFF_STATE = "off" + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the lights for the Ring devices.""" + cameras = hass.data[DATA_RING_STICKUP_CAMS] + lights = [] + + for device in cameras: + if device.has_capability("light"): + lights.append(RingLight(device)) + + add_entities(lights, True) + + +class RingLight(Light): + """Creates a switch to turn the ring cameras light on and off.""" + + def __init__(self, device): + """Initialize the light.""" + self._device = device + self._unique_id = self._device.id + self._light_on = False + self._no_updates_until = datetime.now() + + 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) + + @property + def name(self): + """Name of the light.""" + return "{} light".format(self._device.name) + + @property + def unique_id(self): + """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.""" + self._device.lights = new_state + self._light_on = new_state == ON_STATE + self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY + self.async_schedule_update_ha_state(True) + + def turn_on(self, **kwargs): + """Turn the light on for 30 seconds.""" + self._set_light(ON_STATE) + + 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 > datetime.now(): + _LOGGER.debug("Skipping update...") + return + + self._light_on = self._device.lights == ON_STATE diff --git a/homeassistant/components/ring/switch.py b/homeassistant/components/ring/switch.py new file mode 100644 index 00000000000..3b6bd4ea024 --- /dev/null +++ b/homeassistant/components/ring/switch.py @@ -0,0 +1,108 @@ +"""This component provides HA switch support for Ring Door Bell/Chimes.""" +import logging +from datetime import datetime, timedelta +from homeassistant.components.switch import SwitchDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback + +from . import DATA_RING_STICKUP_CAMS, SIGNAL_UPDATE_RING + +_LOGGER = logging.getLogger(__name__) + +SIREN_ICON = "mdi:alarm-bell" + + +# It takes a few seconds for the API to correctly return an update indicating +# that the changes have been made. Once we request a change (i.e. a light +# being turned on) we simply wait for this time delta before we allow +# updates to take place. + +SKIP_UPDATES_DELAY = timedelta(seconds=5) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Create the switches for the Ring devices.""" + cameras = hass.data[DATA_RING_STICKUP_CAMS] + switches = [] + for device in cameras: + if device.has_capability("siren"): + switches.append(SirenSwitch(device)) + + add_entities(switches, True) + + +class BaseRingSwitch(SwitchDevice): + """Represents a switch for controlling an aspect of a ring device.""" + + def __init__(self, device, device_type): + """Initialize the switch.""" + self._device = device + self._device_type = device_type + self._unique_id = "{}-{}".format(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.""" + return "{} {}".format(self._device.name, self._device_type) + + @property + def unique_id(self): + """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): + """Initialize the switch for a device with a siren.""" + super().__init__(device, "siren") + self._no_updates_until = datetime.now() + self._siren_on = False + + def _set_switch(self, new_state): + """Update switch state, and causes HASS to correctly update.""" + self._device.siren = new_state + self._siren_on = new_state > 0 + self._no_updates_until = datetime.now() + SKIP_UPDATES_DELAY + self.schedule_update_ha_state() + + @property + def is_on(self): + """If the switch is currently on or off.""" + return self._siren_on + + def turn_on(self, **kwargs): + """Turn the siren on for 30 seconds.""" + self._set_switch(1) + + def turn_off(self, **kwargs): + """Turn the siren off.""" + self._set_switch(0) + + @property + def icon(self): + """Return the icon.""" + return SIREN_ICON + + def update(self): + """Update state of the siren.""" + if self._no_updates_until > datetime.now(): + _LOGGER.debug("Skipping update...") + return + self._siren_on = self._device.siren > 0 diff --git a/homeassistant/components/roomba/vacuum.py b/homeassistant/components/roomba/vacuum.py index ba202c4ad9e..766fd72cdba 100644 --- a/homeassistant/components/roomba/vacuum.py +++ b/homeassistant/components/roomba/vacuum.py @@ -199,7 +199,7 @@ class RoombaVacuum(VacuumDevice): await self.hass.async_add_job(self.vacuum.send_command, "resume") self._is_on = True - async def async_pause(self, **kwargs): + async def async_pause(self): """Pause the cleaning cycle.""" await self.hass.async_add_job(self.vacuum.send_command, "pause") self._is_on = False diff --git a/homeassistant/components/ruter/__init__.py b/homeassistant/components/ruter/__init__.py deleted file mode 100644 index 84e25904d9e..00000000000 --- a/homeassistant/components/ruter/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The ruter component.""" diff --git a/homeassistant/components/ruter/manifest.json b/homeassistant/components/ruter/manifest.json deleted file mode 100644 index 57688d0e025..00000000000 --- a/homeassistant/components/ruter/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "domain": "ruter", - "name": "Ruter", - "documentation": "https://www.home-assistant.io/components/ruter", - "requirements": [ - "pyruter==1.1.0" - ], - "dependencies": [], - "codeowners": [ - "@ludeeus" - ] -} diff --git a/homeassistant/components/ruter/sensor.py b/homeassistant/components/ruter/sensor.py deleted file mode 100644 index 74e84fdec7c..00000000000 --- a/homeassistant/components/ruter/sensor.py +++ /dev/null @@ -1,93 +0,0 @@ -"""A sensor to provide information about next departures from Ruter.""" -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 -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -_LOGGER = logging.getLogger(__name__) - -CONF_STOP_ID = "stop_id" -CONF_DESTINATION = "destination" -CONF_OFFSET = "offset" - -DEFAULT_NAME = "Ruter" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_STOP_ID): cv.positive_int, - vol.Optional(CONF_DESTINATION): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_OFFSET, default=0): cv.positive_int, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Create the sensor.""" - from pyruter.api import Departures - - _LOGGER.warning( - "The API used in this sensor is shutting down soon, " - "you should consider starting to use the " - "'entur_public_transport' sensor instead" - ) - stop_id = config[CONF_STOP_ID] - destination = config.get(CONF_DESTINATION) - name = config[CONF_NAME] - offset = config[CONF_OFFSET] - - session = async_get_clientsession(hass) - ruter = Departures(hass.loop, stop_id, destination, session) - sensor = [RuterSensor(ruter, name, offset)] - async_add_entities(sensor, True) - - -class RuterSensor(Entity): - """Representation of a Ruter sensor.""" - - def __init__(self, ruter, name, offset): - """Initialize the sensor.""" - self.ruter = ruter - self._attributes = {} - self._name = name - self._offset = offset - self._state = None - - async def async_update(self): - """Get the latest data from the Ruter API.""" - await self.ruter.get_departures() - if self.ruter.departures is None: - _LOGGER.error("No data recieved from Ruter.") - return - try: - data = self.ruter.departures[self._offset] - self._state = data["time"] - self._attributes["line"] = data["line"] - self._attributes["destination"] = data["destination"] - except (KeyError, IndexError) as error: - _LOGGER.debug("Error getting data from Ruter, %s", error) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def icon(self): - """Return the icon of the sensor.""" - return "mdi:bus" - - @property - def device_state_attributes(self): - """Return attributes for the sensor.""" - return self._attributes diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index f5870e42490..bf5e90e21f1 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -10,6 +10,7 @@ from homeassistant.const import ( CONF_HOST, CONF_API_KEY, CONF_NAME, + CONF_PATH, CONF_PORT, CONF_SENSORS, CONF_SSL, @@ -69,6 +70,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_API_KEY): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PATH): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_SENSORS): vol.All( @@ -102,17 +104,20 @@ async def async_configure_sabnzbd( host = config[CONF_HOST] port = config[CONF_PORT] + web_root = config.get(CONF_PATH) uri_scheme = "https" if use_ssl else "http" base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) if api_key is None: conf = await hass.async_add_job(load_json, hass.config.path(CONFIG_FILE)) api_key = conf.get(base_url, {}).get(CONF_API_KEY, "") - sab_api = SabnzbdApi(base_url, api_key, session=async_get_clientsession(hass)) + sab_api = SabnzbdApi( + base_url, api_key, web_root=web_root, session=async_get_clientsession(hass) + ) if await async_check_sabnzbd(sab_api): async_setup_sabnzbd(hass, sab_api, config, name) else: - async_request_configuration(hass, config, base_url) + async_request_configuration(hass, config, base_url, web_root) async def async_setup(hass, config): @@ -181,7 +186,7 @@ def async_setup_sabnzbd(hass, sab_api, config, name): @callback -def async_request_configuration(hass, config, host): +def async_request_configuration(hass, config, host, web_root): """Request configuration steps from the user.""" from pysabnzbd import SabnzbdApi @@ -197,7 +202,9 @@ def async_request_configuration(hass, config, host): async def async_configuration_callback(data): """Handle configuration changes.""" api_key = data.get(CONF_API_KEY) - sab_api = SabnzbdApi(host, api_key, session=async_get_clientsession(hass)) + sab_api = SabnzbdApi( + host, api_key, web_root=web_root, session=async_get_clientsession(hass) + ) if not await async_check_sabnzbd(sab_api): return diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 5ddb1116d8f..1f71a24c304 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -12,6 +12,9 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "scene" STATE = "scening" STATES = "states" diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index c7e60140dbf..ec9807d4e00 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -3,7 +3,7 @@ "name": "Scrape", "documentation": "https://www.home-assistant.io/components/scrape", "requirements": [ - "beautifulsoup4==4.7.1" + "beautifulsoup4==4.8.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index 44e11d83afa..d810d50cfbf 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -20,6 +20,7 @@ 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 ENTITY_SERVICE_SCHEMA +from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.script import Script @@ -31,6 +32,9 @@ ATTR_LAST_ACTION = "last_action" ATTR_LAST_TRIGGERED = "last_triggered" ATTR_VARIABLES = "variables" +CONF_DESCRIPTION = "description" +CONF_EXAMPLE = "example" +CONF_FIELDS = "fields" CONF_SEQUENCE = "sequence" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -38,7 +42,17 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" GROUP_NAME_ALL_SCRIPTS = "all scripts" SCRIPT_ENTRY_SCHEMA = vol.Schema( - {CONF_ALIAS: cv.string, vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA} + { + CONF_ALIAS: cv.string, + vol.Required(CONF_SEQUENCE): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_DESCRIPTION, default=""): cv.string, + vol.Optional(CONF_FIELDS, default={}): { + cv.string: { + vol.Optional(CONF_DESCRIPTION): cv.string, + vol.Optional(CONF_EXAMPLE): cv.string, + } + }, + } ) CONFIG_SCHEMA = vol.Schema( @@ -137,6 +151,13 @@ async def _async_process_config(hass, config, component): DOMAIN, object_id, service_handler, schema=SCRIPT_SERVICE_SCHEMA ) + # Register the service description + service_desc = { + CONF_DESCRIPTION: cfg[CONF_DESCRIPTION], + CONF_FIELDS: cfg[CONF_FIELDS], + } + async_set_service_schema(hass, DOMAIN, object_id, service_desc) + await component.async_add_entities(scripts) diff --git a/homeassistant/components/season/.translations/sensor.es-419.json b/homeassistant/components/season/.translations/sensor.es-419.json index 65df6a58b10..09ad22740cd 100644 --- a/homeassistant/components/season/.translations/sensor.es-419.json +++ b/homeassistant/components/season/.translations/sensor.es-419.json @@ -2,7 +2,6 @@ "state": { "autumn": "Oto\u00f1o", "spring": "Primavera", - "summer": "Verano", - "winter": "Invierno" + "summer": "Verano" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/season.hr.json b/homeassistant/components/sensor/.translations/season.hr.json new file mode 100644 index 00000000000..ff36d1ca66b --- /dev/null +++ b/homeassistant/components/sensor/.translations/season.hr.json @@ -0,0 +1,8 @@ +{ + "state": { + "autumn": "Jesen", + "spring": "Prolje\u0107e", + "summer": "Ljeto", + "winter": "Zima" + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index cc460cdd4a9..9ca11b5266a 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -21,6 +21,9 @@ from homeassistant.helpers.config_validation import ( # noqa ) from homeassistant.helpers.entity_component import EntityComponent + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "sensor" diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index eeb5f1c5309..7ecc298e3f6 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -3,7 +3,7 @@ "name": "Shodan", "documentation": "https://www.home-assistant.io/components/shodan", "requirements": [ - "shodan==1.13.0" + "shodan==1.15.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 028121a9663..d44a1c7760a 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -170,7 +170,7 @@ class SimpliSafeAlarm(AlarmControlPanel): """Update alarm status.""" event_data = self._simplisafe.last_event_data[self._system.system_id] - if event_data["pinName"]: + if event_data.get("pinName"): self._changed_by = event_data["pinName"] if self._system.state == SystemStates.error: diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 130a9d23a3a..8a03ac47402 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/simplisafe", "requirements": [ - "simplisafe-python==4.2.0" + "simplisafe-python==4.3.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 09fcda7f8c8..771641c9b1d 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -35,7 +35,7 @@ async def async_setup(hass, config): """Filters out excessively verbose logs from SocketIO.""" def filter(self, record): - if record.msg.contains("waiting for connection"): + if "waiting for connection" in record.msg: return False return True diff --git a/homeassistant/components/sisyphus/manifest.json b/homeassistant/components/sisyphus/manifest.json index cac821aa9f2..f8e6e1bf14d 100644 --- a/homeassistant/components/sisyphus/manifest.json +++ b/homeassistant/components/sisyphus/manifest.json @@ -3,7 +3,7 @@ "name": "Sisyphus", "documentation": "https://www.home-assistant.io/components/sisyphus", "requirements": [ - "sisyphus-control==2.2" + "sisyphus-control==2.2.1" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sma/manifest.json b/homeassistant/components/sma/manifest.json index 8795029bff2..ea3a33d55ff 100644 --- a/homeassistant/components/sma/manifest.json +++ b/homeassistant/components/sma/manifest.json @@ -3,7 +3,7 @@ "name": "Sma", "documentation": "https://www.home-assistant.io/components/sma", "requirements": [ - "pysma==0.3.2" + "pysma==0.3.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 8e6b94ef5f8..34aed146cf0 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -19,6 +19,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval +from homeassistant.const import MINOR_VERSION, MAJOR_VERSION _LOGGER = logging.getLogger(__name__) @@ -30,6 +31,7 @@ CONF_SENSORS = "sensors" CONF_UNIT = "unit" GROUPS = ["user", "installer"] +OLD_CONFIG_DEPRECATED = MAJOR_VERSION > 0 or MINOR_VERSION > 98 def _check_sensor_schema(conf): @@ -41,16 +43,39 @@ def _check_sensor_schema(conf): except (ImportError, AttributeError): return conf - for name in conf[CONF_CUSTOM]: - valid.append(name) + customs = list(conf[CONF_CUSTOM].keys()) - for sname, attrs in conf[CONF_SENSORS].items(): - if sname not in valid: - raise vol.Invalid("{} does not exist".format(sname)) - for attr in attrs: - if attr in valid: - continue - raise vol.Invalid("{} does not exist [{}]".format(attr, sname)) + if isinstance(conf[CONF_SENSORS], dict): + msg = '"sensors" should be a simple list from 0.99' + if OLD_CONFIG_DEPRECATED: + raise vol.Invalid(msg) + _LOGGER.warning(msg) + valid.extend(customs) + + for sname, attrs in conf[CONF_SENSORS].items(): + if sname not in valid: + raise vol.Invalid("{} does not exist".format(sname)) + if attrs: + _LOGGER.warning( + "Attributes on sensors will be deprecated in 0.99. Start using only individual sensors: %s: %s", + sname, + ", ".join(attrs), + ) + for attr in attrs: + if attr in valid: + continue + raise vol.Invalid("{} does not exist [{}]".format(attr, sname)) + return conf + + # Sensors is a list (only option from from 0.99) + for sensor in conf[CONF_SENSORS]: + if sensor in customs: + _LOGGER.warning( + "All custom sensors will be added automatically, no need to include them in sensors: %s", + sensor, + ) + elif sensor not in valid: + raise vol.Invalid("{} does not exist".format(sensor)) return conf @@ -59,7 +84,7 @@ CUSTOM_SCHEMA = vol.Any( vol.Required(CONF_KEY): vol.All(cv.string, vol.Length(min=13, max=15)), vol.Required(CONF_UNIT): cv.string, vol.Optional(CONF_FACTOR, default=1): vol.Coerce(float), - vol.Optional(CONF_PATH): vol.All(cv.ensure_list, [str]), + vol.Optional(CONF_PATH): vol.All(cv.ensure_list, [cv.string]), } ) @@ -71,8 +96,9 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_GROUP, default=GROUPS[0]): vol.In(GROUPS), - vol.Optional(CONF_SENSORS, default={}): cv.schema_with_slug_keys( - cv.ensure_list + vol.Optional(CONF_SENSORS, default=[]): vol.Any( + cv.schema_with_slug_keys(cv.ensure_list), # will be deprecated + vol.All(cv.ensure_list, [str]), ), vol.Optional(CONF_CUSTOM, default={}): cv.schema_with_slug_keys( CUSTOM_SCHEMA @@ -104,20 +130,29 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Use all sensors by default config_sensors = config[CONF_SENSORS] - if not config_sensors: - config_sensors = {s.name: [] for s in sensor_def} - - # Prepare all HASS sensor entities hass_sensors = [] used_sensors = [] - 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)) - used_sensors.append(name) - used_sensors.extend(attr) - async_add_entities(hass_sensors) + if isinstance(config_sensors, dict): # will be remove from 0.99 + if not config_sensors: # Use all sensors by default + config_sensors = {s.name: [] for s in sensor_def} + + # Prepare all HASS 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)) + used_sensors.append(name) + used_sensors.extend(attr) + + if isinstance(config_sensors, list): + if not config_sensors: # Use all sensors by default + config_sensors = [s.name for s in sensor_def] + used_sensors = list(set(config_sensors + list(config[CONF_CUSTOM].keys()))) + for sensor in used_sensors: + hass_sensors.append(SMAsensor(sensor_def[sensor], [])) + used_sensors = [sensor_def[s] for s in set(used_sensors)] + async_add_entities(hass_sensors) # Init the SMA interface session = async_get_clientsession(hass, verify_ssl=config[CONF_VERIFY_SSL]) @@ -172,7 +207,7 @@ class SMAsensor(Entity): def __init__(self, pysma_sensor, sub_sensors): """Initialize the sensor.""" self._sensor = pysma_sensor - self._sub_sensors = sub_sensors + self._sub_sensors = sub_sensors # Can be remove from 0.99 self._attr = {s.name: "" for s in sub_sensors} self._state = self._sensor.value @@ -193,7 +228,7 @@ class SMAsensor(Entity): return self._sensor.unit @property - def device_state_attributes(self): + def device_state_attributes(self): # Can be remove from 0.99 """Return the state attributes of the sensor.""" return self._attr @@ -206,7 +241,7 @@ class SMAsensor(Entity): """Update this sensor.""" update = False - for sens in self._sub_sensors: + for sens in self._sub_sensors: # Can be remove from 0.99 newval = "{} {}".format(sens.value, sens.unit) if self._attr[sens.name] != newval: update = True diff --git a/homeassistant/components/smhi/config_flow.py b/homeassistant/components/smhi/config_flow.py index a68c8293a9f..3b60cb66165 100644 --- a/homeassistant/components/smhi/config_flow.py +++ b/homeassistant/components/smhi/config_flow.py @@ -1,7 +1,7 @@ """Config flow to configure SMHI component.""" import voluptuous as vol -from homeassistant import config_entries, data_entry_flow +from homeassistant import config_entries from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import aiohttp_client @@ -21,7 +21,7 @@ def smhi_locations(hass: HomeAssistant): @config_entries.HANDLERS.register(DOMAIN) -class SmhiFlowHandler(data_entry_flow.FlowHandler): +class SmhiFlowHandler(config_entries.ConfigFlow): """Config flow for SMHI component.""" VERSION = 1 diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index 0007a5a66e5..a3ac3af985d 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -3,7 +3,7 @@ "name": "Snmp", "documentation": "https://www.home-assistant.io/components/snmp", "requirements": [ - "pysnmp==4.4.9" + "pysnmp==4.4.11" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/somfy/.translations/fr.json b/homeassistant/components/somfy/.translations/fr.json index 6367e411552..ba873c4f029 100644 --- a/homeassistant/components/somfy/.translations/fr.json +++ b/homeassistant/components/somfy/.translations/fr.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Somfy.", - "authorize_url_timeout": "Durée expirée pour la génération de l'url d'autorisation.", - "missing_configuration": "Le composant Somfy n'est pas configuré. Merci de suivre la documentation." + "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration d'url autoriser.", + "missing_configuration": "Le composant Somfy n'est pas configur\u00e9. Veuillez suivre la documentation." }, "create_entry": { - "default": "Authentification réussie avec Somfy." + "default": "Authentifi\u00e9 avec succ\u00e8s avec Somfy." }, "title": "Somfy" } diff --git a/homeassistant/components/somfy/.translations/hr.json b/homeassistant/components/somfy/.translations/hr.json new file mode 100644 index 00000000000..3a904102076 --- /dev/null +++ b/homeassistant/components/somfy/.translations/hr.json @@ -0,0 +1,8 @@ +{ + "config": { + "create_entry": { + "default": "Uspje\u0161no autentificirano sa Somfy." + }, + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/.translations/hr.json b/homeassistant/components/sonos/.translations/hr.json new file mode 100644 index 00000000000..c91f9a78c29 --- /dev/null +++ b/homeassistant/components/sonos/.translations/hr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Sonos" + } + }, + "title": "Sonos" + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 4e65edc1f5b..8c231ec63e0 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/sonos", "requirements": [ - "pysonos==0.0.22" + "pysonos==0.0.23" ], "dependencies": [], "ssdp": { diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 38ae99f2d3f..86e30621334 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -15,6 +15,7 @@ from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, MEDIA_TYPE_MUSIC, + MEDIA_TYPE_PLAYLIST, SUPPORT_CLEAR_PLAYLIST, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -102,6 +103,7 @@ class SonosData: def __init__(self, hass): """Initialize the data.""" self.entities = [] + self.discovered = [] self.topology_condition = asyncio.Condition() @@ -132,14 +134,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Handle a (re)discovered player.""" try: _LOGGER.debug("Reached _discovered_player, soco=%s", soco) - entity = _get_entity_from_soco_uid(hass, soco.uid) - if not entity: + if soco not in hass.data[DATA_SONOS].discovered: _LOGGER.debug("Adding new entity") + hass.data[DATA_SONOS].discovered.append(soco) hass.add_job(async_add_entities, [SonosEntity(soco)]) else: - _LOGGER.debug("Seen %s", entity) - hass.add_job(entity.async_seen()) + entity = _get_entity_from_soco_uid(hass, soco.uid) + if entity: + _LOGGER.debug("Seen %s", entity) + hass.add_job(entity.async_seen()) except SoCoException as ex: _LOGGER.debug("SoCoException, ex=%s", ex) @@ -222,7 +226,10 @@ class _ProcessSonosEventQueue: def put(self, item, block=True, timeout=None): """Process event.""" - self._handler(item) + try: + self._handler(item) + except SoCoException as ex: + _LOGGER.warning("Error calling %s: %s", self._handler, ex) def _get_entity_from_soco_uid(hass, uid): @@ -318,7 +325,7 @@ class SonosEntity(MediaPlayerDevice): self._night_sound = None self._speech_enhance = None self._source_name = None - self._favorites = None + self._favorites = [] self._soco_snapshot = None self._snapshot_group = None @@ -748,6 +755,8 @@ class SonosEntity(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" + if self._player_volume is None: + return None return self._player_volume / 100 @property @@ -930,20 +939,35 @@ class SonosEntity(MediaPlayerDevice): """ Send the play_media command to the media player. + If media_type is "playlist", media_id should be a Sonos + Playlist name. Otherwise, media_id should be a URI. + If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ - if kwargs.get(ATTR_MEDIA_ENQUEUE): + if media_type == MEDIA_TYPE_MUSIC: + if kwargs.get(ATTR_MEDIA_ENQUEUE): + try: + self.soco.add_uri_to_queue(media_id) + except SoCoUPnPException: + _LOGGER.error( + 'Error parsing media uri "%s", ' + "please check it's a valid media resource " + "supported by Sonos", + media_id, + ) + else: + self.soco.play_uri(media_id) + elif media_type == MEDIA_TYPE_PLAYLIST: try: - self.soco.add_uri_to_queue(media_id) - except SoCoUPnPException: - _LOGGER.error( - 'Error parsing media uri "%s", ' - "please check it's a valid media resource " - "supported by Sonos", - media_id, - ) + playlists = self.soco.get_sonos_playlists() + playlist = next(p for p in playlists if p.title == media_id) + self.soco.clear_queue() + self.soco.add_to_queue(playlist) + self.soco.play_from_queue(0) + except StopIteration: + _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) else: - self.soco.play_uri(media_id) + _LOGGER.error('Sonos does not support a media type of "%s"', media_type) @soco_error() def join(self, slaves): diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 62b591dbe54..a489e3fd736 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,7 +3,7 @@ "name": "Sql", "documentation": "https://www.home-assistant.io/components/sql", "requirements": [ - "sqlalchemy==1.3.5" + "sqlalchemy==1.3.7" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 56d8fcdb4b6..a9873c76afe 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -43,6 +43,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SRP energy.""" + _LOGGER.warning( + "The srp_energy integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + name = config[CONF_NAME] username = config[CONF_USERNAME] password = config[CONF_PASSWORD] diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index b5a903f9fae..a8591ac042b 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -112,7 +112,7 @@ class Scanner: return (entry, info_from_entry(entry, None), domains) return None - # Multiple entries usally share same location. Make sure + # Multiple entries usually share same location. Make sure # we fetch it only once. info_req = self._description_cache.get(xml_location) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 30e112fdfbf..51868c6d0a8 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, EVENT_HOMEASSISTANT_START, STATE_UNKNOWN, + STATE_UNAVAILABLE, ATTR_UNIT_OF_MEASUREMENT, ) from homeassistant.core import callback @@ -81,10 +82,7 @@ class StatisticsSensor(Entity): """Initialize the Statistics sensor.""" self._entity_id = entity_id self.is_binary = self._entity_id.split(".")[0] == "binary_sensor" - if not self.is_binary: - self._name = "{} {}".format(name, ATTR_MEAN) - else: - self._name = "{} {}".format(name, ATTR_COUNT) + self._name = name self._sampling_size = sampling_size self._max_age = max_age self._precision = precision @@ -131,7 +129,7 @@ class StatisticsSensor(Entity): def _add_state_to_queue(self, new_state): """Add the state to the queue.""" - if new_state.state == STATE_UNKNOWN: + if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: return try: diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 4fd3e71a48b..50cc1d8169d 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -4,6 +4,11 @@ import threading import voluptuous as vol +try: + import uvloop +except ImportError: + uvloop = None + from homeassistant.auth.util import generate_secret import homeassistant.helpers.config_validation as cv from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_FILENAME @@ -38,7 +43,7 @@ SERVICE_RECORD_SCHEMA = STREAM_SERVICE_SCHEMA.extend( vol.Optional(CONF_LOOKBACK, default=0): int, } ) - +DATA_UVLOOP_WARN = "stream_uvloop_warn" # Set log level to error for libav logging.getLogger("libav").setLevel(logging.ERROR) @@ -49,6 +54,21 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N if DOMAIN not in hass.config.components: raise HomeAssistantError("Stream integration is not set up.") + if DATA_UVLOOP_WARN not in hass.data: + hass.data[DATA_UVLOOP_WARN] = True + # Warn about https://github.com/home-assistant/home-assistant/issues/22999 + if ( + uvloop is not None + and isinstance(hass.loop, uvloop.Loop) + and ( + "shell_command" in hass.config.components + or "ffmpeg" in hass.config.components + ) + ): + _LOGGER.warning( + "You are using UVLoop with stream and shell_command. This is known to cause issues. Please uninstall uvloop." + ) + if options is None: options = {} diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 426eb568f93..81335783e1a 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -114,7 +114,7 @@ class StreamOutput: @callback def put(self, segment: Segment) -> None: """Store output.""" - # Start idle timeout when we start recieving data + # Start idle timeout when we start receiving data if self._unsub is None: self._unsub = async_call_later( self._stream.hass, self.timeout, self._timeout diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 90b2f518b45..e87221304a3 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -63,7 +63,7 @@ def stream_worker(hass, stream, quit_event): sequence = 1 # Holds the generated silence that needs to be muxed into the output audio_packets = {} - # The presentation timestamp of the first video packet we recieve + # The presentation timestamp of the first video packet we receive first_pts = 0 # The decoder timestamp of the latest packet we processed last_dts = None @@ -130,7 +130,7 @@ def stream_worker(hass, stream, quit_event): # If we are attaching to a live stream that does not reset # timestamps for us, we need to do it ourselves by recording # the first presentation timestamp and subtracting it from - # subsequent packets we recieve. + # subsequent packets we receive. if (packet.pts * packet.time_base) > 1: first_pts = packet.pts packet.dts = 0 diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index a52380882db..348c2a8616b 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -20,6 +20,9 @@ from homeassistant.const import ( ) from homeassistant.components import group + +# mypy: allow-untyped-defs, no-check-untyped-defs + DOMAIN = "switch" SCAN_INTERVAL = timedelta(seconds=30) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 38c66854a0b..0b1094c0dd9 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,5 +1,6 @@ """Light support for switch entities.""" import logging +from typing import cast import voluptuous as vol @@ -18,6 +19,9 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.components.light import PLATFORM_SCHEMA, Light + +# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "Light Switch" @@ -35,7 +39,7 @@ async def async_setup_platform( ) -> None: """Initialize Light Switch platform.""" async_add_entities( - [LightSwitch(config.get(CONF_NAME), config[CONF_ENTITY_ID])], True + [LightSwitch(cast(str, config.get(CONF_NAME)), config[CONF_ENTITY_ID])], True ) diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 1687f8eee88..9f4347d61d2 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -116,7 +116,7 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: async def async_switch_platform_discovered( platform: str, discovery_info: Optional[Dict] ) -> None: - """Use for registering services after switch platform is discoverd.""" + """Use for registering services after switch platform is discovered.""" if platform != DOMAIN: return diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 8fc3b2476cb..a2a45826d9d 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Syncthru", "documentation": "https://www.home-assistant.io/components/syncthru", "requirements": [ - "pysyncthru==0.4.2" + "pysyncthru==0.4.3" ], "dependencies": [], "codeowners": ["@nielstron"] diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index beae6ce55c0..c9bd486053e 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -116,7 +116,7 @@ class LogEntry: return frozenset([self.message, self.root_cause]) def to_dict(self): - """Convert object into dict to maintain backward compatability.""" + """Convert object into dict to maintain backward compatibility.""" return vars(self) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 207440a6646..446a36ec350 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -12,6 +12,9 @@ from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) CONF_ARG = "arg" diff --git a/homeassistant/components/sytadin/manifest.json b/homeassistant/components/sytadin/manifest.json index 0efc84fc552..c1453d88d81 100644 --- a/homeassistant/components/sytadin/manifest.json +++ b/homeassistant/components/sytadin/manifest.json @@ -3,7 +3,7 @@ "name": "Sytadin", "documentation": "https://www.home-assistant.io/components/sytadin", "requirements": [ - "beautifulsoup4==4.7.1" + "beautifulsoup4==4.8.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/sytadin/sensor.py b/homeassistant/components/sytadin/sensor.py index 9cbb8ee939a..4296f2d5b05 100644 --- a/homeassistant/components/sytadin/sensor.py +++ b/homeassistant/components/sytadin/sensor.py @@ -50,6 +50,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up of the Sytadin Traffic sensor platform.""" + _LOGGER.warning( + "The sytadin integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + name = config.get(CONF_NAME) sytadin = SytadinData(URL) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 519c1ced416..ad66a594a86 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -125,12 +125,22 @@ class TadoDataStore: self.tado.resetZoneOverlay(zone_id) self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg - def set_zone_overlay(self, zone_id, mode, temperature=None, duration=None): + def set_zone_overlay( + self, + zone_id, + overlay_mode, + temperature=None, + duration=None, + device_type="HEATING", + mode=None, + ): """Wrap for setZoneOverlay(..).""" - self.tado.setZoneOverlay(zone_id, mode, temperature, duration) + self.tado.setZoneOverlay( + zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + ) self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg - def set_zone_off(self, zone_id, mode): + def set_zone_off(self, zone_id, overlay_mode, device_type="HEATING"): """Set a zone to off.""" - self.tado.setZoneOverlay(zone_id, mode, None, None, "HEATING", "OFF") + self.tado.setZoneOverlay(zone_id, overlay_mode, None, None, device_type, "OFF") self.update(no_throttle=True) # pylint: disable=unexpected-keyword-arg diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 1d310ef7c16..15e01db4082 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -1,18 +1,24 @@ """Support for Tado to create a climate device for each zone.""" import logging +from typing import Optional, List from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_OFF, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, FAN_HIGH, FAN_LOW, FAN_MIDDLE, FAN_OFF, HVAC_MODE_AUTO, HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, + PRESET_HOME, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -40,18 +46,34 @@ CONST_MODE_FAN_LOW = "LOW" FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} -HVAC_MAP_TADO = { +HVAC_MAP_TADO_HEAT = { "MANUAL": HVAC_MODE_HEAT, "TIMER": HVAC_MODE_AUTO, "TADO_MODE": HVAC_MODE_AUTO, "SMART_SCHEDULE": HVAC_MODE_AUTO, "OFF": HVAC_MODE_OFF, } +HVAC_MAP_TADO_COOL = { + "MANUAL": HVAC_MODE_COOL, + "TIMER": HVAC_MODE_AUTO, + "TADO_MODE": HVAC_MODE_AUTO, + "SMART_SCHEDULE": HVAC_MODE_AUTO, + "OFF": HVAC_MODE_OFF, +} +HVAC_MAP_TADO_HEAT_COOL = { + "MANUAL": HVAC_MODE_HEAT_COOL, + "TIMER": HVAC_MODE_AUTO, + "TADO_MODE": HVAC_MODE_AUTO, + "SMART_SCHEDULE": HVAC_MODE_AUTO, + "OFF": HVAC_MODE_OFF, +} SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF] -SUPPORT_FAN = [FAN_HIGH, FAN_MIDDLE, FAN_HIGH, FAN_OFF] -SUPPORT_PRESET = [PRESET_AWAY] +SUPPORT_HVAC_HEAT = [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF] +SUPPORT_HVAC_COOL = [HVAC_MODE_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF] +SUPPORT_HVAC_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF] +SUPPORT_FAN = [FAN_HIGH, FAN_MIDDLE, FAN_LOW, FAN_OFF] +SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] def setup_platform(hass, config, add_entities, discovery_info=None): @@ -80,10 +102,18 @@ def create_climate_device(tado, hass, zone, name, zone_id): capabilities = tado.get_capabilities(zone_id) unit = TEMP_CELSIUS - ac_mode = capabilities["type"] == "AIR_CONDITIONING" + ac_device = capabilities["type"] == "AIR_CONDITIONING" + ac_support_heat = False - if ac_mode: - temperatures = capabilities["HEAT"]["temperatures"] + if ac_device: + # 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 + if "HEAT" in capabilities: + temperatures = capabilities["HEAT"]["temperatures"] + ac_support_heat = True + else: + temperatures = capabilities["COOL"]["temperatures"] elif "temperatures" in capabilities: temperatures = capabilities["temperatures"] else: @@ -103,7 +133,8 @@ def create_climate_device(tado, hass, zone, name, zone_id): hass.config.units.temperature(min_temp, unit), hass.config.units.temperature(max_temp, unit), step, - ac_mode, + ac_device, + ac_support_heat, ) tado.add_sensor( @@ -114,7 +145,7 @@ def create_climate_device(tado, hass, zone, name, zone_id): class TadoClimate(ClimateDevice): - """Representation of a tado climate device.""" + """Representation of a Tado climate device.""" def __init__( self, @@ -125,7 +156,8 @@ class TadoClimate(ClimateDevice): min_temp, max_temp, step, - ac_mode, + ac_device, + ac_support_heat, tolerance=0.3, ): """Initialize of Tado climate device.""" @@ -135,7 +167,9 @@ class TadoClimate(ClimateDevice): self.zone_name = zone_name self.zone_id = zone_id - self.ac_mode = ac_mode + self._ac_device = ac_device + self._ac_support_heat = ac_support_heat + self._cooling = False self._active = False self._device_is_active = False @@ -149,7 +183,6 @@ class TadoClimate(ClimateDevice): self._step = step self._target_temp = None self._tolerance = tolerance - self._cooling = False self._current_fan = CONST_MODE_OFF self._current_operation = CONST_MODE_SMART_SCHEDULE @@ -170,6 +203,10 @@ class TadoClimate(ClimateDevice): """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.""" @@ -181,7 +218,11 @@ class TadoClimate(ClimateDevice): Need to be one of HVAC_MODE_*. """ - return HVAC_MAP_TADO.get(self._current_operation) + if self._ac_device and self._ac_support_heat: + return HVAC_MAP_TADO_HEAT_COOL.get(self._current_operation) + if self._ac_device and not self._ac_support_heat: + return HVAC_MAP_TADO_COOL.get(self._current_operation) + return HVAC_MAP_TADO_HEAT.get(self._current_operation) @property def hvac_modes(self): @@ -189,7 +230,11 @@ class TadoClimate(ClimateDevice): Need to be a subset of HVAC_MODES. """ - return SUPPORT_HVAC + if self._ac_device and self._ac_support_heat: + return SUPPORT_HVAC_HEAT_COOL + if self._ac_device and not self._ac_support_heat: + return SUPPORT_HVAC_COOL + return SUPPORT_HVAC_HEAT @property def hvac_action(self): @@ -197,36 +242,58 @@ class TadoClimate(ClimateDevice): Need to be one of CURRENT_HVAC_*. """ - if self._cooling: - return CURRENT_HVAC_COOL - return CURRENT_HVAC_HEAT + 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._active: + return CURRENT_HVAC_COOL + return CURRENT_HVAC_IDLE + if self._active: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE @property def fan_mode(self): """Return the fan setting.""" - if self.ac_mode: + if self._ac_device: return FAN_MAP_TADO.get(self._current_fan) return None @property def fan_modes(self): """List of available fan modes.""" - if self.ac_mode: + if self._ac_device: return SUPPORT_FAN return None + def set_fan_mode(self, fan_mode: str): + """Turn fan on/off.""" + pass + @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" if self._is_away: return PRESET_AWAY - return None + return PRESET_HOME @property def preset_modes(self): """Return a list of available preset modes.""" return SUPPORT_PRESET + def set_preset_mode(self, preset_mode): + """Set new preset mode.""" + pass + @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" @@ -242,13 +309,23 @@ 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 = CONST_OVERLAY_MANUAL self._overlay_mode = None self._target_temp = temperature self._control_heating() @@ -263,15 +340,17 @@ class TadoClimate(ClimateDevice): mode = CONST_MODE_SMART_SCHEDULE elif hvac_mode == HVAC_MODE_HEAT: mode = CONST_OVERLAY_MANUAL + elif hvac_mode == HVAC_MODE_COOL: + mode = CONST_OVERLAY_MANUAL + elif hvac_mode == HVAC_MODE_HEAT_COOL: + mode = CONST_OVERLAY_MANUAL self._current_operation = mode self._overlay_mode = None + if self._target_temp is None and self._ac_device: + self._target_temp = 27 self._control_heating() - def set_preset_mode(self, preset_mode): - """Set new preset mode.""" - pass - @property def min_temp(self): """Return the minimum temperature.""" @@ -333,6 +412,22 @@ class TadoClimate(ClimateDevice): else: self._device_is_active = True + active = False + if "activityDataPoints" in data: + activity_data = data["activityDataPoints"] + if self._ac_device: + if "acPower" in activity_data and activity_data["acPower"] is not None: + if not activity_data["acPower"]["value"] == "OFF": + active = True + else: + if ( + "heatingPower" in activity_data + and activity_data["heatingPower"] is not None + ): + if float(activity_data["heatingPower"]["percentage"]) > 0.0: + active = True + self._active = active + overlay = False overlay_data = None termination = CONST_MODE_SMART_SCHEDULE @@ -345,6 +440,8 @@ class TadoClimate(ClimateDevice): if overlay: termination = overlay_data["termination"]["type"] + setting = False + setting_data = None if "setting" in overlay_data: setting_data = overlay_data["setting"] @@ -368,34 +465,110 @@ class TadoClimate(ClimateDevice): def _control_heating(self): """Send new target temperature to mytado.""" - if not self._active and None not in (self._cur_temp, self._target_temp): - self._active = True + if None not in (self._cur_temp, self._target_temp): _LOGGER.info( - "Obtained current and target temperature. " "Tado thermostat active" + "Obtained current (%d) and target temperature (%d). " + "Tado thermostat active", + self._cur_temp, + self._target_temp, ) if self._current_operation == CONST_MODE_SMART_SCHEDULE: _LOGGER.info( - "Switching mytado.com to SCHEDULE (default) " "for zone %s", + "Switching mytado.com to SCHEDULE (default) for zone %s (%d)", self.zone_name, + self.zone_id, ) self._store.reset_zone_overlay(self.zone_id) self._overlay_mode = self._current_operation return if self._current_operation == CONST_MODE_OFF: - _LOGGER.info("Switching mytado.com to OFF for zone %s", self.zone_name) - self._store.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL) + 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" + ) + 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") self._overlay_mode = self._current_operation return - _LOGGER.info( - "Switching mytado.com to %s mode for zone %s", - self._current_operation, - self.zone_name, - ) - self._store.set_zone_overlay( - self.zone_id, self._current_operation, self._target_temp - ) + 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", + ) + 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", + ) 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/sensor.py b/homeassistant/components/tado/sensor.py index edabdaebfdc..5cfdbd1f30c 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -12,7 +12,7 @@ ATTR_DATA_ID = "data_id" ATTR_DEVICE = "device" ATTR_ZONE = "zone" -CLIMATE_SENSOR_TYPES = [ +CLIMATE_HEAT_SENSOR_TYPES = [ "temperature", "humidity", "power", @@ -22,6 +22,16 @@ CLIMATE_SENSOR_TYPES = [ "overlay", ] +CLIMATE_COOL_SENSOR_TYPES = [ + "temperature", + "humidity", + "power", + "link", + "ac", + "tado mode", + "overlay", +] + HOT_WATER_SENSOR_TYPES = ["power", "link", "tado mode", "overlay"] @@ -38,7 +48,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_items = [] for zone in zones: if zone["type"] == "HEATING": - for variable in CLIMATE_SENSOR_TYPES: + for variable in CLIMATE_HEAT_SENSOR_TYPES: sensor_items.append( create_zone_sensor(tado, zone, zone["name"], zone["id"], variable) ) @@ -47,6 +57,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): 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( @@ -138,6 +153,8 @@ class TadoSensor(Entity): return "%" if self.zone_variable == "heating": return "%" + if self.zone_variable == "ac": + return "" @property def icon(self): @@ -198,10 +215,25 @@ class TadoSensor(Entity): elif self.zone_variable == "heating": if "activityDataPoints" in data: activity_data = data["activityDataPoints"] - self._state = float(activity_data["heatingPower"]["percentage"]) - self._state_attributes = { - "time": activity_data["heatingPower"]["timestamp"] - } + + if ( + "heatingPower" in activity_data + and activity_data["heatingPower"] is not None + ): + self._state = float(activity_data["heatingPower"]["percentage"]) + self._state_attributes = { + "time": activity_data["heatingPower"]["timestamp"] + } + + elif self.zone_variable == "ac": + if "activityDataPoints" in data: + activity_data = data["activityDataPoints"] + + if "acPower" in activity_data and activity_data["acPower"] is not None: + self._state = activity_data["acPower"]["value"] + self._state_attributes = { + "time": activity_data["acPower"]["timestamp"] + } elif self.zone_variable == "tado bridge status": if "connectionState" in data: diff --git a/homeassistant/components/tellduslive/.translations/bg.json b/homeassistant/components/tellduslive/.translations/bg.json index 3e493a3973a..46ae4eba463 100644 --- a/homeassistant/components/tellduslive/.translations/bg.json +++ b/homeassistant/components/tellduslive/.translations/bg.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_setup": "TelldusLive \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430 \u0433\u0440\u0435\u0448\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f.", "authorize_url_timeout": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f \u0432 \u0441\u0440\u043e\u043a.", diff --git a/homeassistant/components/tellduslive/.translations/ca.json b/homeassistant/components/tellduslive/.translations/ca.json index 437b9b460d2..fafa8798401 100644 --- a/homeassistant/components/tellduslive/.translations/ca.json +++ b/homeassistant/components/tellduslive/.translations/ca.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ja est\u00e0 configurat", "already_setup": "TelldusLive ja est\u00e0 configurat", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", "authorize_url_timeout": "S'ha acabat el temps d'espera mentre \u00e9s generava l'URL d'autoritzaci\u00f3.", diff --git a/homeassistant/components/tellduslive/.translations/da.json b/homeassistant/components/tellduslive/.translations/da.json index 717e3ec5ac9..895570c3698 100644 --- a/homeassistant/components/tellduslive/.translations/da.json +++ b/homeassistant/components/tellduslive/.translations/da.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive er allerede konfigureret", "already_setup": "TelldusLive er allerede konfigureret", "authorize_url_fail": "Ukendt fejl ved generering af en autoriseret url.", "authorize_url_timeout": "Timeout ved generering af autoriseret url.", diff --git a/homeassistant/components/tellduslive/.translations/de.json b/homeassistant/components/tellduslive/.translations/de.json index 6c094ed6a8c..18c3e88666e 100644 --- a/homeassistant/components/tellduslive/.translations/de.json +++ b/homeassistant/components/tellduslive/.translations/de.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ist bereits konfiguriert", "already_setup": "TelldusLive ist bereits konfiguriert", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", diff --git a/homeassistant/components/tellduslive/.translations/en.json b/homeassistant/components/tellduslive/.translations/en.json index c2b00561858..4ed9ef597f4 100644 --- a/homeassistant/components/tellduslive/.translations/en.json +++ b/homeassistant/components/tellduslive/.translations/en.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive is already configured", "already_setup": "TelldusLive is already configured", "authorize_url_fail": "Unknown error generating an authorize url.", "authorize_url_timeout": "Timeout generating authorize url.", diff --git a/homeassistant/components/tellduslive/.translations/es-419.json b/homeassistant/components/tellduslive/.translations/es-419.json index bf74d104835..503530e728a 100644 --- a/homeassistant/components/tellduslive/.translations/es-419.json +++ b/homeassistant/components/tellduslive/.translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ya est\u00e1 configurado", "already_setup": "TelldusLive ya est\u00e1 configurado", "unknown": "Se produjo un error desconocido" }, diff --git a/homeassistant/components/tellduslive/.translations/es.json b/homeassistant/components/tellduslive/.translations/es.json index 6b3cea7f484..b0313a1eee3 100644 --- a/homeassistant/components/tellduslive/.translations/es.json +++ b/homeassistant/components/tellduslive/.translations/es.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ya est\u00e1 configurado", "already_setup": "TelldusLive ya est\u00e1 configurado", "authorize_url_fail": "Error desconocido generando la url de autorizaci\u00f3n", "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n", diff --git a/homeassistant/components/tellduslive/.translations/fr.json b/homeassistant/components/tellduslive/.translations/fr.json index a7ddd4c6fa6..70f4ef6f1d9 100644 --- a/homeassistant/components/tellduslive/.translations/fr.json +++ b/homeassistant/components/tellduslive/.translations/fr.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", "already_setup": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", diff --git a/homeassistant/components/tellduslive/.translations/hu.json b/homeassistant/components/tellduslive/.translations/hu.json index cd219be04e1..8519f02ef5b 100644 --- a/homeassistant/components/tellduslive/.translations/hu.json +++ b/homeassistant/components/tellduslive/.translations/hu.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "A TelldusLive-ot m\u00e1r be\u00e1ll\u00edtottuk.", "already_setup": "A TelldusLive m\u00e1r be van \u00e1ll\u00edtva", "authorize_url_fail": "Ismeretlen hiba t\u00f6rt\u00e9nt a hiteles\u00edt\u00e9si link gener\u00e1l\u00e1sa sor\u00e1n.", "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", diff --git a/homeassistant/components/tellduslive/.translations/it.json b/homeassistant/components/tellduslive/.translations/it.json index 90f13184a67..3baa307de51 100644 --- a/homeassistant/components/tellduslive/.translations/it.json +++ b/homeassistant/components/tellduslive/.translations/it.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive \u00e8 gi\u00e0 configurato", "already_setup": "TelldusLive \u00e8 gi\u00e0 configurato", "authorize_url_fail": "Errore sconosciuto nel generare l'url di autorizzazione", "authorize_url_timeout": "Tempo scaduto nel generare l'url di autorizzazione", diff --git a/homeassistant/components/tellduslive/.translations/ko.json b/homeassistant/components/tellduslive/.translations/ko.json index 6b04e867861..10e289f2520 100644 --- a/homeassistant/components/tellduslive/.translations/ko.json +++ b/homeassistant/components/tellduslive/.translations/ko.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_setup": "TelldusLive \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/tellduslive/.translations/lb.json b/homeassistant/components/tellduslive/.translations/lb.json index 4584635066c..a01436f9ba8 100644 --- a/homeassistant/components/tellduslive/.translations/lb.json +++ b/homeassistant/components/tellduslive/.translations/lb.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive ass scho konfigur\u00e9iert", "already_setup": "TelldusLive ass scho konfigur\u00e9iert", "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", diff --git a/homeassistant/components/tellduslive/.translations/nl.json b/homeassistant/components/tellduslive/.translations/nl.json index a1029d991fe..fac9475f6f3 100644 --- a/homeassistant/components/tellduslive/.translations/nl.json +++ b/homeassistant/components/tellduslive/.translations/nl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive is al geconfigureerd", "already_setup": "TelldusLive is al geconfigureerd", "authorize_url_fail": "Onbekende fout bij het genereren van een autorisatie url.", "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index 2c6439b364f..d311b3b0d38 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive er allerede konfigurert", "already_setup": "TelldusLive er allerede konfigurert", "authorize_url_fail": "Ukjent feil ved generering av autoriseringsadresse.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", diff --git a/homeassistant/components/tellduslive/.translations/pl.json b/homeassistant/components/tellduslive/.translations/pl.json index 9d791e0e786..06391b24b99 100644 --- a/homeassistant/components/tellduslive/.translations/pl.json +++ b/homeassistant/components/tellduslive/.translations/pl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive jest ju\u017c skonfigurowany", "already_setup": "TelldusLive jest ju\u017c skonfigurowany", "authorize_url_fail": "Nieznany b\u0142\u0105d podczas generowania url autoryzacji.", "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", diff --git a/homeassistant/components/tellduslive/.translations/pt-BR.json b/homeassistant/components/tellduslive/.translations/pt-BR.json index 4a3983facf6..2183b9068b8 100644 --- a/homeassistant/components/tellduslive/.translations/pt-BR.json +++ b/homeassistant/components/tellduslive/.translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive j\u00e1 est\u00e1 configurado", "already_setup": "TelldusLive j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Tempo limite de gera\u00e7\u00e3o de url de autoriza\u00e7\u00e3o.", diff --git a/homeassistant/components/tellduslive/.translations/pt.json b/homeassistant/components/tellduslive/.translations/pt.json index a13f71f7505..9f8134ed07d 100644 --- a/homeassistant/components/tellduslive/.translations/pt.json +++ b/homeassistant/components/tellduslive/.translations/pt.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive j\u00e1 est\u00e1 configurado", "already_setup": "TelldusLive j\u00e1 est\u00e1 configurado", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 3b34e048b11..afaaf4edbf5 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", diff --git a/homeassistant/components/tellduslive/.translations/sl.json b/homeassistant/components/tellduslive/.translations/sl.json index 16e6ddcb5f4..7e0a8d28b9a 100644 --- a/homeassistant/components/tellduslive/.translations/sl.json +++ b/homeassistant/components/tellduslive/.translations/sl.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive je \u017ee konfiguriran", "already_setup": "TelldusLive je \u017ee konfiguriran", "authorize_url_fail": "Neznana napaka pri generiranju potrditvenega URL-ja.", "authorize_url_timeout": "\u010casovna omejitev za generiranje URL-ja je potekla.", diff --git a/homeassistant/components/tellduslive/.translations/sv.json b/homeassistant/components/tellduslive/.translations/sv.json index 5636e137948..809132e01b0 100644 --- a/homeassistant/components/tellduslive/.translations/sv.json +++ b/homeassistant/components/tellduslive/.translations/sv.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "Telldus Live! \u00e4r redan konfigurerad", "already_setup": "Telldus Live! \u00e4r redan konfigurerad", "authorize_url_fail": "Ok\u00e4nt fel n\u00e4r genererar en url f\u00f6r att auktorisera.", "authorize_url_timeout": "Timeout n\u00e4r genererar auktorisera url.", diff --git a/homeassistant/components/tellduslive/.translations/zh-Hans.json b/homeassistant/components/tellduslive/.translations/zh-Hans.json index e447fcf92ef..657722f7f13 100644 --- a/homeassistant/components/tellduslive/.translations/zh-Hans.json +++ b/homeassistant/components/tellduslive/.translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "Tellduslive \u5df2\u914d\u7f6e\u5b8c\u6210", "already_setup": "TelldusLive \u5df2\u914d\u7f6e\u5b8c\u6210", "authorize_url_fail": "\u751f\u6210\u6388\u6743\u7f51\u5740\u65f6\u53d1\u751f\u672a\u77e5\u9519\u8bef\u3002", "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002", diff --git a/homeassistant/components/tellduslive/.translations/zh-Hant.json b/homeassistant/components/tellduslive/.translations/zh-Hant.json index c95e96b21c9..23093d247ee 100644 --- a/homeassistant/components/tellduslive/.translations/zh-Hant.json +++ b/homeassistant/components/tellduslive/.translations/zh-Hant.json @@ -1,7 +1,6 @@ { "config": { "abort": { - "all_configured": "TelldusLive \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_setup": "TelldusLive \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", diff --git a/homeassistant/components/template/binary_sensor.py b/homeassistant/components/template/binary_sensor.py index 8b354f4eeb2..e0fc8677200 100644 --- a/homeassistant/components/template/binary_sensor.py +++ b/homeassistant/components/template/binary_sensor.py @@ -1,5 +1,6 @@ """Support for exposing a templated binary sensor.""" import logging +from itertools import chain import voluptuous as vol @@ -30,12 +31,14 @@ _LOGGER = logging.getLogger(__name__) CONF_DELAY_ON = "delay_on" CONF_DELAY_OFF = "delay_off" +CONF_ATTRIBUTE_TEMPLATES = "attribute_templates" SENSOR_SCHEMA = vol.Schema( { vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_ICON_TEMPLATE): cv.template, vol.Optional(CONF_ENTITY_PICTURE_TEMPLATE): cv.template, + vol.Optional(CONF_ATTRIBUTE_TEMPLATES): vol.Schema({cv.string: cv.template}), vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, @@ -59,14 +62,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_picture_template = device_config.get(CONF_ENTITY_PICTURE_TEMPLATE) entity_ids = set() manual_entity_ids = device_config.get(ATTR_ENTITY_ID) + attribute_templates = device_config.get(CONF_ATTRIBUTE_TEMPLATES, {}) invalid_templates = [] - for tpl_name, template in ( - (CONF_VALUE_TEMPLATE, value_template), - (CONF_ICON_TEMPLATE, icon_template), - (CONF_ENTITY_PICTURE_TEMPLATE, entity_picture_template), - ): + templates = { + CONF_VALUE_TEMPLATE: value_template, + CONF_ICON_TEMPLATE: icon_template, + CONF_ENTITY_PICTURE_TEMPLATE: entity_picture_template, + } + + for tpl_name, template in chain(templates.items(), attribute_templates.items()): if template is None: continue template.hass = hass @@ -78,7 +84,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if template_entity_ids == MATCH_ALL: entity_ids = MATCH_ALL # Cut off _template from name - invalid_templates.append(tpl_name[:-9]) + invalid_templates.append(tpl_name.replace("_template", "")) elif entity_ids != MATCH_ALL: entity_ids |= set(template_entity_ids) @@ -114,6 +120,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= entity_ids, delay_on, delay_off, + attribute_templates, ) ) if not sensors: @@ -139,6 +146,7 @@ class BinarySensorTemplate(BinarySensorDevice): entity_ids, delay_on, delay_off, + attribute_templates, ): """Initialize the Template binary sensor.""" self.hass = hass @@ -154,6 +162,8 @@ class BinarySensorTemplate(BinarySensorDevice): self._entities = entity_ids self._delay_on = delay_on self._delay_off = delay_off + self._attribute_templates = attribute_templates + self._attributes = {} async def async_added_to_hass(self): """Register callbacks.""" @@ -203,6 +213,11 @@ class BinarySensorTemplate(BinarySensorDevice): """Return the sensor class of the sensor.""" return self._device_class + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + @property def should_poll(self): """No polling needed.""" @@ -225,10 +240,21 @@ class BinarySensorTemplate(BinarySensorDevice): return _LOGGER.error("Could not render template %s: %s", self._name, ex) - for property_name, template in ( - ("_icon", self._icon_template), - ("_entity_picture", self._entity_picture_template), - ): + templates = { + "_icon": self._icon_template, + "_entity_picture": self._entity_picture_template, + } + + attrs = {} + if self._attribute_templates is not None: + for key, value in self._attribute_templates.items(): + try: + attrs[key] = value.async_render() + except TemplateError as err: + _LOGGER.error("Error rendering attribute %s: %s", key, err) + self._attributes = attrs + + for property_name, template in templates.items(): if template is None: continue diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index c43c38f55d5..f5bd981bad1 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -3,10 +3,11 @@ "name": "Tensorflow", "documentation": "https://www.home-assistant.io/components/tensorflow", "requirements": [ - "numpy==1.16.4", - "pillow==5.4.1", + "tensorflow==1.13.2", + "numpy==1.17.0", + "pillow==6.1.0", "protobuf==3.6.1" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 96ad1211812..e8ed5b06d27 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -126,14 +126,17 @@ class TileScanner: for tile in tiles: await self._async_see( dev_id="tile_{0}".format(slugify(tile["tile_uuid"])), - gps=(tile["tileState"]["latitude"], tile["tileState"]["longitude"]), + gps=( + tile["last_tile_state"]["latitude"], + tile["last_tile_state"]["longitude"], + ), attributes={ - ATTR_ALTITUDE: tile["tileState"]["altitude"], - ATTR_CONNECTION_STATE: tile["tileState"]["connection_state"], + ATTR_ALTITUDE: tile["last_tile_state"]["altitude"], + ATTR_CONNECTION_STATE: tile["last_tile_state"]["connection_state"], ATTR_IS_DEAD: tile["is_dead"], - ATTR_IS_LOST: tile["tileState"]["is_lost"], - ATTR_RING_STATE: tile["tileState"]["ring_state"], - ATTR_VOIP_STATE: tile["tileState"]["voip_state"], + ATTR_IS_LOST: tile["last_tile_state"]["is_lost"], + ATTR_RING_STATE: tile["last_tile_state"]["ring_state"], + ATTR_VOIP_STATE: tile["last_tile_state"]["voip_state"], ATTR_TILE_ID: tile["tile_uuid"], ATTR_TILE_NAME: tile["name"], }, diff --git a/homeassistant/components/tod/binary_sensor.py b/homeassistant/components/tod/binary_sensor.py index 2f8f4acb51f..6c5d8827a86 100644 --- a/homeassistant/components/tod/binary_sensor.py +++ b/homeassistant/components/tod/binary_sensor.py @@ -84,7 +84,7 @@ class TodSensor(BinarySensorDevice): @property def after(self): - """Return the timestamp for the begining of the period.""" + """Return the timestamp for the beginning of the period.""" return self._time_after @property diff --git a/homeassistant/components/tplink/.translations/es-419.json b/homeassistant/components/tplink/.translations/es-419.json index 1d9fb41fc8c..2832804113a 100644 --- a/homeassistant/components/tplink/.translations/es-419.json +++ b/homeassistant/components/tplink/.translations/es-419.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos TP-Link en la red.", + "single_instance_allowed": "Solo es necesaria una \u00fanica configuraci\u00f3n." + }, "step": { "confirm": { + "description": "\u00bfDesea configurar dispositivos inteligentes TP-Link?", "title": "TP-Link Smart Home" } }, diff --git a/homeassistant/components/traccar/.translations/en.json b/homeassistant/components/traccar/.translations/en.json new file mode 100644 index 00000000000..a8804835278 --- /dev/null +++ b/homeassistant/components/traccar/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Traccar.", + "one_instance_allowed": "Only a single instance is necessary." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." + }, + "step": { + "user": { + "description": "Are you sure you want to set up Traccar?", + "title": "Set up Traccar" + } + }, + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/__init__.py b/homeassistant/components/traccar/__init__.py index 03805760c53..8e3f90fb66f 100644 --- a/homeassistant/components/traccar/__init__.py +++ b/homeassistant/components/traccar/__init__.py @@ -1 +1,110 @@ -"""The traccar component.""" +"""Support for Traccar.""" +import logging + +import voluptuous as vol +from aiohttp import web + +import homeassistant.helpers.config_validation as cv +from homeassistant.const import HTTP_UNPROCESSABLE_ENTITY, HTTP_OK, CONF_WEBHOOK_ID +from homeassistant.helpers import config_entry_flow +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER +from .const import ( + ATTR_ACCURACY, + ATTR_ALTITUDE, + ATTR_BATTERY, + ATTR_BEARING, + ATTR_ID, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_SPEED, + ATTR_TIMESTAMP, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +TRACKER_UPDATE = "{}_tracker_update".format(DOMAIN) + + +DEFAULT_ACCURACY = 200 +DEFAULT_BATTERY = -1 + + +def _id(value: str) -> str: + """Coerce id by removing '-'.""" + return value.replace("-", "") + + +WEBHOOK_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ID): vol.All(cv.string, _id), + vol.Required(ATTR_LATITUDE): cv.latitude, + vol.Required(ATTR_LONGITUDE): cv.longitude, + vol.Optional(ATTR_ACCURACY, default=DEFAULT_ACCURACY): vol.Coerce(float), + vol.Optional(ATTR_ALTITUDE): vol.Coerce(float), + vol.Optional(ATTR_BATTERY, default=DEFAULT_BATTERY): vol.Coerce(float), + vol.Optional(ATTR_BEARING): vol.Coerce(float), + vol.Optional(ATTR_SPEED): vol.Coerce(float), + vol.Optional(ATTR_TIMESTAMP): vol.Coerce(int), + } +) + + +async def async_setup(hass, hass_config): + """Set up the Traccar component.""" + hass.data[DOMAIN] = {"devices": set(), "unsub_device_tracker": {}} + return True + + +async def handle_webhook(hass, webhook_id, request): + """Handle incoming webhook with Traccar request.""" + try: + data = WEBHOOK_SCHEMA(dict(request.query)) + except vol.MultipleInvalid as error: + return web.Response(text=error.error_message, status=HTTP_UNPROCESSABLE_ENTITY) + + attrs = { + ATTR_ALTITUDE: data.get(ATTR_ALTITUDE), + ATTR_BEARING: data.get(ATTR_BEARING), + ATTR_SPEED: data.get(ATTR_SPEED), + } + + device = data[ATTR_ID] + + async_dispatcher_send( + hass, + TRACKER_UPDATE, + device, + data[ATTR_LATITUDE], + data[ATTR_LONGITUDE], + data[ATTR_BATTERY], + data[ATTR_ACCURACY], + attrs, + ) + + return web.Response(text="Setting location for {}".format(device), status=HTTP_OK) + + +async def async_setup_entry(hass, entry): + """Configure based on config entry.""" + hass.components.webhook.async_register( + DOMAIN, "Traccar", entry.data[CONF_WEBHOOK_ID], handle_webhook + ) + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, DEVICE_TRACKER) + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) + hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() + await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) + return True + + +# pylint: disable=invalid-name +async_remove_entry = config_entry_flow.webhook_async_remove_entry diff --git a/homeassistant/components/traccar/config_flow.py b/homeassistant/components/traccar/config_flow.py new file mode 100644 index 00000000000..cc3f1f23727 --- /dev/null +++ b/homeassistant/components/traccar/config_flow.py @@ -0,0 +1,10 @@ +"""Config flow for Traccar.""" +from homeassistant.helpers import config_entry_flow +from .const import DOMAIN + + +config_entry_flow.register_webhook_flow( + DOMAIN, + "Traccar Webhook", + {"docs_url": "https://www.home-assistant.io/components/traccar/"}, +) diff --git a/homeassistant/components/traccar/const.py b/homeassistant/components/traccar/const.py index 58f7168cf43..56c0ab5ba1d 100644 --- a/homeassistant/components/traccar/const.py +++ b/homeassistant/components/traccar/const.py @@ -1,16 +1,26 @@ """Constants for Traccar integration.""" +DOMAIN = "traccar" + CONF_MAX_ACCURACY = "max_accuracy" CONF_SKIP_ACCURACY_ON = "skip_accuracy_filter_on" +ATTR_ACCURACY = "accuracy" ATTR_ADDRESS = "address" +ATTR_ALTITUDE = "altitude" +ATTR_BATTERY = "batt" +ATTR_BEARING = "bearing" ATTR_CATEGORY = "category" ATTR_GEOFENCE = "geofence" +ATTR_ID = "id" +ATTR_LATITUDE = "lat" +ATTR_LONGITUDE = "lon" ATTR_MOTION = "motion" ATTR_SPEED = "speed" +ATTR_STATUS = "status" +ATTR_TIMESTAMP = "timestamp" ATTR_TRACKER = "tracker" ATTR_TRACCAR_ID = "traccar_id" -ATTR_STATUS = "status" EVENT_DEVICE_MOVING = "device_moving" EVENT_COMMAND_RESULT = "command_result" diff --git a/homeassistant/components/traccar/device_tracker.py b/homeassistant/components/traccar/device_tracker.py index bc1eebf05da..c7fdda013b0 100644 --- a/homeassistant/components/traccar/device_tracker.py +++ b/homeassistant/components/traccar/device_tracker.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -16,19 +16,33 @@ from homeassistant.const import ( CONF_MONITORED_CONDITIONS, CONF_EVENT, ) +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.helpers import device_registry +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import HomeAssistantType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_track_time_interval from homeassistant.util import slugify + +from . import DOMAIN, TRACKER_UPDATE from .const import ( + ATTR_ACCURACY, ATTR_ADDRESS, + ATTR_ALTITUDE, + ATTR_BATTERY, + ATTR_BEARING, ATTR_CATEGORY, ATTR_GEOFENCE, + ATTR_LATITUDE, + ATTR_LONGITUDE, ATTR_MOTION, ATTR_SPEED, + ATTR_STATUS, ATTR_TRACKER, ATTR_TRACCAR_ID, - ATTR_STATUS, EVENT_DEVICE_MOVING, EVENT_COMMAND_RESULT, EVENT_DEVICE_FUEL_DROP, @@ -101,6 +115,45 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) +async def async_setup_entry(hass: HomeAssistantType, entry, async_add_entities): + """Configure a dispatcher connection based on a config entry.""" + + @callback + def _receive_data(device, latitude, longitude, battery, accuracy, attrs): + """Receive set location.""" + if device in hass.data[DOMAIN]["devices"]: + return + + hass.data[DOMAIN]["devices"].add(device) + + async_add_entities( + [TraccarEntity(device, latitude, longitude, battery, accuracy, attrs)] + ) + + hass.data[DOMAIN]["unsub_device_tracker"][ + entry.entry_id + ] = async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data) + + # Restore previously loaded devices + dev_reg = await device_registry.async_get_registry(hass) + dev_ids = { + identifier[1] + for device in dev_reg.devices.values() + for identifier in device.identifiers + if identifier[0] == DOMAIN + } + if not dev_ids: + return + + entities = [] + for dev_id in dev_ids: + hass.data[DOMAIN]["devices"].add(dev_id) + entity = TraccarEntity(dev_id, None, None, None, None, None) + entities.append(entity) + + async_add_entities(entities) + + async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Traccar scanner.""" from pytraccar.api import API @@ -273,3 +326,123 @@ class TraccarScanner: "attributes": event["attributes"], }, ) + + +class TraccarEntity(TrackerEntity, RestoreEntity): + """Represent a tracked device.""" + + def __init__(self, device, latitude, longitude, battery, accuracy, attributes): + """Set up Geofency entity.""" + self._accuracy = accuracy + self._attributes = attributes + self._name = device + self._battery = battery + self._latitude = latitude + self._longitude = longitude + self._unsub_dispatcher = None + self._unique_id = device + + @property + def battery_level(self): + """Return battery value of the device.""" + return self._battery + + @property + def device_state_attributes(self): + """Return device specific attributes.""" + return self._attributes + + @property + def latitude(self): + """Return latitude value of the device.""" + return self._latitude + + @property + def longitude(self): + """Return longitude value of the device.""" + return self._longitude + + @property + def location_accuracy(self): + """Return the gps accuracy of the device.""" + return self._accuracy + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def unique_id(self): + """Return the unique ID.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info.""" + return {"name": self._name, "identifiers": {(DOMAIN, self._unique_id)}} + + @property + def source_type(self): + """Return the source type, eg gps or router, of the device.""" + return SOURCE_TYPE_GPS + + async def async_added_to_hass(self): + """Register state update callback.""" + await super().async_added_to_hass() + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, TRACKER_UPDATE, self._async_receive_data + ) + + # don't restore if we got created with data + if self._latitude is not None or self._longitude is not None: + return + + state = await self.async_get_last_state() + if state is None: + self._latitude = None + self._longitude = None + self._accuracy = None + self._attributes = { + ATTR_ALTITUDE: None, + ATTR_BEARING: None, + ATTR_SPEED: None, + } + self._battery = None + return + + attr = state.attributes + self._latitude = attr.get(ATTR_LATITUDE) + self._longitude = attr.get(ATTR_LONGITUDE) + self._accuracy = attr.get(ATTR_ACCURACY) + self._attributes = { + ATTR_ALTITUDE: attr.get(ATTR_ALTITUDE), + ATTR_BEARING: attr.get(ATTR_BEARING), + ATTR_SPEED: attr.get(ATTR_SPEED), + } + self._battery = attr.get(ATTR_BATTERY) + + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + await super().async_will_remove_from_hass() + self._unsub_dispatcher() + + @callback + def _async_receive_data( + self, device, latitude, longitude, battery, accuracy, attributes + ): + """Mark the device as seen.""" + if device != self.name: + return + + self._latitude = latitude + self._longitude = longitude + self._battery = battery + self._accuracy = accuracy + self._attributes.update(attributes) + self.async_write_ha_state() diff --git a/homeassistant/components/traccar/manifest.json b/homeassistant/components/traccar/manifest.json index 15b78d0ec7b..7d3e2f22d65 100644 --- a/homeassistant/components/traccar/manifest.json +++ b/homeassistant/components/traccar/manifest.json @@ -1,13 +1,16 @@ { "domain": "traccar", "name": "Traccar", + "config_flow": true, "documentation": "https://www.home-assistant.io/components/traccar", "requirements": [ "pytraccar==0.9.0", "stringcase==1.2.0" ], - "dependencies": [], + "dependencies": [ + "webhook" + ], "codeowners": [ "@ludeeus" ] -} \ No newline at end of file +} diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json new file mode 100644 index 00000000000..19f4eb0da22 --- /dev/null +++ b/homeassistant/components/traccar/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Traccar", + "step": { + "user": { + "title": "Set up Traccar", + "description": "Are you sure you want to set up Traccar?" + } + }, + "abort": { + "one_instance_allowed": "Only a single instance is necessary.", + "not_internet_accessible": "Your Home Assistant instance needs to be accessible from the internet to receive messages from Traccar." + }, + "create_entry": { + "default": "To send events to Home Assistant, you will need to setup the webhook feature in Traccar.\n\nUse the following url: `{webhook_url}`\n\nSee [the documentation]({docs_url}) for further details." + } + } +} diff --git a/homeassistant/components/tradfri/.translations/es-419.json b/homeassistant/components/tradfri/.translations/es-419.json index 55016606e2d..4b3e1ed52d4 100644 --- a/homeassistant/components/tradfri/.translations/es-419.json +++ b/homeassistant/components/tradfri/.translations/es-419.json @@ -1,9 +1,11 @@ { "config": { "abort": { - "already_configured": "El Bridge ya est\u00e1 configurado" + "already_configured": "El Bridge ya est\u00e1 configurado", + "already_in_progress": "La configuraci\u00f3n del puente ya est\u00e1 en progreso." }, "error": { + "cannot_connect": "No se puede conectar a la puerta de enlace.", "invalid_key": "Error al registrarse con la clave proporcionada. Si esto sigue sucediendo, intente reiniciar el gateway.", "timeout": "Tiempo de espera para validar el c\u00f3digo." }, diff --git a/homeassistant/components/tradfri/.translations/fr.json b/homeassistant/components/tradfri/.translations/fr.json index 4fefee631c9..c1dc31028a8 100644 --- a/homeassistant/components/tradfri/.translations/fr.json +++ b/homeassistant/components/tradfri/.translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Le pont est d\u00e9j\u00e0 configur\u00e9." + "already_configured": "Le pont est d\u00e9j\u00e0 configur\u00e9.", + "already_in_progress": "La configuration du pont est d\u00e9j\u00e0 en cours." }, "error": { "cannot_connect": "Impossible de se connecter \u00e0 la passerelle.", diff --git a/homeassistant/components/tradfri/.translations/hr.json b/homeassistant/components/tradfri/.translations/hr.json new file mode 100644 index 00000000000..b9b9cc6c0eb --- /dev/null +++ b/homeassistant/components/tradfri/.translations/hr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_in_progress": "Konfiguracija premosnice je ve\u0107 u tijeku." + }, + "step": { + "auth": { + "data": { + "host": "Host" + } + } + }, + "title": "IKEA TR\u00c5DFRI" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/no.json b/homeassistant/components/tradfri/.translations/no.json index 1448757ca5a..490fbaed5aa 100644 --- a/homeassistant/components/tradfri/.translations/no.json +++ b/homeassistant/components/tradfri/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Bridge er allerede konfigurert", + "already_configured": "Bridge er allerede konfigurert.", "already_in_progress": "Brokonfigurasjon er allerede i gang." }, "error": { diff --git a/homeassistant/components/tradfri/.translations/pl.json b/homeassistant/components/tradfri/.translations/pl.json index a61a028f396..e3fcfc89c5b 100644 --- a/homeassistant/components/tradfri/.translations/pl.json +++ b/homeassistant/components/tradfri/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Mostek jest ju\u017c skonfigurowany", - "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku." + "already_in_progress": "Konfiguracja mostka jest ju\u017c w toku." }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z bram\u0105.", diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 4bf0c0d435e..b9c01c15d20 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -3,7 +3,7 @@ "name": "Trend", "documentation": "https://www.home-assistant.io/components/trend", "requirements": [ - "numpy==1.16.4" + "numpy==1.17.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 862c5e9cf9a..77d24fd7aab 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -8,6 +8,7 @@ import logging import mimetypes import os import re +from typing import Optional from aiohttp import web import voluptuous as vol @@ -25,8 +26,12 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) ATTR_CACHE = "cache" @@ -461,8 +466,8 @@ class SpeechManager: class Provider: """Represent a single TTS provider.""" - hass = None - name = None + hass: Optional[HomeAssistantType] = None + name: Optional[str] = None @property def default_language(self): diff --git a/homeassistant/components/tuya/manifest.json b/homeassistant/components/tuya/manifest.json index 57eb3f17584..8d47d8a0173 100644 --- a/homeassistant/components/tuya/manifest.json +++ b/homeassistant/components/tuya/manifest.json @@ -3,7 +3,7 @@ "name": "Tuya", "documentation": "https://www.home-assistant.io/components/tuya", "requirements": [ - "tuyaha==0.0.2" + "tuyaha==0.0.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/twentemilieu/.translations/en.json b/homeassistant/components/twentemilieu/.translations/en.json index deabeeeb835..ce969a4f464 100644 --- a/homeassistant/components/twentemilieu/.translations/en.json +++ b/homeassistant/components/twentemilieu/.translations/en.json @@ -1,23 +1,23 @@ { "config": { - "title": "Twente Milieu", - "step": { - "user": { - "title": "Twente Milieu", - "description": "Set up Twente Milieu providing waste collection information on your address.", - "data": { - "post_code": "Postal code", - "house_number": "House number", - "house_letter": "House letter/additional" - } - } + "abort": { + "address_exists": "Address already set up." }, "error": { "connection_error": "Failed to connect.", "invalid_address": "Address not found in Twente Milieu service area." }, - "abort": { - "address_already_set_up": "Address already set up." - } + "step": { + "user": { + "data": { + "house_letter": "House letter/additional", + "house_number": "House number", + "post_code": "Postal code" + }, + "description": "Set up Twente Milieu providing waste collection information on your address.", + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" } -} +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/strings.json b/homeassistant/components/twentemilieu/strings.json index b94b7146d2c..811ecdbfa6e 100644 --- a/homeassistant/components/twentemilieu/strings.json +++ b/homeassistant/components/twentemilieu/strings.json @@ -20,4 +20,4 @@ "address_exists": "Address already set up." } } -} +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/hr.json b/homeassistant/components/unifi/.translations/hr.json new file mode 100644 index 00000000000..94a064f34b4 --- /dev/null +++ b/homeassistant/components/unifi/.translations/hr.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "password": "Lozinka", + "port": "Port", + "username": "Korisni\u010dko ime" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 4ca6f68c301..da9bbb8e59e 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -11,9 +11,6 @@ from .const import ( CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, - CONF_DONT_TRACK_CLIENTS, - CONF_DONT_TRACK_DEVICES, - CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, @@ -23,6 +20,9 @@ from .const import ( from .controller import UniFiController CONF_CONTROLLERS = "controllers" +CONF_DONT_TRACK_CLIENTS = "dont_track_clients" +CONF_DONT_TRACK_DEVICES = "dont_track_devices" +CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" CONTROLLER_SCHEMA = vol.Schema( { @@ -34,9 +34,7 @@ CONTROLLER_SCHEMA = vol.Schema( vol.Optional(CONF_DONT_TRACK_CLIENTS): cv.boolean, vol.Optional(CONF_DONT_TRACK_DEVICES): cv.boolean, vol.Optional(CONF_DONT_TRACK_WIRED_CLIENTS): cv.boolean, - vol.Optional(CONF_DETECTION_TIME): vol.All( - cv.time_period, cv.positive_timedelta - ), + vol.Optional(CONF_DETECTION_TIME): cv.positive_int, vol.Optional(CONF_SSID_FILTER): vol.All(cv.ensure_list, [cv.string]), } ) diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e5a8965dff9..e1f0a91c774 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -2,6 +2,7 @@ import voluptuous as vol from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -10,7 +11,20 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from .const import CONF_CONTROLLER, CONF_SITE_ID, DOMAIN, LOGGER +from .const import ( + CONF_CONTROLLER, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + CONF_TRACK_WIRED_CLIENTS, + CONF_DETECTION_TIME, + CONF_SITE_ID, + DEFAULT_TRACK_CLIENTS, + DEFAULT_TRACK_DEVICES, + DEFAULT_TRACK_WIRED_CLIENTS, + DEFAULT_DETECTION_TIME, + DOMAIN, + LOGGER, +) from .controller import get_controller from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect @@ -26,6 +40,12 @@ class UnifiFlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return UnifiOptionsFlowHandler(config_entry) + def __init__(self): """Initialize the UniFi flow.""" self.config = None @@ -142,3 +162,52 @@ class UnifiFlowHandler(config_entries.ConfigFlow): self.desc = import_config[CONF_SITE_ID] return await self.async_step_user(user_input=config) + + +class UnifiOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Unifi options.""" + + def __init__(self, config_entry): + """Initialize UniFi options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the UniFi options.""" + return await self.async_step_device_tracker() + + async def async_step_device_tracker(self, user_input=None): + """Manage the device tracker options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="device_tracker", + data_schema=vol.Schema( + { + vol.Optional( + CONF_TRACK_CLIENTS, + default=self.config_entry.options.get( + CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS + ), + ): bool, + vol.Optional( + CONF_TRACK_WIRED_CLIENTS, + default=self.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ), + ): bool, + vol.Optional( + CONF_TRACK_DEVICES, + default=self.config_entry.options.get( + CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES + ), + ): bool, + vol.Optional( + CONF_DETECTION_TIME, + default=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ), + ): int, + } + ), + ) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index b4864421cb9..ffa9a28818b 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -13,9 +13,16 @@ UNIFI_CONFIG = "unifi_config" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" -CONF_DONT_TRACK_CLIENTS = "dont_track_clients" -CONF_DONT_TRACK_DEVICES = "dont_track_devices" -CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" +CONF_TRACK_CLIENTS = "track_clients" +CONF_TRACK_DEVICES = "track_devices" +CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" +DEFAULT_BLOCK_CLIENTS = [] +DEFAULT_TRACK_CLIENTS = True +DEFAULT_TRACK_DEVICES = True +DEFAULT_TRACK_WIRED_CLIENTS = True +DEFAULT_DETECTION_TIME = 300 +DEFAULT_SSID_FILTER = [] + ATTR_MANUFACTURER = "Ubiquiti Networks" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index cb82e6cf1c1..47c692b12b2 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,4 +1,6 @@ """UniFi Controller abstraction.""" +from datetime import timedelta + import asyncio import ssl import async_timeout @@ -15,8 +17,19 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( CONF_BLOCK_CLIENT, CONF_CONTROLLER, + CONF_DETECTION_TIME, + CONF_TRACK_CLIENTS, + CONF_TRACK_DEVICES, + CONF_TRACK_WIRED_CLIENTS, CONF_SITE_ID, + CONF_SSID_FILTER, CONTROLLER_ID, + DEFAULT_BLOCK_CLIENTS, + DEFAULT_TRACK_CLIENTS, + DEFAULT_TRACK_DEVICES, + DEFAULT_TRACK_WIRED_CLIENTS, + DEFAULT_DETECTION_TIME, + DEFAULT_SSID_FILTER, LOGGER, UNIFI_CONFIG, ) @@ -59,9 +72,40 @@ class UniFiController: return self._site_role @property - def block_clients(self): - """Return list of clients to block.""" - return self.unifi_config.get(CONF_BLOCK_CLIENT, []) + def option_block_clients(self): + """Config entry option with list of clients to control network access.""" + return self.config_entry.options.get(CONF_BLOCK_CLIENT, DEFAULT_BLOCK_CLIENTS) + + @property + def option_track_clients(self): + """Config entry option to not track clients.""" + return self.config_entry.options.get(CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS) + + @property + def option_track_devices(self): + """Config entry option to not track devices.""" + return self.config_entry.options.get(CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES) + + @property + def option_track_wired_clients(self): + """Config entry option to not track wired clients.""" + return self.config_entry.options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ) + + @property + def option_detection_time(self): + """Config entry option defining number of seconds from last seen to away.""" + return timedelta( + seconds=self.config_entry.options.get( + CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME + ) + ) + + @property + def option_ssid_filter(self): + """Config entry option listing what SSIDs are being used to track clients.""" + return self.config_entry.options.get(CONF_SSID_FILTER, DEFAULT_SSID_FILTER) @property def mac(self): @@ -96,7 +140,7 @@ class UniFiController: with async_timeout.timeout(10): await self.api.clients.update() await self.api.devices.update() - if self.block_clients: + if self.option_block_clients: await self.api.clients_all.update() except aiounifi.LoginRequired: @@ -155,6 +199,30 @@ class UniFiController: self.unifi_config = unifi_config break + options = dict(self.config_entry.options) + + if CONF_BLOCK_CLIENT in self.unifi_config: + options[CONF_BLOCK_CLIENT] = self.unifi_config[CONF_BLOCK_CLIENT] + + if CONF_TRACK_CLIENTS in self.unifi_config: + options[CONF_TRACK_CLIENTS] = self.unifi_config[CONF_TRACK_CLIENTS] + + if CONF_TRACK_DEVICES in self.unifi_config: + options[CONF_TRACK_DEVICES] = self.unifi_config[CONF_TRACK_DEVICES] + + if CONF_TRACK_WIRED_CLIENTS in self.unifi_config: + options[CONF_TRACK_WIRED_CLIENTS] = self.unifi_config[ + CONF_TRACK_WIRED_CLIENTS + ] + + if CONF_DETECTION_TIME in self.unifi_config: + options[CONF_DETECTION_TIME] = self.unifi_config[CONF_DETECTION_TIME] + + if CONF_SSID_FILTER in self.unifi_config: + options[CONF_SSID_FILTER] = self.unifi_config[CONF_SSID_FILTER] + + hass.config_entries.async_update_entry(self.config_entry, options=options) + for platform in ["device_tracker", "switch"]: hass.async_create_task( hass.config_entries.async_forward_entry_setup( diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 42a6f496a2a..c8024808e39 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -27,12 +27,7 @@ import homeassistant.util.dt as dt_util from .const import ( ATTR_MANUFACTURER, CONF_CONTROLLER, - CONF_DETECTION_TIME, - CONF_DONT_TRACK_CLIENTS, - CONF_DONT_TRACK_DEVICES, - CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, - CONF_SSID_FILTER, CONTROLLER_ID, DOMAIN as UNIFI_DOMAIN, ) @@ -41,6 +36,7 @@ LOGGER = logging.getLogger(__name__) DEVICE_ATTRIBUTES = [ "_is_guest_by_uap", + "ap_mac", "authorized", "essid", "hostname", @@ -150,11 +146,11 @@ def update_items(controller, async_add_entities, tracked): """Update tracked device state from the controller.""" new_tracked = [] - if not controller.unifi_config.get(CONF_DONT_TRACK_CLIENTS, False): + if controller.option_track_clients: for client_id in controller.api.clients: - if client_id in tracked: + if client_id in tracked and tracked[client_id].entity_id: LOGGER.debug( "Updating UniFi tracked client %s (%s)", tracked[client_id].entity_id, @@ -167,15 +163,12 @@ def update_items(controller, async_add_entities, tracked): if ( not client.is_wired - and CONF_SSID_FILTER in controller.unifi_config - and client.essid not in controller.unifi_config[CONF_SSID_FILTER] + and controller.option_ssid_filter + and client.essid not in controller.option_ssid_filter ): continue - if ( - controller.unifi_config.get(CONF_DONT_TRACK_WIRED_CLIENTS, False) - and client.is_wired - ): + if not controller.option_track_wired_clients and client.is_wired: continue tracked[client_id] = UniFiClientTracker(client, controller) @@ -186,11 +179,11 @@ def update_items(controller, async_add_entities, tracked): client.mac, ) - if not controller.unifi_config.get(CONF_DONT_TRACK_DEVICES, False): + if controller.option_track_devices: for device_id in controller.api.devices: - if device_id in tracked: + if device_id in tracked and tracked[device_id].entity_id: LOGGER.debug( "Updating UniFi tracked device %s (%s)", tracked[device_id].entity_id, @@ -228,14 +221,11 @@ class UniFiClientTracker(ScannerEntity): @property def is_connected(self): """Return true if the client is connected to the network.""" - detection_time = self.controller.unifi_config.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - if ( dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.client.last_seen)) - ) < detection_time: + ) < self.controller.option_detection_time: return True + return False @property @@ -290,15 +280,12 @@ class UniFiDeviceTracker(ScannerEntity): @property def is_connected(self): """Return true if the device is connected to the network.""" - detection_time = self.controller.unifi_config.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - if self.device.state == 1 and ( dt_util.utcnow() - dt_util.utc_from_timestamp(float(self.device.last_seen)) - < detection_time + < self.controller.option_detection_time ): return True + return False @property diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 938ac058d22..c484bfbf09f 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -22,5 +22,20 @@ "already_configured": "Controller site is already configured", "user_privilege": "User needs to be administrator" } + }, + "options": { + "step": { + "init": { + "data": {} + }, + "device_tracker": { + "data": { + "detection_time": "Time in seconds from last seen until considered away", + "track_clients": "Track network clients", + "track_devices": "Track network devices (Ubiquiti devices)", + "track_wired_clients": "Include wired network clients" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index 2b7965d1095..b7bb9b730ad 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -74,7 +74,7 @@ def update_items(controller, async_add_entities, switches, switches_off): devices = controller.api.devices # block client - for client_id in controller.block_clients: + for client_id in controller.option_block_clients: block_client_id = "block-{}".format(client_id) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 9efab69ab26..dd270a0bb75 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -12,22 +12,25 @@ import aiohttp import async_timeout import voluptuous as vol -from homeassistant.const import ATTR_FRIENDLY_NAME, __version__ as current_version +from homeassistant.const import __version__ as current_version from homeassistant.helpers import 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 import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) ATTR_RELEASE_NOTES = "release_notes" +ATTR_NEWEST_VERSION = "newest_version" CONF_REPORTING = "reporting" CONF_COMPONENT_REPORTING = "include_used_components" DOMAIN = "updater" -ENTITY_ID = "updater.updater" +DISPATCHER_REMOTE_UPDATE = "updater_remote_update" UPDATER_URL = "https://updater.home-assistant.io/" UPDATER_UUID_FILE = ".uuid" @@ -47,6 +50,16 @@ RESPONSE_SCHEMA = vol.Schema( ) +class Updater: + """Updater class for data exchange.""" + + def __init__(self, update_available: bool, newest_version: str, release_notes: str): + """Initialize attributes.""" + self.update_available = update_available + self.release_notes = release_notes + self.newest_version = newest_version + + def _create_uuid(hass, filename=UPDATER_UUID_FILE): """Create UUID and save it in a file.""" with open(hass.config.path(filename), "w") as fptr: @@ -73,6 +86,10 @@ async def async_setup(hass, config): # This component only makes sense in release versions _LOGGER.info("Running on 'dev', only analytics will be submitted") + hass.async_create_task( + discovery.async_load_platform(hass, "binary_sensor", DOMAIN, {}, config) + ) + config = config.get(DOMAIN, {}) if config.get(CONF_REPORTING): huuid = await hass.async_add_job(_load_uuid, hass) @@ -88,7 +105,7 @@ async def async_setup(hass, config): if result is None: return - newest, releasenotes = result + newest, release_notes = result # Skip on dev if newest is None or "dev" in current_version: @@ -99,18 +116,17 @@ async def async_setup(hass, config): newest = hass.components.hassio.get_homeassistant_version() # Validate version + update_available = False if StrictVersion(newest) > StrictVersion(current_version): - _LOGGER.info("The latest available version is %s", newest) - hass.states.async_set( - ENTITY_ID, - newest, - { - ATTR_FRIENDLY_NAME: "Update Available", - ATTR_RELEASE_NOTES: releasenotes, - }, - ) + _LOGGER.info("The latest available version of Home Assistant is %s", newest) + update_available = True elif StrictVersion(newest) == StrictVersion(current_version): _LOGGER.info("You are on the latest version (%s) of Home Assistant", newest) + elif StrictVersion(newest) < StrictVersion(current_version): + _LOGGER.debug("Local version is newer than the latest version (%s)", newest) + + updater = Updater(update_available, newest, release_notes) + async_dispatcher_send(hass, DISPATCHER_REMOTE_UPDATE, updater) # Update daily, start 1 hour after startup _dt = dt_util.utcnow() + timedelta(hours=1) @@ -151,7 +167,7 @@ async def get_newest_version(hass, huuid, include_components): info_object, ) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Could not contact Home Assistant Update to check " "for updates") + _LOGGER.error("Could not contact Home Assistant Update to check for updates") return None try: diff --git a/homeassistant/components/updater/binary_sensor.py b/homeassistant/components/updater/binary_sensor.py new file mode 100644 index 00000000000..cae3ae32e3c --- /dev/null +++ b/homeassistant/components/updater/binary_sensor.py @@ -0,0 +1,81 @@ +"""Support for Home Assistant Updater binary sensors.""" + +from homeassistant.core import callback +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from . import ATTR_NEWEST_VERSION, ATTR_RELEASE_NOTES, DISPATCHER_REMOTE_UPDATE, Updater + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the updater binary sensors.""" + async_add_entities([UpdaterBinary()]) + + +class UpdaterBinary(BinarySensorDevice): + """Representation of an updater binary sensor.""" + + def __init__(self): + """Initialize the binary sensor.""" + self._update_available = None + self._release_notes = None + self._newest_version = None + self._unsub_dispatcher = None + + @property + def name(self) -> str: + """Return the name of the binary sensor, if any.""" + return "Updater" + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return "updater" + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return self._update_available + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._update_available is not None + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state.""" + return False + + @property + def device_state_attributes(self) -> dict: + """Return the optional state attributes.""" + data = super().device_state_attributes + if data is None: + data = {} + if self._release_notes: + data[ATTR_RELEASE_NOTES] = self._release_notes + if self._newest_version: + data[ATTR_NEWEST_VERSION] = self._newest_version + return data + + async def async_added_to_hass(self): + """Register update dispatcher.""" + + @callback + def async_state_update(updater: Updater): + """Update callback.""" + self._newest_version = updater.newest_version + self._release_notes = updater.release_notes + self._update_available = updater.update_available + self.async_schedule_update_ha_state() + + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, DISPATCHER_REMOTE_UPDATE, async_state_update + ) + + async def async_will_remove_from_hass(self): + """Register update dispatcher.""" + if self._unsub_dispatcher is not None: + self._unsub_dispatcher() + self._unsub_dispatcher = None diff --git a/homeassistant/components/upnp/.translations/hr.json b/homeassistant/components/upnp/.translations/hr.json new file mode 100644 index 00000000000..941f72f2e7d --- /dev/null +++ b/homeassistant/components/upnp/.translations/hr.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "few": "Nekoliko", + "one": "Jedan", + "other": "Ostalo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index f4de9ad4c0d..6c5b2fb2bb2 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -1,15 +1,30 @@ { - "config": { - "title": "UPnP/IGD", - "step": { - "confirm": { + "config": { "title": "UPnP/IGD", - "description": "Do you want to set up UPnP/IGD?" - } - }, - "abort": { - "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary.", - "no_devices_found": "No UPnP/IGD devices found on the network." + "step": { + "init": { + "title": "UPnP/IGD" + }, + "confirm": { + "title": "UPnP/IGD", + "description": "Do you want to set up UPnP/IGD?" + }, + "user": { + "title": "Configuration options for the UPnP/IGD", + "data": { + "enable_port_mapping": "Enable port mapping for Home Assistant", + "enable_sensors": "Add traffic sensors", + "igd": "UPnP/IGD" + } + } + }, + "abort": { + "already_configured": "UPnP/IGD is already configured", + "incomplete_device": "Ignoring incomplete UPnP device", + "no_devices_discovered": "No UPnP/IGDs discovered", + "no_devices_found": "No UPnP/IGD devices found on the network.", + "no_sensors_or_port_mapping": "Enable at least sensors or port mapping", + "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary." + } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/ups/sensor.py b/homeassistant/components/ups/sensor.py index 0fff07d442f..cfe35a9a63f 100644 --- a/homeassistant/components/ups/sensor.py +++ b/homeassistant/components/ups/sensor.py @@ -40,6 +40,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the UPS platform.""" import upsmychoice + _LOGGER.warning( + "The ups integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + try: cookie = hass.config.path(COOKIE) session = upsmychoice.get_session( diff --git a/homeassistant/components/usps/__init__.py b/homeassistant/components/usps/__init__.py index c33216f5b5b..61da78fa6d7 100644 --- a/homeassistant/components/usps/__init__.py +++ b/homeassistant/components/usps/__init__.py @@ -37,6 +37,12 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Use config values to set up a function enabling status retrieval.""" + _LOGGER.warning( + "The usps integration is deprecated and will be removed " + "in Home Assistant 0.100.0. For more information see ADR-0004:" + "https://github.com/home-assistant/architecture/blob/master/adr/0004-webscraping.md" + ) + conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index ea2f29ed225..9bc376916c6 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -27,6 +27,9 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity, Entity from homeassistant.helpers.icon import icon_for_battery_level + +# mypy: allow-untyped-defs, no-check-untyped-defs + _LOGGER = logging.getLogger(__name__) DOMAIN = "vacuum" @@ -321,6 +324,14 @@ class VacuumDevice(_BaseVacuum, ToggleEntity): """ await self.hass.async_add_executor_job(partial(self.start_pause, **kwargs)) + async def async_pause(self): + """Not supported.""" + pass + + async def async_start(self): + """Not supported.""" + pass + class StateVacuumDevice(_BaseVacuum): """Representation of a vacuum cleaner robot that supports states.""" @@ -375,3 +386,15 @@ class StateVacuumDevice(_BaseVacuum): This method must be run in the event loop. """ await self.hass.async_add_executor_job(self.pause) + + async def async_turn_on(self, **kwargs): + """Not supported.""" + pass + + async def async_turn_off(self, **kwargs): + """Not supported.""" + pass + + async def async_toggle(self, **kwargs): + """Not supported.""" + pass diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 4729e11ff62..b071b354d74 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -7,5 +7,5 @@ ], "config_flow": true, "dependencies": [], - "codeowners": ["@ceral2nd"] + "codeowners": ["@cereal2nd"] } diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json index 07ae7ab3d36..8365ca1a765 100644 --- a/homeassistant/components/vera/manifest.json +++ b/homeassistant/components/vera/manifest.json @@ -3,7 +3,7 @@ "name": "Vera", "documentation": "https://www.home-assistant.io/components/vera", "requirements": [ - "pyvera==0.3.3" + "pyvera==0.3.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/vesync/.translations/en.json b/homeassistant/components/vesync/.translations/en.json index ec0bc728cdb..cd8b3e59cbf 100644 --- a/homeassistant/components/vesync/.translations/en.json +++ b/homeassistant/components/vesync/.translations/en.json @@ -1,20 +1,20 @@ { "config": { - "title": "VeSync", - "step": { - "user": { - "title": "Enter Username and Password", - "data": { - "username": "Email Address", - "password": "Password" - } - } + "abort": { + "already_setup": "Only one Vesync instance is allowed" }, "error": { "invalid_login": "Invalid username or password" }, - "abort": { - "already_setup": "Only one Vesync instance is allow" - } + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "title": "Enter Username and Password" + } + }, + "title": "VeSync" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/strings.json b/homeassistant/components/vesync/strings.json index f28d7d0d0c2..2d808e85bea 100644 --- a/homeassistant/components/vesync/strings.json +++ b/homeassistant/components/vesync/strings.json @@ -1,20 +1,20 @@ { - "config": { - "title": "VeSync", - "step": { - "user": { - "title": "Enter Username and Password", - "data": { - "username": "Email Address", - "password": "Password" + "config": { + "title": "VeSync", + "step": { + "user": { + "title": "Enter Username and Password", + "data": { + "username": "Email Address", + "password": "Password" + } + } + }, + "error": { + "invalid_login": "Invalid username or password" + }, + "abort": { + "already_setup": "Only one Vesync instance is allowed" } - } - }, - "error": { - "invalid_login": "Invalid username or password" - }, - "abort": { - "already_setup": "Only one Vesync instance is allowed" } - } } \ No newline at end of file diff --git a/homeassistant/components/vesync/switch.py b/homeassistant/components/vesync/switch.py index fbbf7f4faad..5ca76a77254 100644 --- a/homeassistant/components/vesync/switch.py +++ b/homeassistant/components/vesync/switch.py @@ -46,7 +46,7 @@ def _async_setup_entities(devices, async_add_entities): dev_list.append(VeSyncLightSwitch(dev)) else: _LOGGER.warning( - "%s - Unkown device type - %s", dev.device_name, dev.device_type + "%s - Unknown device type - %s", dev.device_name, dev.device_type ) continue diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 869d3cb3ed5..96e1d883646 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -29,6 +29,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, + SUPPORT_SHUFFLE_SET, ) from homeassistant.const import ( CONF_HOST, @@ -65,6 +66,7 @@ SUPPORT_VOLUMIO = ( | SUPPORT_PLAY | SUPPORT_VOLUME_STEP | SUPPORT_SELECT_SOURCE + | SUPPORT_SHUFFLE_SET | SUPPORT_CLEAR_PLAYLIST ) @@ -231,6 +233,11 @@ class Volumio(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def shuffle(self): + """Boolean if shuffle is enabled.""" + return self._state.get("random", False) + @property def source_list(self): """Return the list of available input sources.""" @@ -296,6 +303,12 @@ class Volumio(MediaPlayerDevice): "commands", params={"cmd": "volume", "volume": self._lastvol} ) + def async_set_shuffle(self, shuffle): + """Enable/disable shuffle mode.""" + return self.send_volumio_msg( + "commands", params={"cmd": "random", "value": str(shuffle)} + ) + def async_select_source(self, source): """Choose a different available playlist and play it.""" self._currentplaylist = source diff --git a/homeassistant/components/water_heater/__init__.py b/homeassistant/components/water_heater/__init__.py index d4402eec3b5..c41381fe5fa 100644 --- a/homeassistant/components/water_heater/__init__.py +++ b/homeassistant/components/water_heater/__init__.py @@ -27,6 +27,9 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) + +# mypy: allow-untyped-defs, no-check-untyped-defs + DEFAULT_MIN_TEMP = 110 DEFAULT_MAX_TEMP = 140 diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 0bd22304119..8f276279ee5 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -11,6 +11,9 @@ 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__) ATTR_CONDITION_CLASS = "condition_class" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 0dc07ebfd3f..deb3600574f 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -7,6 +7,7 @@ from homeassistant.core import callback, DOMAIN as HASS_DOMAIN from homeassistant.exceptions import Unauthorized, ServiceNotFound, HomeAssistantError 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 . import const, decorators, messages @@ -21,6 +22,7 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_get_services) async_reg(hass, handle_get_config) async_reg(hass, handle_ping) + async_reg(hass, handle_render_template) def pong_message(iden): @@ -202,3 +204,45 @@ def handle_ping(hass, connection, msg): Async friendly. """ connection.send_message(pong_message(msg["id"])) + + +@callback +@decorators.websocket_command( + { + vol.Required("type"): "render_template", + vol.Required("template"): cv.template, + vol.Optional("entity_ids"): cv.entity_ids, + vol.Optional("variables"): dict, + } +) +def handle_render_template(hass, connection, msg): + """Handle render_template command. + + Async friendly. + """ + template = msg["template"] + template.hass = hass + + variables = msg.get("variables") + + entity_ids = msg.get("entity_ids") + if entity_ids is None: + entity_ids = template.extract_entities(variables) + + @callback + def state_listener(*_): + connection.send_message( + messages.event_message( + msg["id"], {"result": template.async_render(variables)} + ) + ) + + if entity_ids and entity_ids != MATCH_ALL: + connection.subscriptions[msg["id"]] = async_track_state_change( + hass, entity_ids, state_listener + ) + else: + connection.subscriptions[msg["id"]] = lambda: None + + connection.send_result(msg["id"]) + state_listener() diff --git a/homeassistant/components/wemo/.translations/es-419.json b/homeassistant/components/wemo/.translations/es-419.json new file mode 100644 index 00000000000..df390e73dd1 --- /dev/null +++ b/homeassistant/components/wemo/.translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Wemo en la red.", + "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Wemo." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Wemo?", + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/fr.json b/homeassistant/components/wemo/.translations/fr.json new file mode 100644 index 00000000000..c1c8830cb25 --- /dev/null +++ b/homeassistant/components/wemo/.translations/fr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/hr.json b/homeassistant/components/wemo/.translations/hr.json new file mode 100644 index 00000000000..389bfbd3cb1 --- /dev/null +++ b/homeassistant/components/wemo/.translations/hr.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/wirelesstag/sensor.py b/homeassistant/components/wirelesstag/sensor.py index df8a9c46ac7..fa72ab184e1 100644 --- a/homeassistant/components/wirelesstag/sensor.py +++ b/homeassistant/components/wirelesstag/sensor.py @@ -72,7 +72,7 @@ class WirelessTagSensor(WirelessTagBaseSensor): @property def entity_id(self): - """Overriden version.""" + """Overridden version.""" return self._entity_id @property diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 03a84ca4599..21f87d9ce0b 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import logging import re +from typing import Any, Callable, Optional, Union import aiohttp import async_timeout @@ -56,29 +57,25 @@ class WUSensorConfig: def __init__( self, - friendly_name, - feature, - value, - unit_of_measurement=None, + friendly_name: Union[str, Callable], + feature: str, + value: Callable[["WUndergroundData"], Any], + unit_of_measurement: Optional[str] = None, entity_picture=None, - icon="mdi:gauge", + icon: str = "mdi:gauge", device_state_attributes=None, device_class=None, ): """Constructor. - Args: - friendly_name (string|func): Friendly name - feature (string): WU feature. See: + :param friendly_name: Friendly name + :param feature: WU feature. See: https://www.wunderground.com/weather/api/d/docs?d=data/index - value (function(WUndergroundData)): callback that - extracts desired value from WUndergroundData object - unit_of_measurement (string): unit of measurement - entity_picture (string): value or callback returning - URL of entity picture - icon (string): icon name or URL - device_state_attributes (dict): dictionary of attributes, - or callable that returns it + :param value: callback that extracts desired value from WUndergroundData object + :param unit_of_measurement: unit of measurement + :param entity_picture: value or callback returning URL of entity picture + :param icon: icon name or URL + :param device_state_attributes: dictionary of attributes, or callable that returns it """ self.friendly_name = friendly_name self.unit_of_measurement = unit_of_measurement @@ -95,21 +92,18 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): def __init__( self, - friendly_name, - field, - icon="mdi:gauge", - unit_of_measurement=None, + friendly_name: Union[str, Callable], + field: str, + icon: Optional[str] = "mdi:gauge", + unit_of_measurement: Optional[str] = None, device_class=None, ): """Constructor. - Args: - friendly_name (string|func): Friendly name of sensor - field (string): Field name in the "current_observation" - dictionary. - icon (string): icon name or URL, if None sensor - will use current weather symbol - unit_of_measurement (string): unit of measurement + :param friendly_name: Friendly name of sensor + :field: Field name in the "current_observation" dictionary. + :icon: icon name or URL, if None sensor will use current weather symbol + :unit_of_measurement: unit of measurement """ super().__init__( friendly_name, @@ -130,13 +124,14 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): class WUDailyTextForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for daily text forecasts.""" - def __init__(self, period, field, unit_of_measurement=None): + def __init__( + self, period: int, field: str, unit_of_measurement: Optional[str] = None + ): """Constructor. - Args: - period (int): forecast period number - field (string): field name to use as value - unit_of_measurement(string): unit of measurement + :param period: forecast period number + :param field: field name to use as value + :param unit_of_measurement: unit of measurement """ super().__init__( friendly_name=lambda wu: wu.data["forecast"]["txt_forecast"]["forecastday"][ @@ -161,24 +156,22 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): def __init__( self, - friendly_name, - period, - field, - wu_unit=None, - ha_unit=None, + friendly_name: str, + period: int, + field: str, + wu_unit: Optional[str] = None, + ha_unit: Optional[str] = None, icon=None, device_class=None, ): """Constructor. - Args: - period (int): forecast period number - field (string): field name to use as value - wu_unit (string): "fahrenheit", "celsius", "degrees" etc. - see the example json at: - https://www.wunderground.com/weather/api/d/docs?d=data/forecast&MR=1 - ha_unit (string): corresponding unit in home assistant - title (string): friendly_name of the sensor + :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 """ super().__init__( friendly_name=friendly_name, @@ -213,12 +206,11 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): class WUHourlyForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for hourly text forecasts.""" - def __init__(self, period, field): + def __init__(self, period: int, field: int): """Constructor. - Args: - period (int): forecast period number - field (int): field name to use as value + :param period: forecast period number + :param field: field name to use as value """ super().__init__( friendly_name=lambda wu: "{} {}".format( @@ -274,24 +266,22 @@ class WUAlmanacSensorConfig(WUSensorConfig): def __init__( self, - friendly_name, - field, - value_type, - wu_unit, - unit_of_measurement, - icon, + friendly_name: Union[str, Callable], + field: str, + value_type: str, + wu_unit: str, + unit_of_measurement: str, + icon: str, device_class=None, ): """Constructor. - Args: - friendly_name (string|func): Friendly name - field (string): value name returned in 'almanac' dict - as returned by the WU API - value_type (string): "record" or "normal" - wu_unit (string): unit name in WU API - icon (string): icon name or URL - unit_of_measurement (string): unit of measurement + :param friendly_name: Friendly name + :param field: value name returned in 'almanac' dict as returned by the WU API + :param value_type: "record" or "normal" + :param wu_unit: unit name in WU API + :param unit_of_measurement: unit of measurement + :param icon: icon name or URL """ super().__init__( friendly_name=friendly_name, @@ -306,11 +296,10 @@ class WUAlmanacSensorConfig(WUSensorConfig): class WUAlertsSensorConfig(WUSensorConfig): """Helper for defining field configuration for alerts.""" - def __init__(self, friendly_name): + def __init__(self, friendly_name: Union[str, Callable]): """Constructor. - Args: - friendly_name (string|func): Friendly name + :param friendly_name: Friendly name """ super().__init__( friendly_name=friendly_name, diff --git a/homeassistant/components/wwlln/.translations/cy.json b/homeassistant/components/wwlln/.translations/cy.json new file mode 100644 index 00000000000..e9de2acbdc6 --- /dev/null +++ b/homeassistant/components/wwlln/.translations/cy.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Enw eisoes wedi gofrestru" + }, + "step": { + "user": { + "data": { + "latitude": "Lledred", + "longitude": "Hydred", + "radius": "Radiws (gan ddefnyddio'ch system uned sylfaenol)" + }, + "title": "Cwblhewch gwybodaeth eich lleoliad" + } + }, + "title": "Rhwydwaith Lleoliad Golau Byd-eang (WWLLN)" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/es-419.json b/homeassistant/components/wwlln/.translations/es-419.json new file mode 100644 index 00000000000..d185410a4ef --- /dev/null +++ b/homeassistant/components/wwlln/.translations/es-419.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Lugar ya registrado" + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Radio (usando su sistema de unidad base)" + }, + "title": "Complete su informaci\u00f3n de ubicaci\u00f3n." + } + }, + "title": "Red Mundial de Localizaci\u00f3n de Rayos (WWLLN)" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/hr.json b/homeassistant/components/wwlln/.translations/hr.json new file mode 100644 index 00000000000..09ca1a0273f --- /dev/null +++ b/homeassistant/components/wwlln/.translations/hr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "identifier_exists": "Lokacija je ve\u0107 registrirana" + }, + "step": { + "user": { + "data": { + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina", + "radius": "Radius (koriste\u0107i sustav osnovne jedinice)" + }, + "title": "Ispunite podatke o lokaciji." + } + }, + "title": "Svjetska mre\u017ea lokacija munje (WWLLN)" + } +} \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json index d233b485bd0..704c7baeecb 100644 --- a/homeassistant/components/wwlln/.translations/pl.json +++ b/homeassistant/components/wwlln/.translations/pl.json @@ -8,11 +8,11 @@ "data": { "latitude": "Szeroko\u015b\u0107 geograficzna", "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "radius": "Promie\u0144" + "radius": "Promie\u0144 (przy u\u017cyciu systemu jednostki bazowej)" }, - "title": "Wprowad\u017a dane o swojej lokalizacji." + "title": "Wpisz informacje o swojej lokalizacji." } }, - "title": "World Wide Lightning Location Network (WWLLN)" + "title": "\u015awiatowa sie\u0107 lokalizacji wy\u0142adowa\u0144 atmosferycznych (WWLLN)" } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/cover.py b/homeassistant/components/xiaomi_aqara/cover.py index 2b6301f4133..da6b24d616a 100644 --- a/homeassistant/components/xiaomi_aqara/cover.py +++ b/homeassistant/components/xiaomi_aqara/cover.py @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for (_, gateway) in hass.data[PY_XIAOMI_GATEWAY].gateways.items(): for device in gateway.devices["cover"]: model = device["model"] - if model == "curtain": + if model in ["curtain", "curtain.aq2", "curtain.hagl04"]: if "proto" not in device or int(device["proto"][0:1]) == 1: data_key = DATA_KEY_PROTO_V1 else: diff --git a/homeassistant/components/xiaomi_aqara/manifest.json b/homeassistant/components/xiaomi_aqara/manifest.json index 8620b1dc34c..36da259f82e 100644 --- a/homeassistant/components/xiaomi_aqara/manifest.json +++ b/homeassistant/components/xiaomi_aqara/manifest.json @@ -3,7 +3,7 @@ "name": "Xiaomi aqara", "documentation": "https://www.home-assistant.io/components/xiaomi_aqara", "requirements": [ - "PyXiaomiGateway==0.12.3" + "PyXiaomiGateway==0.12.4" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 5905f86d1bd..ab686a97989 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -11,12 +11,12 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from . import config_flow # noqa # pylint: disable=unused-import from . import api from .core import ZHAGateway -from .core.channels.registry import populate_channel_registry from .core.const import ( COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, + CONF_ENABLE_QUIRKS, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, @@ -26,7 +26,6 @@ from .core.const import ( DEFAULT_BAUDRATE, DEFAULT_RADIO_TYPE, DOMAIN, - ENABLE_QUIRKS, RadioType, ) from .core.registries import establish_device_mappings @@ -46,7 +45,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema( {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY} ), - vol.Optional(ENABLE_QUIRKS, default=True): cv.boolean, + vol.Optional(CONF_ENABLE_QUIRKS, default=True): cv.boolean, } ) }, @@ -90,7 +89,6 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ establish_device_mappings() - populate_channel_registry() for component in COMPONENTS: hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {}) @@ -99,7 +97,7 @@ async def async_setup_entry(hass, config_entry): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}) - if config.get(ENABLE_QUIRKS, True): + if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported # pylint: disable=W0611, W0612 diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 0e26bfab592..95fea9b5e71 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -20,17 +20,17 @@ from .core.const import ( ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, ATTR_MANUFACTURER, + ATTR_NAME, ATTR_VALUE, - CLIENT_COMMANDS, + CLUSTER_COMMAND_SERVER, + CLUSTER_COMMANDS_CLIENT, + CLUSTER_COMMANDS_SERVER, + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY, DOMAIN, - IN, MFG_CLUSTER_ID_START, - NAME, - OUT, - SERVER, - SERVER_COMMANDS, ) from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters @@ -74,7 +74,7 @@ SERVICE_SCHEMAS = { vol.Required(ATTR_IEEE): convert_ieee, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, - vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string, + vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, vol.Required(ATTR_ATTRIBUTE): cv.positive_int, vol.Required(ATTR_VALUE): cv.string, vol.Optional(ATTR_MANUFACTURER): cv.positive_int, @@ -85,7 +85,7 @@ SERVICE_SCHEMAS = { vol.Required(ATTR_IEEE): convert_ieee, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, - vol.Optional(ATTR_CLUSTER_TYPE, default=IN): cv.string, + vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, vol.Required(ATTR_COMMAND): cv.positive_int, vol.Required(ATTR_COMMAND_TYPE): cv.string, vol.Optional(ATTR_ARGS, default=""): cv.string, @@ -155,7 +155,10 @@ def async_get_device_info(hass, device, ha_device_registry=None): ret_device = {} ret_device.update(device.device_info) ret_device["entities"] = [ - {"entity_id": entity_ref.reference_id, NAME: entity_ref.device_info[NAME]} + { + "entity_id": entity_ref.reference_id, + ATTR_NAME: entity_ref.device_info[ATTR_NAME], + } for entity_ref in zha_gateway.device_registry[device.ieee] ] @@ -201,21 +204,21 @@ async def websocket_device_clusters(hass, connection, msg): if zha_device is not None: clusters_by_endpoint = zha_device.async_get_clusters() for ep_id, clusters in clusters_by_endpoint.items(): - for c_id, cluster in clusters[IN].items(): + for c_id, cluster in clusters[CLUSTER_TYPE_IN].items(): response_clusters.append( { - TYPE: IN, + TYPE: CLUSTER_TYPE_IN, ID: c_id, - NAME: cluster.__class__.__name__, + ATTR_NAME: cluster.__class__.__name__, "endpoint_id": ep_id, } ) - for c_id, cluster in clusters[OUT].items(): + for c_id, cluster in clusters[CLUSTER_TYPE_OUT].items(): response_clusters.append( { - TYPE: OUT, + TYPE: CLUSTER_TYPE_OUT, ID: c_id, - NAME: cluster.__class__.__name__, + ATTR_NAME: cluster.__class__.__name__, "endpoint_id": ep_id, } ) @@ -250,7 +253,9 @@ async def websocket_device_cluster_attributes(hass, connection, msg): ) if attributes is not None: for attr_id in attributes: - cluster_attributes.append({ID: attr_id, NAME: attributes[attr_id][0]}) + cluster_attributes.append( + {ID: attr_id, ATTR_NAME: attributes[attr_id][0]} + ) _LOGGER.debug( "Requested attributes for: %s %s %s %s", "{}: [{}]".format(ATTR_CLUSTER_ID, cluster_id), @@ -289,20 +294,20 @@ async def websocket_device_cluster_commands(hass, connection, msg): ) if commands is not None: - for cmd_id in commands[CLIENT_COMMANDS]: + for cmd_id in commands[CLUSTER_COMMANDS_CLIENT]: cluster_commands.append( { TYPE: CLIENT, ID: cmd_id, - NAME: commands[CLIENT_COMMANDS][cmd_id][0], + ATTR_NAME: commands[CLUSTER_COMMANDS_CLIENT][cmd_id][0], } ) - for cmd_id in commands[SERVER_COMMANDS]: + for cmd_id in commands[CLUSTER_COMMANDS_SERVER]: cluster_commands.append( { - TYPE: SERVER, + TYPE: CLUSTER_COMMAND_SERVER, ID: cmd_id, - NAME: commands[SERVER_COMMANDS][cmd_id][0], + ATTR_NAME: commands[CLUSTER_COMMANDS_SERVER][cmd_id][0], } ) _LOGGER.debug( diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 7ca7dbf9db3..24c2b92e739 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -2,34 +2,35 @@ import logging from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_GAS, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, + DEVICE_CLASS_OCCUPANCY, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_VIBRATION, DOMAIN, BinarySensorDevice, - DEVICE_CLASS_MOVING, - DEVICE_CLASS_MOTION, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_MOISTURE, - DEVICE_CLASS_SMOKE, - DEVICE_CLASS_GAS, - DEVICE_CLASS_VIBRATION, - DEVICE_CLASS_OCCUPANCY, ) from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( + CHANNEL_ATTRIBUTE, + CHANNEL_ON_OFF, + CHANNEL_ZONE, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - ON_OFF_CHANNEL, - ZONE_CHANNEL, - SIGNAL_ATTR_UPDATED, - ATTRIBUTE_CHANNEL, - UNKNOWN, - OPENING, - ZONE, - OCCUPANCY, + SENSOR_ACCELERATION, + SENSOR_OCCUPANCY, + SENSOR_OPENING, SENSOR_TYPE, - ACCELERATION, + SIGNAL_ATTR_UPDATED, + UNKNOWN, + ZHA_DISCOVERY_NEW, + ZONE, ) from .entity import ZhaEntity @@ -54,10 +55,10 @@ async def get_ias_device_class(channel): DEVICE_CLASS_REGISTRY = { UNKNOWN: None, - OPENING: DEVICE_CLASS_OPENING, + SENSOR_OPENING: DEVICE_CLASS_OPENING, ZONE: get_ias_device_class, - OCCUPANCY: DEVICE_CLASS_OCCUPANCY, - ACCELERATION: DEVICE_CLASS_MOVING, + SENSOR_OCCUPANCY: DEVICE_CLASS_OCCUPANCY, + SENSOR_ACCELERATION: DEVICE_CLASS_MOVING, } @@ -108,9 +109,9 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): """Initialize the ZHA binary sensor.""" super().__init__(**kwargs) self._device_state_attributes = {} - self._zone_channel = self.cluster_channels.get(ZONE_CHANNEL) - self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) - self._attr_channel = self.cluster_channels.get(ATTRIBUTE_CHANNEL) + 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] async def _determine_device_class(self): diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 6183d36427c..20756f26b72 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -13,21 +13,18 @@ from random import uniform from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send -from ..helpers import ( - configure_reporting, - construct_unique_id, - safe_read, - get_attr_id_by_name, - bind_cluster, - LogMixin, -) + from ..const import ( + CHANNEL_ATTRIBUTE, + CHANNEL_EVENT_RELAY, + CHANNEL_ZDO, REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_RPT_CHANGE, SIGNAL_ATTR_UPDATED, - ATTRIBUTE_CHANNEL, - EVENT_RELAY_CHANNEL, - ZDO_CHANNEL, ) +from ..helpers import LogMixin, get_attr_id_by_name, safe_read from ..registries import CLUSTER_REPORT_CONFIGS _LOGGER = logging.getLogger(__name__) @@ -83,6 +80,7 @@ class ZigbeeChannel(LogMixin): """Base channel for a Zigbee cluster.""" CHANNEL_NAME = None + REPORT_CONFIG = () def __init__(self, cluster, device): """Initialize ZigbeeChannel.""" @@ -92,9 +90,15 @@ class ZigbeeChannel(LogMixin): self._generic_id = "channel_0x{:04x}".format(cluster.cluster_id) self._cluster = cluster self._zha_device = device - self._unique_id = construct_unique_id(cluster) + 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._report_config = CLUSTER_REPORT_CONFIGS.get( - self._cluster.cluster_id, [{"attr": 0, "config": REPORT_CONFIG_DEFAULT}] + self._cluster.cluster_id, self.REPORT_CONFIG ) self._status = ChannelStatus.CREATED self._cluster.add_listener(self) @@ -133,29 +137,75 @@ class ZigbeeChannel(LogMixin): """Set the reporting configuration.""" self._report_config = report_config + async def bind(self): + """Bind a zigbee cluster. + + This also swallows DeliveryError exceptions that are thrown when + devices are unreachable. + """ + from zigpy.exceptions import DeliveryError + + try: + res = await self.cluster.bind() + self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0]) + except (DeliveryError, Timeout) as ex: + self.debug( + "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) + ) + + async def configure_reporting( + self, + attr, + report_config=( + REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_RPT_CHANGE, + ), + ): + """Configure attribute reporting for a cluster. + + This also swallows DeliveryError exceptions that are thrown when + devices are unreachable. + """ + from zigpy.exceptions import DeliveryError + + attr_name = self.cluster.attributes.get(attr, [attr])[0] + + kwargs = {} + if self.cluster.cluster_id >= 0xFC00 and self.device.manufacturer_code: + kwargs["manufacturer"] = self.device.manufacturer_code + + min_report_int, max_report_int, reportable_change = report_config + try: + res = await self.cluster.configure_reporting( + attr, min_report_int, max_report_int, reportable_change, **kwargs + ) + self.debug( + "reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", + attr_name, + self.cluster.ep_attribute, + min_report_int, + max_report_int, + reportable_change, + res, + ) + except (DeliveryError, Timeout) as ex: + self.debug( + "failed to set reporting for '%s' attr on '%s' cluster: %s", + attr_name, + self.cluster.ep_attribute, + str(ex), + ) + async def async_configure(self): """Set cluster binding and attribute reporting.""" - manufacturer = None - manufacturer_code = self._zha_device.manufacturer_code # Xiaomi devices don't need this and it disrupts pairing if self._zha_device.manufacturer != "LUMI": - if self.cluster.cluster_id >= 0xFC00 and manufacturer_code: - manufacturer = manufacturer_code - await bind_cluster(self._unique_id, self.cluster) - if not self.cluster.bind_only: + await self.bind() + if self.cluster.cluster_id not in self.cluster.endpoint.out_clusters: for report_config in self._report_config: - attr = report_config.get("attr") - min_report_interval, max_report_interval, change = report_config.get( - "config" - ) - await configure_reporting( - self._unique_id, - self.cluster, - attr, - min_report=min_report_interval, - max_report=max_report_interval, - reportable_change=change, - manufacturer=manufacturer, + await self.configure_reporting( + report_config["attr"], report_config["config"] ) await asyncio.sleep(uniform(0.1, 0.5)) @@ -217,7 +267,7 @@ class ZigbeeChannel(LogMixin): def log(self, level, msg, *args): """Log a message.""" msg = "[%s]: " + msg - args = (self.unique_id,) + args + args = (self._log_id,) + args _LOGGER.log(level, msg, *args) def __getattr__(self, name): @@ -232,7 +282,8 @@ class ZigbeeChannel(LogMixin): class AttributeListeningChannel(ZigbeeChannel): """Channel for attribute reports from the cluster.""" - CHANNEL_NAME = ATTRIBUTE_CHANNEL + CHANNEL_NAME = CHANNEL_ATTRIBUTE + REPORT_CONFIG = [{"attr": 0, "config": REPORT_CONFIG_DEFAULT}] def __init__(self, cluster, device): """Initialize AttributeListeningChannel.""" @@ -266,11 +317,11 @@ class ZDOChannel(LogMixin): def __init__(self, cluster, device): """Initialize ZDOChannel.""" - self.name = ZDO_CHANNEL + self.name = CHANNEL_ZDO self._cluster = cluster self._zha_device = device self._status = ChannelStatus.CREATED - self._unique_id = "{}_ZDO".format(device.name) + self._unique_id = "{}:{}_ZDO".format(str(device.ieee), device.name) self._cluster.add_listener(self) @property @@ -320,7 +371,7 @@ class ZDOChannel(LogMixin): class EventRelayChannel(ZigbeeChannel): """Event relay that can be attached to zigbee clusters.""" - CHANNEL_NAME = EVENT_RELAY_CHANNEL + CHANNEL_NAME = CHANNEL_EVENT_RELAY @callback def attribute_updated(self, attrid, value): @@ -345,3 +396,17 @@ class EventRelayChannel(ZigbeeChannel): self.zha_send_event( self._cluster, self._cluster.server_commands.get(command_id)[0], args ) + + +# pylint: disable=wrong-import-position +from . import closures # noqa +from . import general # noqa +from . import homeautomation # noqa +from . import hvac # noqa +from . import lighting # noqa +from . import lightlink # noqa +from . import manufacturerspecific # noqa +from . import measurement # noqa +from . import protocol # noqa +from . import security # noqa +from . import smartenergy # noqa diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py index 5c3a3d46f0d..0559c4a1f76 100644 --- a/homeassistant/components/zha/core/channels/closures.py +++ b/homeassistant/components/zha/core/channels/closures.py @@ -5,18 +5,25 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + +import zigpy.zcl.clusters.closures as closures + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send + from . import ZigbeeChannel -from ..const import SIGNAL_ATTR_UPDATED +from .. import registries +from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.DoorLock.cluster_id) class DoorLockChannel(ZigbeeChannel): """Door lock channel.""" _value_attribute = 0 + REPORT_CONFIG = ({"attr": "lock_state", "config": REPORT_CONFIG_IMMEDIATE},) async def async_update(self): """Retrieve latest state.""" @@ -46,3 +53,17 @@ class DoorLockChannel(ZigbeeChannel): """Initialize channel.""" await self.get_attribute_value(self._value_attribute, from_cache=from_cache) await super().async_initialize(from_cache) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id) +class Shade(ZigbeeChannel): + """Shade channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.WindowCovering.cluster_id) +class WindowCovering(ZigbeeChannel): + """Window channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 5d624c832dd..6a828ef1ad8 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -5,20 +5,240 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + +import zigpy.zcl.clusters.general as general + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later -from . import ZigbeeChannel, parse_and_log_command + +from . import AttributeListeningChannel, ZigbeeChannel, parse_and_log_command +from .. import registries +from ..const import ( + REPORT_CONFIG_ASAP, + REPORT_CONFIG_BATTERY_SAVE, + REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_IMMEDIATE, + SIGNAL_ATTR_UPDATED, + SIGNAL_MOVE_LEVEL, + SIGNAL_SET_LEVEL, +) from ..helpers import get_attr_id_by_name -from ..const import SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL _LOGGER = logging.getLogger(__name__) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) +class Alarms(ZigbeeChannel): + """Alarms channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogInput.cluster_id) +class AnalogInput(AttributeListeningChannel): + """Analog Input channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id) +class AnalogOutput(AttributeListeningChannel): + """Analog Output channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id) +class AnalogValue(AttributeListeningChannel): + """Analog Value channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.ApplianceControl.cluster_id) +class ApplianceContorl(ZigbeeChannel): + """Appliance Control channel.""" + + pass + + +@registries.CHANNEL_ONLY_CLUSTERS.register(general.Basic.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Basic.cluster_id) +class BasicChannel(ZigbeeChannel): + """Channel to interact with the basic cluster.""" + + UNKNOWN = 0 + BATTERY = 3 + + POWER_SOURCES = { + UNKNOWN: "Unknown", + 1: "Mains (single phase)", + 2: "Mains (3 phase)", + BATTERY: "Battery", + 4: "DC source", + 5: "Emergency mains constantly powered", + 6: "Emergency mains and transfer switch", + } + + def __init__(self, cluster, device): + """Initialize BasicChannel.""" + super().__init__(cluster, device) + self._power_source = None + + async def async_configure(self): + """Configure this channel.""" + await super().async_configure() + await self.async_initialize(False) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + self._power_source = await self.get_attribute_value( + "power_source", from_cache=from_cache + ) + await super().async_initialize(from_cache) + + def get_power_source(self): + """Get the power source.""" + return self._power_source + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) +class BinaryInput(AttributeListeningChannel): + """Binary Input channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryOutput.cluster_id) +class BinaryOutput(AttributeListeningChannel): + """Binary Output channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryValue.cluster_id) +class BinaryValue(AttributeListeningChannel): + """Binary Value channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Commissioning.cluster_id) +class Commissioning(ZigbeeChannel): + """Commissioning channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.DeviceTemperature.cluster_id) +class DeviceTemperature(ZigbeeChannel): + """Device Temperature channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.GreenPowerProxy.cluster_id) +class GreenPowerProxy(ZigbeeChannel): + """Green Power Proxy channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Groups.cluster_id) +class Groups(ZigbeeChannel): + """Groups channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Identify.cluster_id) +class Identify(ZigbeeChannel): + """Identify channel.""" + + pass + + +@registries.BINDABLE_CLUSTERS.register(general.LevelControl.cluster_id) +@registries.EVENT_RELAY_CLUSTERS.register(general.LevelControl.cluster_id) +@registries.LIGHT_CLUSTERS.register(general.LevelControl.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id) +class LevelControlChannel(ZigbeeChannel): + """Channel for the LevelControl Zigbee cluster.""" + + CURRENT_LEVEL = 0 + REPORT_CONFIG = ({"attr": "current_level", "config": REPORT_CONFIG_ASAP},) + + @callback + def cluster_command(self, tsn, command_id, args): + """Handle commands received to this cluster.""" + cmd = parse_and_log_command(self, tsn, command_id, args) + + if cmd in ("move_to_level", "move_to_level_with_on_off"): + self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0]) + elif cmd in ("move", "move_with_on_off"): + # We should dim slowly -- for now, just step once + rate = args[1] + if args[0] == 0xFF: + rate = 10 # Should read default move rate + self.dispatch_level_change(SIGNAL_MOVE_LEVEL, -rate if args[0] else rate) + elif cmd in ("step", "step_with_on_off"): + # Step (technically may change on/off) + self.dispatch_level_change( + SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1] + ) + + @callback + def attribute_updated(self, attrid, value): + """Handle attribute updates on this cluster.""" + self.debug("received attribute: %s update with value: %s", attrid, value) + if attrid == self.CURRENT_LEVEL: + self.dispatch_level_change(SIGNAL_SET_LEVEL, value) + + def dispatch_level_change(self, command, level): + """Dispatch level change.""" + async_dispatcher_send( + self._zha_device.hass, "{}_{}".format(self.unique_id, command), level + ) + + async def async_initialize(self, from_cache): + """Initialize channel.""" + await self.get_attribute_value(self.CURRENT_LEVEL, from_cache=from_cache) + await super().async_initialize(from_cache) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateInput.cluster_id) +class MultistateInput(AttributeListeningChannel): + """Multistate Input channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateOutput.cluster_id) +class MultistateOutput(AttributeListeningChannel): + """Multistate Output channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateValue.cluster_id) +class MultistateValue(AttributeListeningChannel): + """Multistate Value channel.""" + + REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.BINARY_SENSOR_CLUSTERS.register(general.OnOff.cluster_id) +@registries.BINDABLE_CLUSTERS.register(general.OnOff.cluster_id) +@registries.EVENT_RELAY_CLUSTERS.register(general.OnOff.cluster_id) +@registries.LIGHT_CLUSTERS.register(general.OnOff.cluster_id) +@registries.SWITCH_CLUSTERS.register(general.OnOff.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id) class OnOffChannel(ZigbeeChannel): """Channel for the OnOff Zigbee cluster.""" ON_OFF = 0 + REPORT_CONFIG = ({"attr": "on_off", "config": REPORT_CONFIG_IMMEDIATE},) def __init__(self, cluster, device): """Initialize OnOffChannel.""" @@ -87,90 +307,44 @@ class OnOffChannel(ZigbeeChannel): await super().async_update() -class LevelControlChannel(ZigbeeChannel): - """Channel for the LevelControl Zigbee cluster.""" +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOffConfiguration.cluster_id) +class OnOffConfiguration(ZigbeeChannel): + """OnOff Configuration channel.""" - CURRENT_LEVEL = 0 - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command(self, tsn, command_id, args) - - if cmd in ("move_to_level", "move_to_level_with_on_off"): - self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0]) - elif cmd in ("move", "move_with_on_off"): - # We should dim slowly -- for now, just step once - rate = args[1] - if args[0] == 0xFF: - rate = 10 # Should read default move rate - self.dispatch_level_change(SIGNAL_MOVE_LEVEL, -rate if args[0] else rate) - elif cmd in ("step", "step_with_on_off"): - # Step (technically may change on/off) - self.dispatch_level_change( - SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1] - ) - - @callback - def attribute_updated(self, attrid, value): - """Handle attribute updates on this cluster.""" - self.debug("received attribute: %s update with value: %s", attrid, value) - if attrid == self.CURRENT_LEVEL: - self.dispatch_level_change(SIGNAL_SET_LEVEL, value) - - def dispatch_level_change(self, command, level): - """Dispatch level change.""" - async_dispatcher_send( - self._zha_device.hass, "{}_{}".format(self.unique_id, command), level - ) - - async def async_initialize(self, from_cache): - """Initialize channel.""" - await self.get_attribute_value(self.CURRENT_LEVEL, from_cache=from_cache) - await super().async_initialize(from_cache) + pass -class BasicChannel(ZigbeeChannel): - """Channel to interact with the basic cluster.""" +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Ota.cluster_id) +class Ota(ZigbeeChannel): + """OTA Channel.""" - UNKNOWN = 0 - BATTERY = 3 - - POWER_SOURCES = { - UNKNOWN: "Unknown", - 1: "Mains (single phase)", - 2: "Mains (3 phase)", - BATTERY: "Battery", - 4: "DC source", - 5: "Emergency mains constantly powered", - 6: "Emergency mains and transfer switch", - } - - def __init__(self, cluster, device): - """Initialize BasicChannel.""" - super().__init__(cluster, device) - self._power_source = None - - async def async_configure(self): - """Configure this channel.""" - await super().async_configure() - await self.async_initialize(False) - - async def async_initialize(self, from_cache): - """Initialize channel.""" - self._power_source = await self.get_attribute_value( - "power_source", from_cache=from_cache - ) - await super().async_initialize(from_cache) - - def get_power_source(self): - """Get the power source.""" - return self._power_source + pass +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Partition.cluster_id) +class Partition(ZigbeeChannel): + """Partition channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PollControl.cluster_id) +class PollControl(ZigbeeChannel): + """Poll Control channel.""" + + pass + + +@registries.DEVICE_TRACKER_CLUSTERS.register(general.PowerConfiguration.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) class PowerConfigurationChannel(ZigbeeChannel): """Channel for the zigbee power configuration cluster.""" + REPORT_CONFIG = ( + {"attr": "battery_voltage", "config": REPORT_CONFIG_BATTERY_SAVE}, + {"attr": "battery_percentage_remaining", "config": REPORT_CONFIG_BATTERY_SAVE}, + ) + @callback def attribute_updated(self, attrid, value): """Handle attribute updates on this cluster.""" @@ -203,3 +377,32 @@ class PowerConfigurationChannel(ZigbeeChannel): ) await self.get_attribute_value("battery_voltage", from_cache=from_cache) await self.get_attribute_value("battery_quantity", from_cache=from_cache) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) +class PowerProfile(ZigbeeChannel): + """Power Profile channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.RSSILocation.cluster_id) +class RSSILocation(ZigbeeChannel): + """RSSI Location channel.""" + + pass + + +@registries.OUTPUT_CHANNEL_ONLY_CLUSTERS.register(general.Scenes.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Scenes.cluster_id) +class Scenes(ZigbeeChannel): + """Scenes channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Time.cluster_id) +class Time(ZigbeeChannel): + """Time channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index dee6e43f475..198eec67a46 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -5,17 +5,65 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + +import zigpy.zcl.clusters.homeautomation as homeautomation + from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import AttributeListeningChannel -from ..const import SIGNAL_ATTR_UPDATED, ELECTRICAL_MEASUREMENT_CHANNEL + +from . import AttributeListeningChannel, ZigbeeChannel +from .. import registries +from ..const import ( + CHANNEL_ELECTRICAL_MEASUREMENT, + REPORT_CONFIG_DEFAULT, + SIGNAL_ATTR_UPDATED, +) _LOGGER = logging.getLogger(__name__) +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.ApplianceEventAlerts.cluster_id +) +class ApplianceEventAlerts(ZigbeeChannel): + """Appliance Event Alerts channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.ApplianceIdentification.cluster_id +) +class ApplianceIdentification(ZigbeeChannel): + """Appliance Identification channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.ApplianceStatistics.cluster_id +) +class ApplianceStatistics(ZigbeeChannel): + """Appliance Statistics channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.Diagnostic.cluster_id) +class Diagnostic(ZigbeeChannel): + """Diagnostic channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.ElectricalMeasurement.cluster_id +) class ElectricalMeasurementChannel(AttributeListeningChannel): """Channel that polls active power level.""" - CHANNEL_NAME = ELECTRICAL_MEASUREMENT_CHANNEL + CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT + + REPORT_CONFIG = ({"attr": "active_power", "config": REPORT_CONFIG_DEFAULT},) async def async_update(self): """Retrieve latest state.""" @@ -33,3 +81,12 @@ class ElectricalMeasurementChannel(AttributeListeningChannel): """Initialize channel.""" await self.get_attribute_value("active_power", from_cache=from_cache) await super().async_initialize(from_cache) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + homeautomation.MeterIdentification.cluster_id +) +class MeterIdentification(ZigbeeChannel): + """Metering Identification channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/hvac.py b/homeassistant/components/zha/core/channels/hvac.py index 164497abd35..46d9ffb52e5 100644 --- a/homeassistant/components/zha/core/channels/hvac.py +++ b/homeassistant/components/zha/core/channels/hvac.py @@ -5,19 +5,34 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + +import zigpy.zcl.clusters.hvac as hvac + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send + from . import ZigbeeChannel -from ..const import SIGNAL_ATTR_UPDATED +from .. import registries +from ..const import REPORT_CONFIG_OP, SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Dehumidification.cluster_id) +class Dehumidification(ZigbeeChannel): + """Dehumidification channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Fan.cluster_id) class FanChannel(ZigbeeChannel): """Fan channel.""" _value_attribute = 0 + REPORT_CONFIG = ({"attr": "fan_mode", "config": REPORT_CONFIG_OP},) + async def async_set_speed(self, value) -> None: """Set the speed of the fan.""" from zigpy.exceptions import DeliveryError @@ -56,3 +71,24 @@ class FanChannel(ZigbeeChannel): """Initialize channel.""" await self.get_attribute_value(self._value_attribute, from_cache=from_cache) await super().async_initialize(from_cache) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Pump.cluster_id) +class Pump(ZigbeeChannel): + """Pump channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id) +class Thermostat(ZigbeeChannel): + """Thermostat channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id) +class UserInterface(ZigbeeChannel): + """User interface (thermostat) channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index e36da629ace..d8f769a3e24 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -5,17 +5,38 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + +import zigpy.zcl.clusters.lighting as lighting + from . import ZigbeeChannel +from .. import registries +from ..const import REPORT_CONFIG_DEFAULT _LOGGER = logging.getLogger(__name__) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Ballast.cluster_id) +class Ballast(ZigbeeChannel): + """Ballast channel.""" + + pass + + +@registries.BINDABLE_CLUSTERS.register(lighting.Color.cluster_id) +@registries.EVENT_RELAY_CLUSTERS.register(lighting.Color.cluster_id) +@registries.LIGHT_CLUSTERS.register(lighting.Color.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id) class ColorChannel(ZigbeeChannel): """Color channel.""" CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_TEMP = 0x10 UNSUPPORTED_ATTRIBUTE = 0x86 + REPORT_CONFIG = ( + {"attr": "current_x", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "current_y", "config": REPORT_CONFIG_DEFAULT}, + {"attr": "color_temperature", "config": REPORT_CONFIG_DEFAULT}, + ) def __init__(self, cluster, device): """Initialize ColorChannel.""" diff --git a/homeassistant/components/zha/core/channels/lightlink.py b/homeassistant/components/zha/core/channels/lightlink.py index 83fca6e80c2..99fed7d5d68 100644 --- a/homeassistant/components/zha/core/channels/lightlink.py +++ b/homeassistant/components/zha/core/channels/lightlink.py @@ -6,4 +6,17 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.lightlink as lightlink + +from . import ZigbeeChannel +from .. import registries + _LOGGER = logging.getLogger(__name__) + + +@registries.CHANNEL_ONLY_CLUSTERS.register(lightlink.LightLink.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(lightlink.LightLink.cluster_id) +class LightLink(ZigbeeChannel): + """Lightlink channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/manufacturerspecific.py b/homeassistant/components/zha/core/channels/manufacturerspecific.py index a0eebd78343..6ed9de9b303 100644 --- a/homeassistant/components/zha/core/channels/manufacturerspecific.py +++ b/homeassistant/components/zha/core/channels/manufacturerspecific.py @@ -6,4 +6,35 @@ https://home-assistant.io/components/zha/ """ import logging +from . import AttributeListeningChannel +from .. import registries +from ..const import REPORT_CONFIG_ASAP, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT + + _LOGGER = logging.getLogger(__name__) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.SMARTTHINGS_HUMIDITY_CLUSTER) +class SmartThingsHumidity(AttributeListeningChannel): + """Smart Things Humidity channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), + } + ] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + registries.SMARTTHINGS_ACCELERATION_CLUSTER +) +class SmartThingsAcceleration(AttributeListeningChannel): + """Smart Things Acceleration channel.""" + + REPORT_CONFIG = [ + {"attr": "acceleration", "config": REPORT_CONFIG_ASAP}, + {"attr": "x_axis", "config": REPORT_CONFIG_ASAP}, + {"attr": "y_axis", "config": REPORT_CONFIG_ASAP}, + {"attr": "z_axis", "config": REPORT_CONFIG_ASAP}, + ] diff --git a/homeassistant/components/zha/core/channels/measurement.py b/homeassistant/components/zha/core/channels/measurement.py index 51146289e69..94d885592eb 100644 --- a/homeassistant/components/zha/core/channels/measurement.py +++ b/homeassistant/components/zha/core/channels/measurement.py @@ -6,4 +6,81 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.measurement as measurement + +from . import AttributeListeningChannel +from .. import registries +from ..const import ( + REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_IMMEDIATE, + REPORT_CONFIG_MAX_INT, + REPORT_CONFIG_MIN_INT, +) + _LOGGER = logging.getLogger(__name__) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.FlowMeasurement.cluster_id) +class FlowMeasurement(AttributeListeningChannel): + """Flow Measurement channel.""" + + REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.IlluminanceLevelSensing.cluster_id +) +class IlluminanceLevelSensing(AttributeListeningChannel): + """Illuminance Level Sensing channel.""" + + REPORT_CONFIG = [{"attr": "level_status", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.IlluminanceMeasurement.cluster_id +) +class IlluminanceMeasurement(AttributeListeningChannel): + """Illuminance Measurement channel.""" + + REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.BINARY_SENSOR_CLUSTERS.register(measurement.OccupancySensing.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id) +class OccupancySensing(AttributeListeningChannel): + """Occupancy Sensing channel.""" + + REPORT_CONFIG = [{"attr": "occupancy", "config": REPORT_CONFIG_IMMEDIATE}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PressureMeasurement.cluster_id) +class PressureMeasurement(AttributeListeningChannel): + """Pressure measurement channel.""" + + REPORT_CONFIG = [{"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.RelativeHumidity.cluster_id) +class RelativeHumidity(AttributeListeningChannel): + """Relative Humidity measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), + } + ] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + measurement.TemperatureMeasurement.cluster_id +) +class TemperatureMeasurement(AttributeListeningChannel): + """Temperature measurement channel.""" + + REPORT_CONFIG = [ + { + "attr": "measured_value", + "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), + } + ] diff --git a/homeassistant/components/zha/core/channels/protocol.py b/homeassistant/components/zha/core/channels/protocol.py index 2cae156aec5..b9785068f21 100644 --- a/homeassistant/components/zha/core/channels/protocol.py +++ b/homeassistant/components/zha/core/channels/protocol.py @@ -6,4 +6,157 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.protocol as protocol + +from .. import registries +from ..channels import ZigbeeChannel + _LOGGER = logging.getLogger(__name__) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputExtended.cluster_id) +class AnalogInputExtended(ZigbeeChannel): + """Analog Input Extended channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputRegular.cluster_id) +class AnalogInputRegular(ZigbeeChannel): + """Analog Input Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputExtended.cluster_id) +class AnalogOutputExtended(ZigbeeChannel): + """Analog Output Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputRegular.cluster_id) +class AnalogOutputRegular(ZigbeeChannel): + """Analog Output Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueExtended.cluster_id) +class AnalogValueExtended(ZigbeeChannel): + """Analog Value Extended edition channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueRegular.cluster_id) +class AnalogValueRegular(ZigbeeChannel): + """Analog Value Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BacnetProtocolTunnel.cluster_id) +class BacnetProtocolTunnel(ZigbeeChannel): + """Bacnet Protocol Tunnel channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputExtended.cluster_id) +class BinaryInputExtended(ZigbeeChannel): + """Binary Input Extended channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputRegular.cluster_id) +class BinaryInputRegular(ZigbeeChannel): + """Binary Input Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputExtended.cluster_id) +class BinaryOutputExtended(ZigbeeChannel): + """Binary Output Extended channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputRegular.cluster_id) +class BinaryOutputRegular(ZigbeeChannel): + """Binary Output Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueExtended.cluster_id) +class BinaryValueExtended(ZigbeeChannel): + """Binary Value Extended channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueRegular.cluster_id) +class BinaryValueRegular(ZigbeeChannel): + """Binary Value Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.GenericTunnel.cluster_id) +class GenericTunnel(ZigbeeChannel): + """Generic Tunnel channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + protocol.MultistateInputExtended.cluster_id +) +class MultiStateInputExtended(ZigbeeChannel): + """Multistate Input Extended channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateInputRegular.cluster_id) +class MultiStateInputRegular(ZigbeeChannel): + """Multistate Input Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + protocol.MultistateOutputExtended.cluster_id +) +class MultiStateOutputExtended(ZigbeeChannel): + """Multistate Output Extended channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + protocol.MultistateOutputRegular.cluster_id +) +class MultiStateOutputRegular(ZigbeeChannel): + """Multistate Output Regular channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register( + protocol.MultistateValueExtended.cluster_id +) +class MultiStateValueExtended(ZigbeeChannel): + """Multistate Value Extended channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateValueRegular.cluster_id) +class MultiStateValueRegular(ZigbeeChannel): + """Multistate Value Regular channel.""" + + pass diff --git a/homeassistant/components/zha/core/channels/registry.py b/homeassistant/components/zha/core/channels/registry.py deleted file mode 100644 index 9a590600f70..00000000000 --- a/homeassistant/components/zha/core/channels/registry.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Channel registry module for Zigbee Home Automation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/components/zha/ -""" -from . import ZigbeeChannel - -from .closures import DoorLockChannel -from .general import ( - OnOffChannel, - LevelControlChannel, - PowerConfigurationChannel, - BasicChannel, -) -from .homeautomation import ElectricalMeasurementChannel -from .hvac import FanChannel -from .lighting import ColorChannel -from .security import IASZoneChannel - -ZIGBEE_CHANNEL_REGISTRY = {} - - -def populate_channel_registry(): - """Populate the channel registry.""" - from zigpy import zcl - - ZIGBEE_CHANNEL_REGISTRY.update( - { - zcl.clusters.general.Alarms.cluster_id: ZigbeeChannel, - zcl.clusters.general.Commissioning.cluster_id: ZigbeeChannel, - zcl.clusters.general.Identify.cluster_id: ZigbeeChannel, - zcl.clusters.general.Groups.cluster_id: ZigbeeChannel, - zcl.clusters.general.Scenes.cluster_id: ZigbeeChannel, - zcl.clusters.general.Partition.cluster_id: ZigbeeChannel, - zcl.clusters.general.Ota.cluster_id: ZigbeeChannel, - zcl.clusters.general.PowerProfile.cluster_id: ZigbeeChannel, - zcl.clusters.general.ApplianceControl.cluster_id: ZigbeeChannel, - zcl.clusters.general.PollControl.cluster_id: ZigbeeChannel, - zcl.clusters.general.GreenPowerProxy.cluster_id: ZigbeeChannel, - zcl.clusters.general.OnOffConfiguration.cluster_id: ZigbeeChannel, - zcl.clusters.general.OnOff.cluster_id: OnOffChannel, - zcl.clusters.general.LevelControl.cluster_id: LevelControlChannel, - zcl.clusters.lighting.Color.cluster_id: ColorChannel, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: ElectricalMeasurementChannel, - zcl.clusters.general.PowerConfiguration.cluster_id: PowerConfigurationChannel, - zcl.clusters.general.Basic.cluster_id: BasicChannel, - zcl.clusters.security.IasZone.cluster_id: IASZoneChannel, - zcl.clusters.hvac.Fan.cluster_id: FanChannel, - zcl.clusters.lightlink.LightLink.cluster_id: ZigbeeChannel, - zcl.clusters.closures.DoorLock.cluster_id: DoorLockChannel, - } - ) diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 6b2a8af837f..cac93ea7214 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -5,15 +5,35 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ import logging + +import zigpy.zcl.clusters.security as security + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send + from . import ZigbeeChannel -from ..helpers import bind_cluster +from .. import registries from ..const import SIGNAL_ATTR_UPDATED _LOGGER = logging.getLogger(__name__) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasAce.cluster_id) +class IasAce(ZigbeeChannel): + """IAS Ancillary Control Equipment channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) +class IasWd(ZigbeeChannel): + """IAS Warning Device channel.""" + + pass + + +@registries.BINARY_SENSOR_CLUSTERS.register(security.IasZone.cluster_id) +@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasZone.cluster_id) class IASZoneChannel(ZigbeeChannel): """Channel for the IASZone Zigbee cluster.""" @@ -37,13 +57,14 @@ class IASZoneChannel(ZigbeeChannel): """Configure IAS device.""" # Xiaomi devices don't need this and it disrupts pairing if self._zha_device.manufacturer == "LUMI": + self.debug("finished IASZoneChannel configuration") return from zigpy.exceptions import DeliveryError self.debug("started IASZoneChannel configuration") - await bind_cluster(self.unique_id, self._cluster) - ieee = self._cluster.endpoint.device.application.ieee + await self.bind() + ieee = self.cluster.endpoint.device.application.ieee try: res = await self._cluster.write_attributes({"cie_addr": ieee}) @@ -60,7 +81,7 @@ class IASZoneChannel(ZigbeeChannel): self._cluster.ep_attribute, str(ex), ) - self.debug("%s: finished IASZoneChannel configuration") + self.debug("finished IASZoneChannel configuration") await self.get_attribute_value("zone_type", from_cache=False) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index d17eae30a96..a182193caba 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -6,4 +6,94 @@ https://home-assistant.io/components/zha/ """ import logging +import zigpy.zcl.clusters.smartenergy as smartenergy + +from .. import registries +from ..channels import AttributeListeningChannel, ZigbeeChannel +from ..const import REPORT_CONFIG_DEFAULT + _LOGGER = logging.getLogger(__name__) + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Calendar.cluster_id) +class Calendar(ZigbeeChannel): + """Calendar channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.DeviceManagement.cluster_id) +class DeviceManagement(ZigbeeChannel): + """Device Management channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Drlc.cluster_id) +class Drlc(ZigbeeChannel): + """Demand Response and Load Control channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.EnergyManagement.cluster_id) +class EnergyManagement(ZigbeeChannel): + """Energy Management channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Events.cluster_id) +class Events(ZigbeeChannel): + """Event channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.KeyEstablishment.cluster_id) +class KeyEstablishment(ZigbeeChannel): + """Key Establishment channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.MduPairing.cluster_id) +class MduPairing(ZigbeeChannel): + """Pairing channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Messaging.cluster_id) +class Messaging(ZigbeeChannel): + """Messaging channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Metering.cluster_id) +class Metering(AttributeListeningChannel): + """Metering channel.""" + + REPORT_CONFIG = [{"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT}] + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id) +class Prepayment(ZigbeeChannel): + """Prepayment channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Price.cluster_id) +class Price(ZigbeeChannel): + """Price channel.""" + + pass + + +@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Tunneling.cluster_id) +class Tunneling(ZigbeeChannel): + """Tunneling channel.""" + + pass diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 8f58f0c9af0..c35cb168fdf 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -10,132 +10,100 @@ from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH -DOMAIN = "zha" +ATTR_ARGS = "args" +ATTR_ATTRIBUTE = "attribute" +ATTR_AVAILABLE = "available" +ATTR_CLUSTER_ID = "cluster_id" +ATTR_CLUSTER_TYPE = "cluster_type" +ATTR_COMMAND = "command" +ATTR_COMMAND_TYPE = "command_type" +ATTR_ENDPOINT_ID = "endpoint_id" +ATTR_IEEE = "ieee" +ATTR_LAST_SEEN = "last_seen" +ATTR_LEVEL = "level" +ATTR_LQI = "lqi" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MANUFACTURER_CODE = "manufacturer_code" +ATTR_MODEL = "model" +ATTR_NAME = "name" +ATTR_NWK = "nwk" +ATTR_POWER_SOURCE = "power_source" +ATTR_QUIRK_APPLIED = "quirk_applied" +ATTR_QUIRK_CLASS = "quirk_class" +ATTR_RSSI = "rssi" +ATTR_SIGNATURE = "signature" +ATTR_TYPE = "type" +ATTR_VALUE = "value" BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] -DATA_ZHA = "zha" -DATA_ZHA_CONFIG = "config" -DATA_ZHA_BRIDGE_ID = "zha_bridge_id" -DATA_ZHA_DISPATCHERS = "zha_dispatchers" -DATA_ZHA_CORE_EVENTS = "zha_core_events" -DATA_ZHA_GATEWAY = "zha_gateway" -ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" +CHANNEL_ATTRIBUTE = "attribute" +CHANNEL_BASIC = "basic" +CHANNEL_COLOR = "light_color" +CHANNEL_DOORLOCK = "door_lock" +CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" +CHANNEL_EVENT_RELAY = "event_relay" +CHANNEL_FAN = "fan" +CHANNEL_LEVEL = ATTR_LEVEL +CHANNEL_ON_OFF = "on_off" +CHANNEL_POWER_CONFIGURATION = "power" +CHANNEL_ZDO = "zdo" +CHANNEL_ZONE = ZONE = "ias_zone" + +CLUSTER_COMMAND_SERVER = "server" +CLUSTER_COMMANDS_CLIENT = "client_commands" +CLUSTER_COMMANDS_SERVER = "server_commands" +CLUSTER_TYPE_IN = "in" +CLUSTER_TYPE_OUT = "out" COMPONENTS = (BINARY_SENSOR, DEVICE_TRACKER, FAN, LIGHT, LOCK, SENSOR, SWITCH) CONF_BAUDRATE = "baudrate" CONF_DATABASE = "database_path" CONF_DEVICE_CONFIG = "device_config" +CONF_ENABLE_QUIRKS = "enable_quirks" CONF_RADIO_TYPE = "radio_type" CONF_USB_PATH = "usb_path" -DATA_DEVICE_CONFIG = "zha_device_config" -ENABLE_QUIRKS = "enable_quirks" - -RADIO = "radio" -RADIO_DESCRIPTION = "radio_description" CONTROLLER = "controller" +DATA_DEVICE_CONFIG = "zha_device_config" +DATA_ZHA = "zha" +DATA_ZHA_CONFIG = "config" +DATA_ZHA_BRIDGE_ID = "zha_bridge_id" +DATA_ZHA_CORE_EVENTS = "zha_core_events" +DATA_ZHA_DISPATCHERS = "zha_dispatchers" +DATA_ZHA_GATEWAY = "zha_gateway" + +DEBUG_COMP_BELLOWS = "bellows" +DEBUG_COMP_ZHA = "homeassistant.components.zha" +DEBUG_COMP_ZIGPY = "zigpy" +DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" +DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" +DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate" +DEBUG_LEVEL_CURRENT = "current" +DEBUG_LEVEL_ORIGINAL = "original" +DEBUG_LEVELS = { + DEBUG_COMP_BELLOWS: logging.DEBUG, + DEBUG_COMP_ZHA: logging.DEBUG, + DEBUG_COMP_ZIGPY: logging.DEBUG, + DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, + DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, + DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG, +} +DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY] + DEFAULT_RADIO_TYPE = "ezsp" DEFAULT_BAUDRATE = 57600 DEFAULT_DATABASE_NAME = "zigbee.db" +DISCOVERY_KEY = "zha_discovery_info" -ATTR_CLUSTER_ID = "cluster_id" -ATTR_CLUSTER_TYPE = "cluster_type" -ATTR_ATTRIBUTE = "attribute" -ATTR_VALUE = "value" -ATTR_MANUFACTURER = "manufacturer" -ATTR_COMMAND = "command" -ATTR_COMMAND_TYPE = "command_type" -ATTR_ARGS = "args" -ATTR_ENDPOINT_ID = "endpoint_id" -ATTR_AVAILABLE = "available" +DOMAIN = "zha" -IN = "in" -OUT = "out" -CLIENT_COMMANDS = "client_commands" -SERVER_COMMANDS = "server_commands" -SERVER = "server" -IEEE = "ieee" -MODEL = "model" -NAME = "name" -LQI = "lqi" -RSSI = "rssi" -LAST_SEEN = "last_seen" - -SENSOR_TYPE = "sensor_type" -HUMIDITY = "humidity" -TEMPERATURE = "temperature" -ILLUMINANCE = "illuminance" -PRESSURE = "pressure" -METERING = "metering" -ELECTRICAL_MEASUREMENT = "electrical_measurement" -GENERIC = "generic" -BATTERY = "battery" -UNKNOWN = "unknown" -UNKNOWN_MANUFACTURER = "unk_manufacturer" -UNKNOWN_MODEL = "unk_model" -OPENING = "opening" -OCCUPANCY = "occupancy" -ACCELERATION = "acceleration" - -ATTR_LEVEL = "level" - -ZDO_CHANNEL = "zdo" -ON_OFF_CHANNEL = "on_off" -ATTRIBUTE_CHANNEL = "attribute" -BASIC_CHANNEL = "basic" -COLOR_CHANNEL = "light_color" -FAN_CHANNEL = "fan" -LEVEL_CHANNEL = ATTR_LEVEL -ZONE_CHANNEL = ZONE = "ias_zone" -ELECTRICAL_MEASUREMENT_CHANNEL = "electrical_measurement" -POWER_CONFIGURATION_CHANNEL = "power" -EVENT_RELAY_CHANNEL = "event_relay" -DOORLOCK_CHANNEL = "door_lock" - -SIGNAL_ATTR_UPDATED = "attribute_updated" -SIGNAL_MOVE_LEVEL = "move_level" -SIGNAL_SET_LEVEL = "set_level" -SIGNAL_STATE_ATTR = "update_state_attribute" -SIGNAL_AVAILABLE = "available" -SIGNAL_REMOVE = "remove" - -QUIRK_APPLIED = "quirk_applied" -QUIRK_CLASS = "quirk_class" -MANUFACTURER_CODE = "manufacturer_code" -POWER_SOURCE = "power_source" -MAINS_POWERED = "Mains" -BATTERY_OR_UNKNOWN = "Battery or Unknown" - -BELLOWS = "bellows" -ZHA = "homeassistant.components.zha" -ZIGPY = "zigpy" -ZIGPY_XBEE = "zigpy_xbee" -ZIGPY_DECONZ = "zigpy_deconz" -ORIGINAL = "original" -CURRENT = "current" -DEBUG_LEVELS = { - BELLOWS: logging.DEBUG, - ZHA: logging.DEBUG, - ZIGPY: logging.DEBUG, - ZIGPY_XBEE: logging.DEBUG, - ZIGPY_DECONZ: logging.DEBUG, -} -ADD_DEVICE_RELAY_LOGGERS = [ZHA, ZIGPY] -TYPE = "type" -NWK = "nwk" -SIGNATURE = "signature" -RAW_INIT = "raw_device_initialized" -ZHA_GW_MSG = "zha_gateway_message" -DEVICE_REMOVED = "device_removed" -DEVICE_INFO = "device_info" -DEVICE_FULL_INIT = "device_fully_initialized" -DEVICE_JOINED = "device_joined" -LOG_OUTPUT = "log_output" -LOG_ENTRY = "log_entry" MFG_CLUSTER_ID_START = 0xFC00 +POWER_MAINS_POWERED = "Mains" +POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" + class RadioType(enum.Enum): """Possible options for radio type.""" @@ -143,6 +111,7 @@ class RadioType(enum.Enum): ezsp = "ezsp" xbee = "xbee" deconz = "deconz" + zigate = "zigate" @classmethod def list(cls): @@ -150,8 +119,6 @@ class RadioType(enum.Enum): return [e.value for e in RadioType] -DISCOVERY_KEY = "zha_discovery_info" - REPORT_CONFIG_MAX_INT = 900 REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800 REPORT_CONFIG_MIN_INT = 30 @@ -185,3 +152,39 @@ REPORT_CONFIG_OP = ( REPORT_CONFIG_MAX_INT, REPORT_CONFIG_RPT_CHANGE, ) + +SENSOR_ACCELERATION = "acceleration" +SENSOR_BATTERY = "battery" +SENSOR_ELECTRICAL_MEASUREMENT = "electrical_measurement" +SENSOR_GENERIC = "generic" +SENSOR_HUMIDITY = "humidity" +SENSOR_ILLUMINANCE = "illuminance" +SENSOR_METERING = "metering" +SENSOR_OCCUPANCY = "occupancy" +SENSOR_OPENING = "opening" +SENSOR_PRESSURE = "pressure" +SENSOR_TEMPERATURE = "temperature" +SENSOR_TYPE = "sensor_type" + +SIGNAL_ATTR_UPDATED = "attribute_updated" +SIGNAL_AVAILABLE = "available" +SIGNAL_MOVE_LEVEL = "move_level" +SIGNAL_REMOVE = "remove" +SIGNAL_SET_LEVEL = "set_level" +SIGNAL_STATE_ATTR = "update_state_attribute" + +UNKNOWN = "unknown" +UNKNOWN_MANUFACTURER = "unk_manufacturer" +UNKNOWN_MODEL = "unk_model" + +ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" +ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" +ZHA_GW_MSG = "zha_gateway_message" +ZHA_GW_MSG_DEVICE_REMOVED = "device_removed" +ZHA_GW_MSG_DEVICE_INFO = "device_info" +ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized" +ZHA_GW_MSG_DEVICE_JOINED = "device_joined" +ZHA_GW_MSG_LOG_OUTPUT = "log_output" +ZHA_GW_MSG_LOG_ENTRY = "log_entry" +ZHA_GW_RADIO = "radio" +ZHA_GW_RADIO_DESCRIPTION = "radio_description" diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py new file mode 100644 index 00000000000..4148cff6ca9 --- /dev/null +++ b/homeassistant/components/zha/core/decorators.py @@ -0,0 +1,37 @@ +"""Decorators for ZHA core registries.""" +from typing import Callable, TypeVar, Union + +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # noqa pylint: disable=invalid-name + + +class DictRegistry(dict): + """Dict Registry of items.""" + + def register( + self, name: Union[int, str], item: Union[str, CALLABLE_T] = None + ) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Return decorator to register item with a specific name.""" + + def decorator(channel: CALLABLE_T) -> CALLABLE_T: + """Register decorated channel or item.""" + if item is None: + self[name] = channel + else: + self[name] = item + return channel + + return decorator + + +class SetRegistry(set): + """Set Registry of items.""" + + def register(self, name: Union[int, str]) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Return decorator to register item with a specific name.""" + + def decorator(channel: CALLABLE_T) -> CALLABLE_T: + """Register decorated channel or item.""" + self.add(name) + return channel + + return decorator diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index a76c580a3f1..1c22b41ce86 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -21,42 +21,44 @@ from .channels import EventRelayChannel from .const import ( ATTR_ARGS, ATTR_ATTRIBUTE, + ATTR_AVAILABLE, ATTR_CLUSTER_ID, ATTR_COMMAND, ATTR_COMMAND_TYPE, ATTR_ENDPOINT_ID, + ATTR_IEEE, + ATTR_LAST_SEEN, + ATTR_LQI, ATTR_MANUFACTURER, + ATTR_MANUFACTURER_CODE, + ATTR_MODEL, + ATTR_NAME, + ATTR_NWK, + ATTR_POWER_SOURCE, + ATTR_QUIRK_APPLIED, + ATTR_QUIRK_CLASS, + ATTR_RSSI, ATTR_VALUE, - BATTERY_OR_UNKNOWN, - CLIENT_COMMANDS, - IEEE, - IN, - MAINS_POWERED, - MANUFACTURER_CODE, - MODEL, - NAME, - NWK, - OUT, - POWER_CONFIGURATION_CHANNEL, - POWER_SOURCE, - QUIRK_APPLIED, - QUIRK_CLASS, - SERVER, - SERVER_COMMANDS, + CHANNEL_BASIC, + CHANNEL_POWER_CONFIGURATION, + CHANNEL_ZDO, + CLUSTER_COMMAND_SERVER, + CLUSTER_COMMANDS_CLIENT, + CLUSTER_COMMANDS_SERVER, + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, + POWER_BATTERY_OR_UNKNOWN, + POWER_MAINS_POWERED, SIGNAL_AVAILABLE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, - ZDO_CHANNEL, - LQI, - RSSI, - LAST_SEEN, - ATTR_AVAILABLE, ) from .helpers import LogMixin _LOGGER = logging.getLogger(__name__) _KEEP_ALIVE_INTERVAL = 7200 _UPDATE_ALIVE_INTERVAL = timedelta(seconds=60) +_CHECKIN_GRACE_PERIODS = 2 class DeviceStatus(Enum): @@ -81,6 +83,7 @@ class ZHADevice(LogMixin): self._available_signal = "{}_{}_{}".format( self.name, self.ieee, SIGNAL_AVAILABLE ) + self._checkins_missed_count = 2 self._unsub = async_dispatcher_connect( self.hass, self._available_signal, self.async_initialize ) @@ -155,7 +158,9 @@ class ZHADevice(LogMixin): @property def power_source(self): """Return the power source for the device.""" - return MAINS_POWERED if self.is_mains_powered else BATTERY_OR_UNKNOWN + return ( + POWER_MAINS_POWERED if self.is_mains_powered else POWER_BATTERY_OR_UNKNOWN + ) @property def is_router(self): @@ -202,9 +207,26 @@ class ZHADevice(LogMixin): else: difference = time.time() - self.last_seen if difference > _KEEP_ALIVE_INTERVAL: - self.update_available(False) + if self._checkins_missed_count < _CHECKIN_GRACE_PERIODS: + self._checkins_missed_count += 1 + if ( + CHANNEL_BASIC in self.cluster_channels + and self.manufacturer != "LUMI" + ): + self.debug( + "Attempting to checkin with device - missed checkins: %s", + self._checkins_missed_count, + ) + self.hass.async_create_task( + self.cluster_channels[CHANNEL_BASIC].get_attribute_value( + ATTR_MANUFACTURER, from_cache=False + ) + ) + else: + self.update_available(False) else: self.update_available(True) + self._checkins_missed_count = 0 def update_available(self, available): """Set sensor availability.""" @@ -223,18 +245,18 @@ class ZHADevice(LogMixin): time_struct = time.localtime(self.last_seen) update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct) return { - IEEE: ieee, - NWK: self.nwk, + ATTR_IEEE: ieee, + ATTR_NWK: self.nwk, ATTR_MANUFACTURER: self.manufacturer, - MODEL: self.model, - NAME: self.name or ieee, - QUIRK_APPLIED: self.quirk_applied, - QUIRK_CLASS: self.quirk_class, - MANUFACTURER_CODE: self.manufacturer_code, - POWER_SOURCE: self.power_source, - LQI: self.lqi, - RSSI: self.rssi, - LAST_SEEN: update_time, + ATTR_MODEL: self.model, + ATTR_NAME: self.name or ieee, + ATTR_QUIRK_APPLIED: self.quirk_applied, + ATTR_QUIRK_CLASS: self.quirk_class, + ATTR_MANUFACTURER_CODE: self.manufacturer_code, + ATTR_POWER_SOURCE: self.power_source, + ATTR_LQI: self.lqi, + ATTR_RSSI: self.rssi, + ATTR_LAST_SEEN: update_time, ATTR_AVAILABLE: self.available, } @@ -242,8 +264,8 @@ class ZHADevice(LogMixin): """Add cluster channel to device.""" # only keep 1 power configuration channel if ( - cluster_channel.name is POWER_CONFIGURATION_CHANNEL - and POWER_CONFIGURATION_CHANNEL in self.cluster_channels + cluster_channel.name is CHANNEL_POWER_CONFIGURATION + and CHANNEL_POWER_CONFIGURATION in self.cluster_channels ): return @@ -318,7 +340,7 @@ class ZHADevice(LogMixin): semaphore = asyncio.Semaphore(3) zdo_task = None for channel in channels: - if channel.name == ZDO_CHANNEL: + if channel.name == CHANNEL_ZDO: # pylint: disable=E1111 if zdo_task is None: # We only want to do this once zdo_task = self._async_create_task( @@ -356,7 +378,10 @@ class ZHADevice(LogMixin): def async_get_clusters(self): """Get all clusters for this device.""" return { - ep_id: {IN: endpoint.in_clusters, OUT: endpoint.out_clusters} + ep_id: { + CLUSTER_TYPE_IN: endpoint.in_clusters, + CLUSTER_TYPE_OUT: endpoint.out_clusters, + } for (ep_id, endpoint) in self._zigpy_device.endpoints.items() if ep_id != 0 } @@ -367,19 +392,24 @@ class ZHADevice(LogMixin): from zigpy.profiles import zha, zll return { - ep_id: {IN: endpoint.in_clusters, OUT: endpoint.out_clusters} + ep_id: { + CLUSTER_TYPE_IN: endpoint.in_clusters, + CLUSTER_TYPE_OUT: endpoint.out_clusters, + } for (ep_id, endpoint) in self._zigpy_device.endpoints.items() if ep_id != 0 and endpoint.profile_id in (zha.PROFILE_ID, zll.PROFILE_ID) } @callback - def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=IN): + def async_get_cluster(self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN): """Get zigbee cluster from this entity.""" clusters = self.async_get_clusters() return clusters[endpoint_id][cluster_type][cluster_id] @callback - def async_get_cluster_attributes(self, endpoint_id, cluster_id, cluster_type=IN): + def async_get_cluster_attributes( + self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN + ): """Get zigbee attributes for specified cluster.""" cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: @@ -387,14 +417,16 @@ class ZHADevice(LogMixin): return cluster.attributes @callback - def async_get_cluster_commands(self, endpoint_id, cluster_id, cluster_type=IN): + def async_get_cluster_commands( + self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN + ): """Get zigbee commands for specified cluster.""" cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) if cluster is None: return None return { - CLIENT_COMMANDS: cluster.client_commands, - SERVER_COMMANDS: cluster.server_commands, + CLUSTER_COMMANDS_CLIENT: cluster.client_commands, + CLUSTER_COMMANDS_SERVER: cluster.server_commands, } async def write_zigbee_attribute( @@ -403,7 +435,7 @@ class ZHADevice(LogMixin): cluster_id, attribute, value, - cluster_type=IN, + cluster_type=CLUSTER_TYPE_IN, manufacturer=None, ): """Write a value to a zigbee attribute for a cluster in this entity.""" @@ -444,7 +476,7 @@ class ZHADevice(LogMixin): command, command_type, args, - cluster_type=IN, + cluster_type=CLUSTER_TYPE_IN, manufacturer=None, ): """Issue a command against specified zigbee cluster on this entity.""" @@ -452,7 +484,7 @@ class ZHADevice(LogMixin): if cluster is None: return None response = None - if command_type == SERVER: + if command_type == CLUSTER_COMMAND_SERVER: response = await cluster.command( command, *args, manufacturer=manufacturer, expect_reply=True ) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 97506494387..c4489164b0c 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -12,28 +12,29 @@ 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 .channels.registry import ZIGBEE_CHANNEL_REGISTRY from .const import ( - CONF_DEVICE_CONFIG, COMPONENTS, - ZHA_DISCOVERY_NEW, + CONF_DEVICE_CONFIG, DATA_ZHA, + SENSOR_GENERIC, SENSOR_TYPE, UNKNOWN, - GENERIC, + ZHA_DISCOVERY_NEW, ) from .registries import ( BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, - EVENT_RELAY_CLUSTERS, - SENSOR_TYPES, - DEVICE_CLASS, COMPONENT_CLUSTERS, - SINGLE_INPUT_CLUSTER_DEVICE_CLASS, - SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS, + 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, ) _LOGGER = logging.getLogger(__name__) @@ -291,7 +292,7 @@ def _async_handle_single_cluster_match( if component == SENSOR: discovery_info.update( - {SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, GENERIC)} + {SENSOR_TYPE: SENSOR_TYPES.get(cluster.cluster_id, SENSOR_GENERIC)} ) if component == BINARY_SENSOR: discovery_info.update( diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 9994c445dbc..9cf93b56581 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -22,50 +22,51 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from ..api import async_get_device_info from .const import ( - ADD_DEVICE_RELAY_LOGGERS, + ATTR_IEEE, ATTR_MANUFACTURER, - BELLOWS, + ATTR_MODEL, + ATTR_NWK, + ATTR_SIGNATURE, + ATTR_TYPE, CONF_BAUDRATE, CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, - CURRENT, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_GATEWAY, + DEBUG_COMP_BELLOWS, + DEBUG_COMP_ZHA, + DEBUG_COMP_ZIGPY, + DEBUG_COMP_ZIGPY_DECONZ, + DEBUG_COMP_ZIGPY_XBEE, + DEBUG_COMP_ZIGPY_ZIGATE, + DEBUG_LEVEL_CURRENT, + DEBUG_LEVEL_ORIGINAL, DEBUG_LEVELS, + DEBUG_RELAY_LOGGERS, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, - DEVICE_FULL_INIT, - DEVICE_INFO, - DEVICE_JOINED, - DEVICE_REMOVED, DOMAIN, - IEEE, - LOG_ENTRY, - LOG_OUTPUT, - MODEL, - NWK, - ORIGINAL, - RADIO, - RADIO_DESCRIPTION, - RAW_INIT, SIGNAL_REMOVE, - SIGNATURE, - TYPE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, - ZHA, ZHA_GW_MSG, - ZIGPY, - ZIGPY_DECONZ, - ZIGPY_XBEE, + ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO, + ZHA_GW_MSG_DEVICE_JOINED, + ZHA_GW_MSG_DEVICE_REMOVED, + ZHA_GW_MSG_LOG_ENTRY, + ZHA_GW_MSG_LOG_OUTPUT, + ZHA_GW_MSG_RAW_INIT, + ZHA_GW_RADIO, + ZHA_GW_RADIO_DESCRIPTION, ) from .device import DeviceStatus, ZHADevice from .discovery import async_dispatch_discovery_info, async_process_endpoint from .patches import apply_application_controller_patch -from .registries import INPUT_BIND_ONLY_CLUSTERS, RADIO_TYPES +from .registries import RADIO_TYPES from .store import async_get_registry _LOGGER = logging.getLogger(__name__) @@ -91,8 +92,8 @@ class ZHAGateway: self.radio_description = None hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self self._log_levels = { - ORIGINAL: async_capture_log_levels(), - CURRENT: async_capture_log_levels(), + DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), + DEBUG_LEVEL_CURRENT: async_capture_log_levels(), } self.debug_enabled = False self._log_relay_handler = LogRelayHandler(hass, self) @@ -107,9 +108,9 @@ class ZHAGateway: baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) radio_type = self._config_entry.data.get(CONF_RADIO_TYPE) - radio_details = RADIO_TYPES[radio_type][RADIO]() - radio = radio_details[RADIO] - self.radio_description = RADIO_TYPES[radio_type][RADIO_DESCRIPTION] + radio_details = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() + radio = radio_details[ZHA_GW_RADIO] + self.radio_description = RADIO_TYPES[radio_type][ZHA_GW_RADIO_DESCRIPTION] await radio.connect(usb_path, baudrate) if CONF_DATABASE in self._config: @@ -126,10 +127,19 @@ class ZHAGateway: ) init_tasks = [] + semaphore = asyncio.Semaphore(2) + + async def init_with_semaphore(coro, semaphore): + """Don't flood the zigbee network during initialization.""" + async with semaphore: + await coro + for device in self.application_controller.devices.values(): if device.nwk == 0x0000: continue - init_tasks.append(self.async_device_initialized(device, False)) + init_tasks.append( + init_with_semaphore(self.async_device_restored(device), semaphore) + ) await asyncio.gather(*init_tasks) def device_joined(self, device): @@ -141,7 +151,11 @@ class ZHAGateway: async_dispatcher_send( self._hass, ZHA_GW_MSG, - {TYPE: DEVICE_JOINED, NWK: device.nwk, IEEE: str(device.ieee)}, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_JOINED, + ATTR_NWK: device.nwk, + ATTR_IEEE: str(device.ieee), + }, ) def raw_device_initialized(self, device): @@ -154,18 +168,18 @@ class ZHAGateway: self._hass, ZHA_GW_MSG, { - TYPE: RAW_INIT, - NWK: device.nwk, - IEEE: str(device.ieee), - MODEL: device.model if device.model else UNKNOWN_MODEL, + ATTR_TYPE: ZHA_GW_MSG_RAW_INIT, + ATTR_NWK: device.nwk, + ATTR_IEEE: str(device.ieee), + ATTR_MODEL: device.model if device.model else UNKNOWN_MODEL, ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER, - SIGNATURE: device.get_signature(), + ATTR_SIGNATURE: device.get_signature(), }, ) def device_initialized(self, device): """Handle device joined and basic information discovered.""" - self._hass.async_create_task(self.async_device_initialized(device, True)) + self._hass.async_create_task(self.async_device_initialized(device)) def device_left(self, device): """Handle device leaving the network.""" @@ -198,7 +212,10 @@ class ZHAGateway: async_dispatcher_send( self._hass, ZHA_GW_MSG, - {TYPE: DEVICE_REMOVED, DEVICE_INFO: device_info}, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_REMOVED, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, ) def get_device(self, ieee): @@ -254,11 +271,11 @@ class ZHAGateway: @callback def async_enable_debug_mode(self): """Enable debug mode for ZHA.""" - self._log_levels[ORIGINAL] = async_capture_log_levels() + self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels() async_set_logger_levels(DEBUG_LEVELS) - self._log_levels[CURRENT] = async_capture_log_levels() + self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() - for logger_name in ADD_DEVICE_RELAY_LOGGERS: + for logger_name in DEBUG_RELAY_LOGGERS: logging.getLogger(logger_name).addHandler(self._log_relay_handler) self.debug_enabled = True @@ -266,14 +283,14 @@ class ZHAGateway: @callback def async_disable_debug_mode(self): """Disable debug mode for ZHA.""" - async_set_logger_levels(self._log_levels[ORIGINAL]) - self._log_levels[CURRENT] = async_capture_log_levels() - for logger_name in ADD_DEVICE_RELAY_LOGGERS: + async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL]) + self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() + for logger_name in DEBUG_RELAY_LOGGERS: logging.getLogger(logger_name).removeHandler(self._log_relay_handler) self.debug_enabled = False @callback - def _async_get_or_create_device(self, zigpy_device, is_new_join): + def _async_get_or_create_device(self, zigpy_device): """Get or create a ZHA device.""" zha_device = self._devices.get(zigpy_device.ieee) if zha_device is None: @@ -287,9 +304,6 @@ class ZHAGateway: manufacturer=zha_device.manufacturer, model=zha_device.model, ) - if not is_new_join: - entry = self.zha_storage.async_get_or_create(zha_device) - zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback @@ -314,51 +328,94 @@ class ZHAGateway: self.zha_storage.async_update(device) await self.zha_storage.async_save() - async def async_device_initialized(self, device, is_new_join): + async def async_device_initialized(self, device): """Handle device joined and basic information discovered (async).""" if device.nwk == 0x0000: return - zha_device = self._async_get_or_create_device(device, is_new_join) + zha_device = self._async_get_or_create_device(device) - is_rejoin = False - if zha_device.status is not DeviceStatus.INITIALIZED: - discovery_infos = [] - for endpoint_id, endpoint in device.endpoints.items(): - async_process_endpoint( - self._hass, - self._config, - endpoint_id, - endpoint, - discovery_infos, - device, - zha_device, - is_new_join, - ) - if endpoint_id != 0: - for cluster in endpoint.in_clusters.values(): - cluster.bind_only = ( - cluster.cluster_id in INPUT_BIND_ONLY_CLUSTERS - ) - for cluster in endpoint.out_clusters.values(): - # output clusters are always bind only - cluster.bind_only = True - else: - is_rejoin = is_new_join is True + _LOGGER.debug( + "device - %s entering async_device_initialized - is_new_join: %s", + "0x{:04x}:{}".format(device.nwk, device.ieee), + zha_device.status is not DeviceStatus.INITIALIZED, + ) + + if zha_device.status is DeviceStatus.INITIALIZED: + # ZHA already has an initialized device so either the device was assigned a + # new nwk or device was physically reset and added again without being removed _LOGGER.debug( - "skipping discovery for previously discovered device: %s", - "{} - is rejoin: {}".format(zha_device.ieee, is_rejoin), + "device - %s has been reset and readded or its nwk address changed", + "0x{:04x}:{}".format(device.nwk, device.ieee), + ) + await self._async_device_rejoined(zha_device) + else: + _LOGGER.debug( + "device - %s has joined the ZHA zigbee network", + "0x{:04x}:{}".format(device.nwk, device.ieee), + ) + await self._async_device_joined(device, zha_device) + + # This is real traffic from a device so lets update last seen on the entry + entry = self.zha_storage.async_get_or_create(zha_device) + zha_device.async_update_last_seen(entry.last_seen) + + device_info = async_get_device_info( + self._hass, zha_device, self.ha_device_registry + ) + async_dispatcher_send( + self._hass, + ZHA_GW_MSG, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, + ) + + async def _async_device_joined(self, device, zha_device): + discovery_infos = [] + for endpoint_id, endpoint in device.endpoints.items(): + async_process_endpoint( + self._hass, + self._config, + endpoint_id, + endpoint, + discovery_infos, + device, + zha_device, + True, ) - if is_new_join: - # configure the device - await zha_device.async_configure() - zha_device.update_available(True) - elif zha_device.is_mains_powered: + await zha_device.async_configure() + # will cause async_init to fire so don't explicitly call it + zha_device.update_available(True) + + for discovery_info in discovery_infos: + async_dispatch_discovery_info(self._hass, True, discovery_info) + + # only public for testing + async def async_device_restored(self, device): + """Add an existing device to the ZHA zigbee network when ZHA first starts.""" + zha_device = self._async_get_or_create_device(device) + discovery_infos = [] + for endpoint_id, endpoint in device.endpoints.items(): + async_process_endpoint( + self._hass, + self._config, + endpoint_id, + endpoint, + discovery_infos, + device, + zha_device, + False, + ) + + if zha_device.is_mains_powered: # the device isn't a battery powered device so we should be able # to update it now _LOGGER.debug( - "attempting to request fresh state for %s %s", + "attempting to request fresh state for device - %s %s %s", + "0x{:04x}:{}".format(zha_device.nwk, zha_device.ieee), zha_device.name, "with power source: {}".format(zha_device.power_source), ) @@ -366,19 +423,18 @@ class ZHAGateway: else: await zha_device.async_initialize(from_cache=True) - if not is_rejoin: - for discovery_info in discovery_infos: - async_dispatch_discovery_info(self._hass, is_new_join, discovery_info) + for discovery_info in discovery_infos: + async_dispatch_discovery_info(self._hass, False, discovery_info) - if is_new_join: - device_info = async_get_device_info( - self._hass, zha_device, self.ha_device_registry - ) - async_dispatcher_send( - self._hass, - ZHA_GW_MSG, - {TYPE: DEVICE_FULL_INIT, DEVICE_INFO: device_info}, - ) + async def _async_device_rejoined(self, zha_device): + _LOGGER.debug( + "skipping discovery for previously discovered device - %s", + "0x{:04x}:{}".format(zha_device.nwk, zha_device.ieee), + ) + # we don't have to do this on a nwk swap but we don't have a way to tell currently + await zha_device.async_configure() + # will cause async_init to fire so don't explicitly call it + zha_device.update_available(True) async def shutdown(self): """Stop ZHA Controller Application.""" @@ -390,22 +446,30 @@ class ZHAGateway: def async_capture_log_levels(): """Capture current logger levels for ZHA.""" return { - BELLOWS: logging.getLogger(BELLOWS).getEffectiveLevel(), - ZHA: logging.getLogger(ZHA).getEffectiveLevel(), - ZIGPY: logging.getLogger(ZIGPY).getEffectiveLevel(), - ZIGPY_XBEE: logging.getLogger(ZIGPY_XBEE).getEffectiveLevel(), - ZIGPY_DECONZ: logging.getLogger(ZIGPY_DECONZ).getEffectiveLevel(), + DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(), + DEBUG_COMP_ZHA: logging.getLogger(DEBUG_COMP_ZHA).getEffectiveLevel(), + DEBUG_COMP_ZIGPY: logging.getLogger(DEBUG_COMP_ZIGPY).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_XBEE: logging.getLogger( + DEBUG_COMP_ZIGPY_XBEE + ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger( + DEBUG_COMP_ZIGPY_DECONZ + ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_ZIGATE: logging.getLogger( + DEBUG_COMP_ZIGPY_ZIGATE + ).getEffectiveLevel(), } @callback def async_set_logger_levels(levels): """Set logger levels for ZHA.""" - logging.getLogger(BELLOWS).setLevel(levels[BELLOWS]) - logging.getLogger(ZHA).setLevel(levels[ZHA]) - logging.getLogger(ZIGPY).setLevel(levels[ZIGPY]) - logging.getLogger(ZIGPY_XBEE).setLevel(levels[ZIGPY_XBEE]) - logging.getLogger(ZIGPY_DECONZ).setLevel(levels[ZIGPY_DECONZ]) + logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS]) + logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA]) + logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY]) + logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) + logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ]) + logging.getLogger(DEBUG_COMP_ZIGPY_ZIGATE).setLevel(levels[DEBUG_COMP_ZIGPY_ZIGATE]) class LogRelayHandler(logging.Handler): @@ -426,5 +490,7 @@ class LogRelayHandler(logging.Handler): entry = LogEntry(record, stack, _figure_out_source(record, stack, self.hass)) async_dispatcher_send( - self.hass, ZHA_GW_MSG, {TYPE: LOG_OUTPUT, LOG_ENTRY: entry.to_dict()} + self.hass, + ZHA_GW_MSG, + {ATTR_TYPE: ZHA_GW_MSG_LOG_OUTPUT, ZHA_GW_MSG_LOG_ENTRY: entry.to_dict()}, ) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 3a053d7be01..37bc6c7a2c1 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -7,17 +7,10 @@ https://home-assistant.io/components/zha/ import asyncio import collections import logging -from concurrent.futures import TimeoutError as Timeout + from homeassistant.core import callback -from .const import ( - DEFAULT_BAUDRATE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_RPT_CHANGE, - RadioType, - IN, - OUT, -) + +from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DEFAULT_BAUDRATE, RadioType from .registries import BINDABLE_CLUSTERS _LOGGER = logging.getLogger(__name__) @@ -46,104 +39,6 @@ async def safe_read( return {} -async def bind_cluster(entity_id, cluster): - """Bind a zigbee cluster. - - This also swallows DeliveryError exceptions that are thrown when devices - are unreachable. - """ - from zigpy.exceptions import DeliveryError - - cluster_name = cluster.ep_attribute - try: - res = await cluster.bind() - _LOGGER.debug("%s: bound '%s' cluster: %s", entity_id, cluster_name, res[0]) - except (DeliveryError, Timeout) as ex: - _LOGGER.debug( - "%s: Failed to bind '%s' cluster: %s", entity_id, cluster_name, str(ex) - ) - - -async def configure_reporting( - entity_id, - cluster, - attr, - min_report=REPORT_CONFIG_MIN_INT, - max_report=REPORT_CONFIG_MAX_INT, - reportable_change=REPORT_CONFIG_RPT_CHANGE, - manufacturer=None, -): - """Configure attribute reporting for a cluster. - - This also swallows DeliveryError exceptions that are thrown when devices - are unreachable. - """ - from zigpy.exceptions import DeliveryError - - attr_name = cluster.attributes.get(attr, [attr])[0] - - if isinstance(attr, str): - attr_id = get_attr_id_by_name(cluster, attr_name) - else: - attr_id = attr - - cluster_name = cluster.ep_attribute - kwargs = {} - if manufacturer: - kwargs["manufacturer"] = manufacturer - try: - res = await cluster.configure_reporting( - attr_id, min_report, max_report, reportable_change, **kwargs - ) - _LOGGER.debug( - "%s: reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", - entity_id, - attr_name, - cluster_name, - min_report, - max_report, - reportable_change, - res, - ) - except (DeliveryError, Timeout) as ex: - _LOGGER.debug( - "%s: failed to set reporting for '%s' attr on '%s' cluster: %s", - entity_id, - attr_name, - cluster_name, - str(ex), - ) - - -async def bind_configure_reporting( - entity_id, - cluster, - attr, - skip_bind=False, - min_report=REPORT_CONFIG_MIN_INT, - max_report=REPORT_CONFIG_MAX_INT, - reportable_change=REPORT_CONFIG_RPT_CHANGE, - manufacturer=None, -): - """Bind and configure zigbee attribute reporting for a cluster. - - This also swallows DeliveryError exceptions that are thrown when devices - are unreachable. - """ - if not skip_bind: - await bind_cluster(entity_id, cluster) - - await configure_reporting( - entity_id, - cluster, - attr, - min_report=min_report, - max_report=max_report, - reportable_change=reportable_change, - manufacturer=manufacturer, - ) - - async def check_zigpy_connection(usb_path, radio_type, database_path): """Test zigpy radio connection.""" if radio_type == RadioType.ezsp.name: @@ -161,6 +56,11 @@ async def check_zigpy_connection(usb_path, radio_type, database_path): from zigpy_deconz.zigbee.application import ControllerApplication radio = zigpy_deconz.api.Deconz() + elif radio_type == RadioType.zigate.name: + import zigpy_zigate.api + from zigpy_zigate.zigbee.application import ControllerApplication + + radio = zigpy_zigate.api.ZiGate() try: await radio.connect(usb_path, DEFAULT_BAUDRATE) controller = ControllerApplication(radio, database_path) @@ -180,13 +80,6 @@ def convert_ieee(ieee_str): return EUI64([uint8_t(p, base=16) for p in ieee_str.split(":")]) -def construct_unique_id(cluster): - """Construct a unique id from a cluster.""" - return "0x{:04x}:{}:0x{:04x}".format( - cluster.endpoint.device.nwk, cluster.endpoint.endpoint_id, cluster.cluster_id - ) - - def get_attr_id_by_name(cluster, attr_name): """Get the attribute id for a cluster attribute by its name.""" return next( @@ -206,14 +99,18 @@ async def get_matched_clusters(source_zha_device, target_zha_device): clusters_to_bind = [] for endpoint_id in source_clusters: - for cluster_id in source_clusters[endpoint_id][OUT]: + for cluster_id in source_clusters[endpoint_id][CLUSTER_TYPE_OUT]: if cluster_id not in BINDABLE_CLUSTERS: continue for t_endpoint_id in target_clusters: - if cluster_id in target_clusters[t_endpoint_id][IN]: + if cluster_id in target_clusters[t_endpoint_id][CLUSTER_TYPE_IN]: cluster_pair = ClusterPair( - source_cluster=source_clusters[endpoint_id][OUT][cluster_id], - target_cluster=target_clusters[t_endpoint_id][IN][cluster_id], + source_cluster=source_clusters[endpoint_id][CLUSTER_TYPE_OUT][ + cluster_id + ], + target_cluster=target_clusters[t_endpoint_id][CLUSTER_TYPE_IN][ + cluster_id + ], ) clusters_to_bind.append(cluster_pair) return clusters_to_bind @@ -225,13 +122,12 @@ def async_is_bindable_target(source_zha_device, target_zha_device): source_clusters = source_zha_device.async_get_std_clusters() target_clusters = target_zha_device.async_get_std_clusters() - bindables = set(BINDABLE_CLUSTERS) for endpoint_id in source_clusters: for t_endpoint_id in target_clusters: - matches = set(source_clusters[endpoint_id][OUT].keys()).intersection( - target_clusters[t_endpoint_id][IN].keys() - ) - if any(bindable in bindables for bindable in matches): + matches = set( + source_clusters[endpoint_id][CLUSTER_TYPE_OUT].keys() + ).intersection(target_clusters[t_endpoint_id][CLUSTER_TYPE_IN].keys()) + if any(bindable in BINDABLE_CLUSTERS for bindable in matches): return True return False diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index e0cfbeeee7b..db7e89dce82 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -4,6 +4,7 @@ Mapping registries for Zigbee Home Automation. For more details about this component, please refer to the documentation at https://home-assistant.io/components/zha/ """ +import collections from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER @@ -14,58 +15,57 @@ from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from .const import ( - HUMIDITY, - TEMPERATURE, - ILLUMINANCE, - PRESSURE, - METERING, - ELECTRICAL_MEASUREMENT, - OCCUPANCY, - REPORT_CONFIG_IMMEDIATE, - OPENING, - ZONE, - RADIO_DESCRIPTION, - REPORT_CONFIG_ASAP, - REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_OP, - ACCELERATION, - RadioType, - RADIO, CONTROLLER, - BATTERY, + 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 -SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 -SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 -SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 - -DEVICE_CLASS = {} -SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {} -SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {} -SENSOR_TYPES = {} -RADIO_TYPES = {} +BINARY_SENSOR_CLUSTERS = SetRegistry() BINARY_SENSOR_TYPES = {} -REMOTE_DEVICE_TYPES = {} +BINDABLE_CLUSTERS = SetRegistry() +CHANNEL_ONLY_CLUSTERS = SetRegistry() CLUSTER_REPORT_CONFIGS = {} CUSTOM_CLUSTER_MAPPINGS = {} -EVENT_RELAY_CLUSTERS = [] -CHANNEL_ONLY_CLUSTERS = [] -OUTPUT_CHANNEL_ONLY_CLUSTERS = [] -BINDABLE_CLUSTERS = [] -INPUT_BIND_ONLY_CLUSTERS = [] -BINARY_SENSOR_CLUSTERS = set() -DEVICE_TRACKER_CLUSTERS = set() -LIGHT_CLUSTERS = set() -SWITCH_CLUSTERS = set() +DEVICE_CLASS = collections.defaultdict(dict) +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 + COMPONENT_CLUSTERS = { BINARY_SENSOR: BINARY_SENSOR_CLUSTERS, + DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS, LIGHT: LIGHT_CLUSTERS, SWITCH: SWITCH_CLUSTERS, - DEVICE_TRACKER: DEVICE_TRACKER_CLUSTERS, } +ZIGBEE_CHANNEL_REGISTRY = DictRegistry() + +# importing channels updates registries +from . import channels # noqa pylint: disable=wrong-import-position,unused-import + def establish_device_mappings(): """Establish mappings between ZCL objects and HA ZHA objects. @@ -76,113 +76,117 @@ def establish_device_mappings(): from zigpy import zcl from zigpy.profiles import zha, zll - if zha.PROFILE_ID not in DEVICE_CLASS: - DEVICE_CLASS[zha.PROFILE_ID] = {} - if zll.PROFILE_ID not in DEVICE_CLASS: - DEVICE_CLASS[zll.PROFILE_ID] = {} - - if zha.PROFILE_ID not in REMOTE_DEVICE_TYPES: - REMOTE_DEVICE_TYPES[zha.PROFILE_ID] = [] - if zll.PROFILE_ID not in REMOTE_DEVICE_TYPES: - REMOTE_DEVICE_TYPES[zll.PROFILE_ID] = [] - def get_ezsp_radio(): import bellows.ezsp from bellows.zigbee.application import ControllerApplication - return {RADIO: bellows.ezsp.EZSP(), CONTROLLER: ControllerApplication} + return {ZHA_GW_RADIO: bellows.ezsp.EZSP(), CONTROLLER: ControllerApplication} RADIO_TYPES[RadioType.ezsp.name] = { - RADIO: get_ezsp_radio, - RADIO_DESCRIPTION: "EZSP", - } - - def get_xbee_radio(): - import zigpy_xbee.api - from zigpy_xbee.zigbee.application import ControllerApplication - - return {RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication} - - RADIO_TYPES[RadioType.xbee.name] = { - RADIO: get_xbee_radio, - RADIO_DESCRIPTION: "XBee", + ZHA_GW_RADIO: get_ezsp_radio, + ZHA_GW_RADIO_DESCRIPTION: "EZSP", } def get_deconz_radio(): import zigpy_deconz.api from zigpy_deconz.zigbee.application import ControllerApplication - return {RADIO: zigpy_deconz.api.Deconz(), CONTROLLER: ControllerApplication} + return { + ZHA_GW_RADIO: zigpy_deconz.api.Deconz(), + CONTROLLER: ControllerApplication, + } RADIO_TYPES[RadioType.deconz.name] = { - RADIO: get_deconz_radio, - RADIO_DESCRIPTION: "Deconz", + ZHA_GW_RADIO: get_deconz_radio, + ZHA_GW_RADIO_DESCRIPTION: "Deconz", } - EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) - EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) + def get_xbee_radio(): + import zigpy_xbee.api + from zigpy_xbee.zigbee.application import ControllerApplication - CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id) - CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) + return {ZHA_GW_RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication} - OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.cluster_id) + RADIO_TYPES[RadioType.xbee.name] = { + ZHA_GW_RADIO: get_xbee_radio, + ZHA_GW_RADIO_DESCRIPTION: "XBee", + } - BINDABLE_CLUSTERS.append(zcl.clusters.general.LevelControl.cluster_id) - BINDABLE_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) - BINDABLE_CLUSTERS.append(zcl.clusters.lighting.Color.cluster_id) + def get_zigate_radio(): + import zigpy_zigate.api + from zigpy_zigate.zigbee.application import ControllerApplication - INPUT_BIND_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) + return { + ZHA_GW_RADIO: zigpy_zigate.api.ZiGate(), + CONTROLLER: ControllerApplication, + } + + RADIO_TYPES[RadioType.zigate.name] = { + ZHA_GW_RADIO: get_zigate_radio, + 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, + } + ) DEVICE_CLASS[zha.PROFILE_ID].update( { - zha.DeviceType.SMART_PLUG: SWITCH, - zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, - zha.DeviceType.ON_OFF_LIGHT: LIGHT, - zha.DeviceType.DIMMABLE_LIGHT: LIGHT, - zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, - zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, - zha.DeviceType.ON_OFF_BALLAST: SWITCH, - zha.DeviceType.DIMMABLE_BALLAST: LIGHT, - zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, - zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, - zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, + zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, + zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zha.DeviceType.DIMMABLE_BALLAST: LIGHT, + zha.DeviceType.DIMMABLE_LIGHT: LIGHT, + zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, + zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zha.DeviceType.ON_OFF_BALLAST: SWITCH, + zha.DeviceType.ON_OFF_LIGHT: LIGHT, + zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, + zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, + zha.DeviceType.SMART_PLUG: SWITCH, } ) DEVICE_CLASS[zll.PROFILE_ID].update( { - zll.DeviceType.ON_OFF_LIGHT: LIGHT, - zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, + zll.DeviceType.COLOR_LIGHT: LIGHT, + zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, zll.DeviceType.DIMMABLE_LIGHT: LIGHT, zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, - zll.DeviceType.COLOR_LIGHT: LIGHT, zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zll.DeviceType.ON_OFF_LIGHT: LIGHT, + zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, } ) SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update( { - zcl.clusters.general.OnOff: SWITCH, - zcl.clusters.measurement.RelativeHumidity: SENSOR, # this works for now but if we hit conflicts we can break it out to # a different dict that is keyed by manufacturer - SMARTTHINGS_HUMIDITY_CLUSTER: SENSOR, - zcl.clusters.measurement.TemperatureMeasurement: SENSOR, - zcl.clusters.measurement.PressureMeasurement: SENSOR, - zcl.clusters.measurement.IlluminanceMeasurement: SENSOR, - zcl.clusters.smartenergy.Metering: SENSOR, - zcl.clusters.homeautomation.ElectricalMeasurement: SENSOR, - zcl.clusters.security.IasZone: BINARY_SENSOR, - zcl.clusters.measurement.OccupancySensing: BINARY_SENSOR, - zcl.clusters.hvac.Fan: FAN, SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR, - zcl.clusters.general.MultistateInput.cluster_id: SENSOR, - zcl.clusters.general.AnalogInput.cluster_id: 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, } ) @@ -192,134 +196,30 @@ def establish_device_mappings(): SENSOR_TYPES.update( { - zcl.clusters.measurement.RelativeHumidity.cluster_id: HUMIDITY, - SMARTTHINGS_HUMIDITY_CLUSTER: HUMIDITY, - zcl.clusters.measurement.TemperatureMeasurement.cluster_id: TEMPERATURE, - zcl.clusters.measurement.PressureMeasurement.cluster_id: PRESSURE, - zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: ILLUMINANCE, - zcl.clusters.smartenergy.Metering.cluster_id: METERING, - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: ELECTRICAL_MEASUREMENT, - zcl.clusters.general.PowerConfiguration.cluster_id: BATTERY, - } - ) - - BINARY_SENSOR_TYPES.update( - { - zcl.clusters.measurement.OccupancySensing.cluster_id: OCCUPANCY, - zcl.clusters.security.IasZone.cluster_id: ZONE, - zcl.clusters.general.OnOff.cluster_id: OPENING, - SMARTTHINGS_ACCELERATION_CLUSTER: ACCELERATION, + 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, } ) zhap = zha.PROFILE_ID - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_SCENE_CONTROLLER) REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.REMOTE_CONTROL) REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.SCENE_SELECTOR) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH) zllp = zll.PROFILE_ID REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_CONTROLLER) REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROL_BRIDGE) REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROLLER) REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROL_BRIDGE) - - CLUSTER_REPORT_CONFIGS.update( - { - zcl.clusters.general.Alarms.cluster_id: [], - zcl.clusters.general.Basic.cluster_id: [], - zcl.clusters.general.Commissioning.cluster_id: [], - zcl.clusters.general.Identify.cluster_id: [], - zcl.clusters.general.Groups.cluster_id: [], - zcl.clusters.general.Scenes.cluster_id: [], - zcl.clusters.general.Partition.cluster_id: [], - zcl.clusters.general.Ota.cluster_id: [], - zcl.clusters.general.PowerProfile.cluster_id: [], - zcl.clusters.general.ApplianceControl.cluster_id: [], - zcl.clusters.general.PollControl.cluster_id: [], - zcl.clusters.general.GreenPowerProxy.cluster_id: [], - zcl.clusters.general.OnOffConfiguration.cluster_id: [], - zcl.clusters.lightlink.LightLink.cluster_id: [], - zcl.clusters.general.OnOff.cluster_id: [ - {"attr": "on_off", "config": REPORT_CONFIG_IMMEDIATE} - ], - zcl.clusters.general.LevelControl.cluster_id: [ - {"attr": "current_level", "config": REPORT_CONFIG_ASAP} - ], - zcl.clusters.lighting.Color.cluster_id: [ - {"attr": "current_x", "config": REPORT_CONFIG_DEFAULT}, - {"attr": "current_y", "config": REPORT_CONFIG_DEFAULT}, - {"attr": "color_temperature", "config": REPORT_CONFIG_DEFAULT}, - ], - zcl.clusters.measurement.RelativeHumidity.cluster_id: [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ], - zcl.clusters.measurement.TemperatureMeasurement.cluster_id: [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ], - SMARTTHINGS_ACCELERATION_CLUSTER: [ - {"attr": "acceleration", "config": REPORT_CONFIG_ASAP}, - {"attr": "x_axis", "config": REPORT_CONFIG_ASAP}, - {"attr": "y_axis", "config": REPORT_CONFIG_ASAP}, - {"attr": "z_axis", "config": REPORT_CONFIG_ASAP}, - ], - SMARTTHINGS_HUMIDITY_CLUSTER: [ - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - } - ], - zcl.clusters.measurement.PressureMeasurement.cluster_id: [ - {"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT} - ], - zcl.clusters.measurement.IlluminanceMeasurement.cluster_id: [ - {"attr": "measured_value", "config": REPORT_CONFIG_DEFAULT} - ], - zcl.clusters.smartenergy.Metering.cluster_id: [ - {"attr": "instantaneous_demand", "config": REPORT_CONFIG_DEFAULT} - ], - zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: [ - {"attr": "active_power", "config": REPORT_CONFIG_DEFAULT} - ], - zcl.clusters.general.PowerConfiguration.cluster_id: [ - {"attr": "battery_voltage", "config": REPORT_CONFIG_DEFAULT}, - { - "attr": "battery_percentage_remaining", - "config": REPORT_CONFIG_DEFAULT, - }, - ], - zcl.clusters.measurement.OccupancySensing.cluster_id: [ - {"attr": "occupancy", "config": REPORT_CONFIG_IMMEDIATE} - ], - zcl.clusters.hvac.Fan.cluster_id: [ - {"attr": "fan_mode", "config": REPORT_CONFIG_OP} - ], - zcl.clusters.closures.DoorLock.cluster_id: [ - {"attr": "lock_state", "config": REPORT_CONFIG_IMMEDIATE} - ], - } - ) - - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.security.IasZone.cluster_id) - BINARY_SENSOR_CLUSTERS.add(zcl.clusters.measurement.OccupancySensing.cluster_id) - BINARY_SENSOR_CLUSTERS.add(SMARTTHINGS_ACCELERATION_CLUSTER) - - DEVICE_TRACKER_CLUSTERS.add(zcl.clusters.general.PowerConfiguration.cluster_id) - - LIGHT_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) - LIGHT_CLUSTERS.add(zcl.clusters.general.LevelControl.cluster_id) - LIGHT_CLUSTERS.add(zcl.clusters.lighting.Color.cluster_id) - - SWITCH_CLUSTERS.add(zcl.clusters.general.OnOff.cluster_id) diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 312cfc7e545..85b4261e4ec 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -1,16 +1,15 @@ """Data storage helper for ZHA.""" -import logging -from collections import OrderedDict - # pylint: disable=W0611 +from collections import OrderedDict +import logging from typing import MutableMapping # noqa: F401 from typing import cast import attr from homeassistant.core import callback -from homeassistant.loader import bind_hass from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 6b881ff7a7d..60a1f6c3c40 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -1,16 +1,18 @@ """Support for the ZHA platform.""" import logging import time -from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER, DOMAIN + +from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER from homeassistant.components.device_tracker.config_entry import ScannerEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( + CHANNEL_POWER_CONFIGURATION, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - POWER_CONFIGURATION_CHANNEL, SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity from .sensor import battery_percentage_remaining_formatter @@ -56,7 +58,7 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): def __init__(self, **kwargs): """Initialize the ZHA device tracker.""" super().__init__(**kwargs) - self._battery_channel = self.cluster_channels.get(POWER_CONFIGURATION_CHANNEL) + self._battery_channel = self.cluster_channels.get(CHANNEL_POWER_CONFIGURATION) self._connected = False self._keepalive_interval = 60 self._should_poll = True diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 46cb7583ef3..694f7b25695 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -13,11 +13,11 @@ from homeassistant.util import slugify from .core.const import ( ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, - MODEL, - NAME, SIGNAL_REMOVE, ) from .core.helpers import LogMixin @@ -99,8 +99,8 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): "connections": {(CONNECTION_ZIGBEE, ieee)}, "identifiers": {(DOMAIN, ieee)}, ATTR_MANUFACTURER: zha_device_info[ATTR_MANUFACTURER], - MODEL: zha_device_info[MODEL], - NAME: zha_device_info[NAME], + ATTR_MODEL: zha_device_info[ATTR_MODEL], + ATTR_NAME: zha_device_info[ATTR_NAME], "via_device": (DOMAIN, self.hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID]), } diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 318e2962e54..1f119ef6657 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,7 +1,6 @@ """Fans on Zigbee Home Automation networks.""" import logging -from homeassistant.core import callback from homeassistant.components.fan import ( DOMAIN, SPEED_HIGH, @@ -11,13 +10,15 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( + CHANNEL_FAN, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - FAN_CHANNEL, SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -91,7 +92,7 @@ class ZhaFan(ZhaEntity, FanEntity): def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._fan_channel = self.cluster_channels.get(FAN_CHANNEL) + self._fan_channel = self.cluster_channels.get(CHANNEL_FAN) async def async_added_to_hass(self): """Run when about to be added to hass.""" diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 64b1897bb3a..379f69febbb 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -3,21 +3,23 @@ from datetime import timedelta import logging from zigpy.zcl.foundation import Status + from homeassistant.components import light from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util + from .core.const import ( + CHANNEL_COLOR, + CHANNEL_LEVEL, + CHANNEL_ON_OFF, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - COLOR_CHANNEL, - ON_OFF_CHANNEL, - LEVEL_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_SET_LEVEL, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -83,9 +85,9 @@ class Light(ZhaEntity, light.Light): self._color_temp = None self._hs_color = None self._brightness = None - self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) - self._level_channel = self.cluster_channels.get(LEVEL_CHANNEL) - self._color_channel = self.cluster_channels.get(COLOR_CHANNEL) + self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) + self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) if self._level_channel: self._supported_features |= light.SUPPORT_BRIGHTNESS diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index f3c6326d32d..afc4618343c 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -2,20 +2,22 @@ import logging from zigpy.zcl.foundation import Status -from homeassistant.core import callback + from homeassistant.components.lock import ( DOMAIN, - STATE_UNLOCKED, STATE_LOCKED, + STATE_UNLOCKED, LockDevice, ) +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( + CHANNEL_DOORLOCK, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - DOORLOCK_CHANNEL, SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -73,7 +75,7 @@ class ZhaDoorLock(ZhaEntity, LockDevice): def __init__(self, unique_id, zha_device, channels, **kwargs): """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._doorlock_channel = self.cluster_channels.get(DOORLOCK_CHANNEL) + self._doorlock_channel = self.cluster_channels.get(CHANNEL_DOORLOCK) async def async_added_to_hass(self): """Run when about to be added to hass.""" diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 88c5f171116..8e7de41e626 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,11 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/zha", "requirements": [ - "bellows-homeassistant==0.9.0", - "zha-quirks==0.0.20", - "zigpy-deconz==0.2.1", - "zigpy-homeassistant==0.7.0", - "zigpy-xbee-homeassistant==0.4.0" + "bellows-homeassistant==0.9.1", + "zha-quirks==0.0.22", + "zigpy-deconz==0.2.2", + "zigpy-homeassistant==0.7.1", + "zigpy-xbee-homeassistant==0.4.0", + "zigpy-zigate==0.1.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index fe0096dd150..e38acebb22c 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -2,37 +2,38 @@ import logging import numbers -from homeassistant.core import callback from homeassistant.components.sensor import ( - DOMAIN, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, - DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_PRESSURE, DEVICE_CLASS_POWER, - DEVICE_CLASS_BATTERY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DOMAIN, ) -from homeassistant.const import TEMP_CELSIUS, POWER_WATT, ATTR_UNIT_OF_MEASUREMENT +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( + CHANNEL_ATTRIBUTE, + CHANNEL_ELECTRICAL_MEASUREMENT, + CHANNEL_POWER_CONFIGURATION, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - HUMIDITY, - TEMPERATURE, - ILLUMINANCE, - PRESSURE, - METERING, - ELECTRICAL_MEASUREMENT, - GENERIC, + SENSOR_BATTERY, + SENSOR_ELECTRICAL_MEASUREMENT, + SENSOR_GENERIC, + SENSOR_HUMIDITY, + SENSOR_ILLUMINANCE, + SENSOR_METERING, + SENSOR_PRESSURE, + SENSOR_TEMPERATURE, SENSOR_TYPE, - ATTRIBUTE_CHANNEL, - ELECTRICAL_MEASUREMENT_CHANNEL, SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, UNKNOWN, - BATTERY, - POWER_CONFIGURATION_CHANNEL, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -121,49 +122,49 @@ async def async_battery_device_state_attr_provider(channel): FORMATTER_FUNC_REGISTRY = { - HUMIDITY: humidity_formatter, - TEMPERATURE: temperature_formatter, - PRESSURE: pressure_formatter, - ELECTRICAL_MEASUREMENT: active_power_formatter, - ILLUMINANCE: illuminance_formatter, - GENERIC: pass_through_formatter, - BATTERY: battery_percentage_remaining_formatter, + 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 = { - HUMIDITY: "%", - TEMPERATURE: TEMP_CELSIUS, - PRESSURE: "hPa", - ILLUMINANCE: "lx", - METERING: POWER_WATT, - ELECTRICAL_MEASUREMENT: POWER_WATT, - GENERIC: None, - BATTERY: "%", + SENSOR_HUMIDITY: "%", + SENSOR_TEMPERATURE: TEMP_CELSIUS, + SENSOR_PRESSURE: "hPa", + SENSOR_ILLUMINANCE: "lx", + SENSOR_METERING: POWER_WATT, + SENSOR_ELECTRICAL_MEASUREMENT: POWER_WATT, + SENSOR_GENERIC: None, + SENSOR_BATTERY: "%", } CHANNEL_REGISTRY = { - ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL, - BATTERY: POWER_CONFIGURATION_CHANNEL, + SENSOR_ELECTRICAL_MEASUREMENT: CHANNEL_ELECTRICAL_MEASUREMENT, + SENSOR_BATTERY: CHANNEL_POWER_CONFIGURATION, } -POLLING_REGISTRY = {ELECTRICAL_MEASUREMENT: True} +POLLING_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: True} -FORCE_UPDATE_REGISTRY = {ELECTRICAL_MEASUREMENT: False} +FORCE_UPDATE_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: False} DEVICE_CLASS_REGISTRY = { UNKNOWN: None, - HUMIDITY: DEVICE_CLASS_HUMIDITY, - TEMPERATURE: DEVICE_CLASS_TEMPERATURE, - PRESSURE: DEVICE_CLASS_PRESSURE, - ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE, - METERING: DEVICE_CLASS_POWER, - ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER, - BATTERY: DEVICE_CLASS_BATTERY, + 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 = { - BATTERY: async_battery_device_state_attr_provider + SENSOR_BATTERY: async_battery_device_state_attr_provider } @@ -217,7 +218,7 @@ class Sensor(ZhaEntity): 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, GENERIC) + self._sensor_type = kwargs.get(SENSOR_TYPE, SENSOR_GENERIC) self._unit = UNIT_REGISTRY.get(self._sensor_type) self._formatter_function = FORMATTER_FUNC_REGISTRY.get( self._sensor_type, pass_through_formatter @@ -225,7 +226,7 @@ class Sensor(ZhaEntity): self._force_update = FORCE_UPDATE_REGISTRY.get(self._sensor_type, False) self._should_poll = POLLING_REGISTRY.get(self._sensor_type, False) self._channel = self.cluster_channels.get( - CHANNEL_REGISTRY.get(self._sensor_type, ATTRIBUTE_CHANNEL) + CHANNEL_REGISTRY.get(self._sensor_type, CHANNEL_ATTRIBUTE) ) self._device_class = DEVICE_CLASS_REGISTRY.get(self._sensor_type, None) self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get( diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index b6d7948c0b3..e1ed6a678e3 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,21 +1,20 @@ { - "config": { - "title": "ZHA", - "step": { - "user": { + "config": { "title": "ZHA", - "description": "", - "data": { - "usb_path": "USB Device Path", - "radio_type": "Radio Type" + "step": { + "user": { + "title": "ZHA", + "data": { + "radio_type": "Radio Type", + "usb_path": "USB Device Path" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to ZHA device." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of ZHA is allowed." } - } - }, - "abort": { - "single_instance_allowed": "Only a single configuration of ZHA is allowed." - }, - "error": { - "cannot_connect": "Unable to connect to ZHA device." } - } } \ No newline at end of file diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 853a50993ab..bfe816d614a 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -2,16 +2,18 @@ import logging from zigpy.zcl.foundation import Status + from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .core.const import ( + CHANNEL_ON_OFF, DATA_ZHA, DATA_ZHA_DISPATCHERS, - ZHA_DISCOVERY_NEW, - ON_OFF_CHANNEL, SIGNAL_ATTR_UPDATED, + ZHA_DISCOVERY_NEW, ) from .entity import ZhaEntity @@ -63,7 +65,7 @@ class Switch(ZhaEntity, SwitchDevice): def __init__(self, **kwargs): """Initialize the ZHA switch.""" super().__init__(**kwargs) - self._on_off_channel = self.cluster_channels.get(ON_OFF_CHANNEL) + self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) @property def is_on(self) -> bool: diff --git a/homeassistant/components/zhong_hong/climate.py b/homeassistant/components/zhong_hong/climate.py index bfc730a285e..8514ec711cb 100644 --- a/homeassistant/components/zhong_hong/climate.py +++ b/homeassistant/components/zhong_hong/climate.py @@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): async_dispatcher_connect(hass, SIGNAL_DEVICE_ADDED, startup) - # add devices after SIGNAL_DEVICE_SETTED_UP event is listend + # add devices after SIGNAL_DEVICE_SETTED_UP event is listened add_entities(devices) def stop_listen(event): diff --git a/homeassistant/components/zone/.translations/hr.json b/homeassistant/components/zone/.translations/hr.json new file mode 100644 index 00000000000..8a9f543be0a --- /dev/null +++ b/homeassistant/components/zone/.translations/hr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "name_exists": "Ime ve\u0107 postoji" + }, + "step": { + "init": { + "data": { + "icon": "Ikona", + "latitude": "Zemljopisna \u0161irina", + "longitude": "Zemljopisna du\u017eina", + "name": "Ime", + "passive": "Pasivno", + "radius": "Radijus" + }, + "title": "Definirajte parametre zone" + } + }, + "title": "Zona" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/es-419.json b/homeassistant/components/zwave/.translations/es-419.json index 2e246fb9931..f2ca1a19aa4 100644 --- a/homeassistant/components/zwave/.translations/es-419.json +++ b/homeassistant/components/zwave/.translations/es-419.json @@ -4,6 +4,9 @@ "already_configured": "Z-Wave ya est\u00e1 configurado", "one_instance_only": "El componente solo admite una instancia de Z-Wave" }, + "error": { + "option_error": "La validaci\u00f3n de Z-Wave fall\u00f3. \u00bfEs correcta la ruta a la memoria USB?" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index cd456ede75e..bc40d46b8ba 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -564,11 +564,6 @@ async def async_setup_entry(hass, config_entry): _LOGGER.info("Z-Wave soft_reset have been initialized") network.controller.soft_reset() - def update_config(service): - """Update the config from git.""" - _LOGGER.info("Configuration update has been initialized") - network.controller.update_ozw_config() - def test_network(service): """Test the network by sending commands to all the nodes.""" _LOGGER.info("Z-Wave test_network have been initialized") @@ -891,7 +886,6 @@ async def async_setup_entry(hass, config_entry): hass.services.register(DOMAIN, const.SERVICE_CANCEL_COMMAND, cancel_command) hass.services.register(DOMAIN, const.SERVICE_HEAL_NETWORK, heal_network) hass.services.register(DOMAIN, const.SERVICE_SOFT_RESET, soft_reset) - hass.services.register(DOMAIN, const.SERVICE_UPDATE_CONFIG, update_config) hass.services.register(DOMAIN, const.SERVICE_TEST_NETWORK, test_network) hass.services.register(DOMAIN, const.SERVICE_STOP_NETWORK, stop_network) hass.services.register( diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 6f66c6f36c4..2c7ce4b18a4 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -19,6 +19,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_OFF, PRESET_BOOST, PRESET_NONE, + SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, @@ -40,7 +41,8 @@ REMOTEC_ZXT_120 = 0x8377 REMOTEC_ZXT_120_THERMOSTAT = (REMOTEC, REMOTEC_ZXT_120) ATTR_OPERATING_STATE = "operating_state" ATTR_FAN_STATE = "fan_state" - +ATTR_FAN_ACTION = "fan_action" +AUX_HEAT_ZWAVE_MODE = "Aux Heat" # Device is in manufacturer specific mode (e.g. setting the valve manually) PRESET_MANUFACTURER_SPECIFIC = "Manufacturer Specific" @@ -54,7 +56,6 @@ HVAC_STATE_MAPPINGS = { "heat": HVAC_MODE_HEAT, "heat mode": HVAC_MODE_HEAT, "heat (default)": HVAC_MODE_HEAT, - "aux heat": HVAC_MODE_HEAT, "furnace": HVAC_MODE_HEAT, "fan only": HVAC_MODE_FAN_ONLY, "dry air": HVAC_MODE_DRY, @@ -62,6 +63,7 @@ HVAC_STATE_MAPPINGS = { "cool": HVAC_MODE_COOL, "heat_cool": HVAC_MODE_HEAT_COOL, "auto": HVAC_MODE_HEAT_COOL, + "auto changeover": HVAC_MODE_HEAT_COOL, } HVAC_CURRENT_MAPPINGS = { @@ -127,13 +129,14 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._hvac_list = None # [zwave_mode] self._hvac_mapping = None # {ha_mode:zwave_mode} self._hvac_mode = None # ha_mode + self._aux_heat = None self._default_hvac_mode = None # ha_mode self._preset_mapping = None # {ha_mode:zwave_mode} self._preset_list = None # [zwave_mode] self._preset_mode = None # ha_mode if exists, else zwave_mode self._current_fan_mode = None self._fan_modes = None - self._fan_state = None + self._fan_action = None self._current_swing_mode = None self._swing_modes = None self._unit = temp_unit @@ -159,6 +162,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): support |= SUPPORT_FAN_MODE if self._zxt_120 == 1 and self.values.zxt_120_swing_mode: support |= SUPPORT_SWING_MODE + if self._aux_heat: + support |= SUPPORT_AUX_HEAT if self._preset_list: support |= SUPPORT_PRESET_MODE return support @@ -177,7 +182,10 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): for mode in mode_list: ha_mode = HVAC_STATE_MAPPINGS.get(str(mode).lower()) ha_preset = PRESET_MAPPINGS.get(str(mode).lower()) - if ha_mode and ha_mode not in self._hvac_mapping: + if mode == AUX_HEAT_ZWAVE_MODE: + # Aux Heat should not be included in any mapping + self._aux_heat = True + elif ha_mode and ha_mode not in self._hvac_mapping: self._hvac_mapping[ha_mode] = mode self._hvac_list.append(ha_mode) elif ha_preset and ha_preset not in self._preset_mapping: @@ -246,6 +254,7 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("self._hvac_mode=%s", self._hvac_mode) _LOGGER.debug("self._default_hvac_mode=%s", self._default_hvac_mode) _LOGGER.debug("self._hvac_action=%s", self._hvac_action) + _LOGGER.debug("self._aux_heat=%s", self._aux_heat) _LOGGER.debug("self._preset_mapping=%s", self._preset_mapping) _LOGGER.debug("self._preset_list=%s", self._preset_list) _LOGGER.debug("self._preset_mode=%s", self._preset_mode) @@ -291,8 +300,8 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): self._hvac_action = HVAC_CURRENT_MAPPINGS.get(str(mode).lower(), mode) # Fan operating state - if self.values.fan_state: - self._fan_state = self.values.fan_state.data + if self.values.fan_action: + self._fan_action = self.values.fan_action.data @property def fan_mode(self): @@ -356,6 +365,15 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): """ return self._hvac_action + @property + def is_aux_heat(self): + """Return true if aux heater.""" + if not self._aux_heat: + return None + if self.values.mode.data == AUX_HEAT_ZWAVE_MODE: + return True + return False + @property def preset_mode(self): """Return preset operation ie. eco, away. @@ -404,6 +422,25 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): _LOGGER.debug("Set operation_mode to %s", operation_mode) self.values.mode.data = operation_mode + def turn_aux_heat_on(self): + """Turn auxillary heater on.""" + if not self._aux_heat: + return + operation_mode = AUX_HEAT_ZWAVE_MODE + _LOGGER.debug("Aux heat on. Set operation mode to %s", operation_mode) + self.values.mode.data = operation_mode + + def turn_aux_heat_off(self): + """Turn auxillary heater off.""" + if not self._aux_heat: + return + if HVAC_MODE_HEAT in self._hvac_mapping: + operation_mode = self._hvac_mapping.get(HVAC_MODE_HEAT) + else: + operation_mode = self._hvac_mapping.get(HVAC_MODE_OFF) + _LOGGER.debug("Aux heat off. Set operation mode to %s", operation_mode) + self.values.mode.data = operation_mode + def set_preset_mode(self, preset_mode): """Set new target preset mode.""" _LOGGER.debug("Set preset_mode to %s", preset_mode) @@ -426,3 +463,11 @@ class ZWaveClimate(ZWaveDeviceEntity, ClimateDevice): if self._zxt_120 == 1: if self.values.zxt_120_swing_mode: self.values.zxt_120_swing_mode.data = swing_mode + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + data = super().device_state_attributes + if self._fan_action: + data[ATTR_FAN_ACTION] = self._fan_action + return data diff --git a/homeassistant/components/zwave/const.py b/homeassistant/components/zwave/const.py index 72e08299691..83fb43fd3fb 100644 --- a/homeassistant/components/zwave/const.py +++ b/homeassistant/components/zwave/const.py @@ -68,7 +68,6 @@ SERVICE_RENAME_VALUE = "rename_value" SERVICE_REFRESH_ENTITY = "refresh_entity" SERVICE_REFRESH_NODE = "refresh_node" SERVICE_RESET_NODE_METERS = "reset_node_meters" -SERVICE_UPDATE_CONFIG = "update_config" EVENT_SCENE_ACTIVATED = "zwave.scene_activated" EVENT_NODE_EVENT = "zwave.node_event" @@ -173,7 +172,7 @@ COMMAND_CLASS_SWITCH_TOGGLE_MULTILEVEL = 41 COMMAND_CLASS_TARIFF_TBL_CONFIG = 74 COMMAND_CLASS_TARIFF_TBL_MONITOR = 75 COMMAND_CLASS_THERMOSTAT_FAN_MODE = 68 -COMMAND_CLASS_THERMOSTAT_FAN_STATE = 69 +COMMAND_CLASS_THERMOSTAT_FAN_ACTION = 69 COMMAND_CLASS_THERMOSTAT_MODE = 64 COMMAND_CLASS_THERMOSTAT_OPERATING_STATE = 66 COMMAND_CLASS_THERMOSTAT_SETBACK = 71 diff --git a/homeassistant/components/zwave/discovery_schemas.py b/homeassistant/components/zwave/discovery_schemas.py index 1be9ca2834e..dbec1484508 100644 --- a/homeassistant/components/zwave/discovery_schemas.py +++ b/homeassistant/components/zwave/discovery_schemas.py @@ -78,9 +78,9 @@ DISCOVERY_SCHEMAS = [ ], const.DISC_OPTIONAL: True, }, - "fan_state": { + "fan_action": { const.DISC_COMMAND_CLASS: [ - const.COMMAND_CLASS_THERMOSTAT_FAN_STATE + const.COMMAND_CLASS_THERMOSTAT_FAN_ACTION ], const.DISC_OPTIONAL: True, }, diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index 37b12232759..52b135eba81 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -137,9 +137,6 @@ set_wakeup: value: description: Value of the interval to set. (integer) -update_config: - description: Attempt to update ozw configuration files from git to support newer devices. - start_network: description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. diff --git a/homeassistant/components/zwave/websocket_api.py b/homeassistant/components/zwave/websocket_api.py index 794d901cce3..7454f2e2c6a 100644 --- a/homeassistant/components/zwave/websocket_api.py +++ b/homeassistant/components/zwave/websocket_api.py @@ -7,7 +7,14 @@ import voluptuous as vol from homeassistant.components import websocket_api from homeassistant.core import callback -from .const import DATA_NETWORK +from .const import ( + CONF_AUTOHEAL, + CONF_DEBUG, + CONF_POLLING_INTERVAL, + CONF_USB_STICK_PATH, + DATA_NETWORK, + DATA_ZWAVE_CONFIG, +) _LOGGER = logging.getLogger(__name__) @@ -23,7 +30,24 @@ def websocket_network_status(hass, connection, msg): connection.send_result(msg[ID], {"state": network.state}) +@websocket_api.require_admin +@websocket_api.websocket_command({vol.Required(TYPE): "zwave/get_config"}) +def websocket_get_config(hass, connection, msg): + """Get Z-Wave configuration.""" + config = hass.data[DATA_ZWAVE_CONFIG] + connection.send_result( + msg[ID], + { + CONF_AUTOHEAL: config[CONF_AUTOHEAL], + CONF_DEBUG: config[CONF_DEBUG], + CONF_POLLING_INTERVAL: config[CONF_POLLING_INTERVAL], + CONF_USB_STICK_PATH: config[CONF_USB_STICK_PATH], + }, + ) + + @callback def async_load_websocket_api(hass): """Set up the web socket API.""" websocket_api.async_register_command(hass, websocket_network_status) + websocket_api.async_register_command(hass, websocket_get_config) diff --git a/homeassistant/config.py b/homeassistant/config.py index 3ba59fd80ec..1f42b3db25e 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -53,8 +53,11 @@ from homeassistant.const import ( ) from homeassistant.core import DOMAIN as CONF_CORE, SOURCE_YAML, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import Integration, async_get_integration, IntegrationNotFound -from homeassistant.requirements import async_process_requirements +from homeassistant.loader import Integration, IntegrationNotFound +from homeassistant.requirements import ( + async_get_integration_with_requirements, + RequirementsNotFound, +) from homeassistant.util.yaml import load_yaml, SECRET_YAML from homeassistant.util.package import is_docker_env import homeassistant.helpers.config_validation as cv @@ -319,7 +322,8 @@ async def async_hass_config_yaml(hass: HomeAssistant) -> Dict: config = load_yaml_config_file(path) return config - config = await hass.async_add_executor_job(_load_hass_yaml_config) + # Not using async_add_executor_job because this is an internal method. + config = await hass.loop.run_in_executor(None, _load_hass_yaml_config) core_config = config.get(CONF_CORE, {}) await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {})) return config @@ -628,10 +632,9 @@ def _recursive_merge(conf: Dict[str, Any], package: Dict[str, Any]) -> Union[boo error = _recursive_merge(conf=conf[key], package=pack_conf) elif isinstance(pack_conf, list): - if not pack_conf: - continue - conf[key] = cv.ensure_list(conf.get(key)) - conf[key].extend(cv.ensure_list(pack_conf)) + conf[key] = cv.remove_falsy( + cv.ensure_list(conf.get(key)) + cv.ensure_list(pack_conf) + ) else: if conf.get(key) is not None: @@ -658,45 +661,25 @@ async def merge_packages_config( domain = comp_name.split(" ")[0] try: - integration = await async_get_integration(hass, domain) - except IntegrationNotFound: - _log_pkg_error(pack_name, comp_name, config, "does not exist") - continue - - if ( - not hass.config.skip_pip - and integration.requirements - and not await async_process_requirements( - hass, integration.domain, integration.requirements + integration = await async_get_integration_with_requirements( + hass, domain ) - ): - _log_pkg_error( - pack_name, comp_name, config, "unable to install all requirements" - ) - continue - - try: component = integration.get_component() - except ImportError: - _log_pkg_error(pack_name, comp_name, config, "unable to import") + except (IntegrationNotFound, RequirementsNotFound, ImportError) as ex: + _log_pkg_error(pack_name, comp_name, config, str(ex)) continue - if hasattr(component, "PLATFORM_SCHEMA"): - if not comp_conf: - continue # Ensure we dont add Falsy items to list - config[comp_name] = cv.ensure_list(config.get(comp_name)) - config[comp_name].extend(cv.ensure_list(comp_conf)) - continue + merge_list = hasattr(component, "PLATFORM_SCHEMA") - if hasattr(component, "CONFIG_SCHEMA"): + if not merge_list and hasattr(component, "CONFIG_SCHEMA"): merge_type, _ = _identify_config_schema(component) + merge_list = merge_type == "list" - if merge_type == "list": - if not comp_conf: - continue # Ensure we dont add Falsy items to list - config[comp_name] = cv.ensure_list(config.get(comp_name)) - config[comp_name].extend(cv.ensure_list(comp_conf)) - continue + if merge_list: + config[comp_name] = cv.remove_falsy( + cv.ensure_list(config.get(comp_name)) + cv.ensure_list(comp_conf) + ) + continue if comp_conf is None: comp_conf = OrderedDict() @@ -775,26 +758,15 @@ async def async_process_component_config( continue try: - p_integration = await async_get_integration(hass, p_name) - except IntegrationNotFound: - continue - - if ( - not hass.config.skip_pip - and p_integration.requirements - and not await async_process_requirements( - hass, p_integration.domain, p_integration.requirements - ) - ): - _LOGGER.error( - "Unable to install all requirements for %s.%s", domain, p_name - ) + p_integration = await async_get_integration_with_requirements(hass, p_name) + except (RequirementsNotFound, IntegrationNotFound) as ex: + _LOGGER.error("Platform error: %s - %s", domain, ex) continue try: platform = p_integration.get_platform(domain) except ImportError: - _LOGGER.exception("Failed to get platform %s.%s", domain, p_name) + _LOGGER.exception("Platform error: %s", domain) continue # Validate platform specific schema diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index fb064f63075..c2da37943c1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -3,21 +3,17 @@ import asyncio import logging import functools import uuid -from typing import ( - Any, - Callable, - List, - Optional, - Set, # noqa pylint: disable=unused-import -) +from typing import Any, Callable, List, Optional, Set 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.helpers import entity_registry # mypy: allow-untyped-defs @@ -88,6 +84,7 @@ class ConfigEntry: "title", "data", "options", + "system_options", "source", "connection_class", "state", @@ -104,6 +101,7 @@ class ConfigEntry: data: dict, source: str, connection_class: str, + system_options: dict, options: Optional[dict] = None, entry_id: Optional[str] = None, state: str = ENTRY_STATE_NOT_LOADED, @@ -127,6 +125,9 @@ class ConfigEntry: # Entry options self.options = options or {} + # Entry system options + self.system_options = SystemOptions(**system_options) + # Source of the configuration (user, discovery, cloud) self.source = source @@ -155,8 +156,6 @@ class ConfigEntry: try: component = integration.get_component() - if self.domain == integration.domain: - integration.get_platform("config_flow") except ImportError as err: _LOGGER.error( "Error importing integration %s to set up %s config entry: %s", @@ -168,8 +167,20 @@ class ConfigEntry: self.state = ENTRY_STATE_SETUP_ERROR return - # Perform migration - if integration.domain == self.domain: + if self.domain == integration.domain: + try: + integration.get_platform("config_flow") + except ImportError as err: + _LOGGER.error( + "Error importing platform config_flow from integration %s to set up %s config entry: %s", + integration.domain, + self.domain, + err, + ) + self.state = ENTRY_STATE_SETUP_ERROR + return + + # Perform migration if not await self.async_migrate(hass): self.state = ENTRY_STATE_MIGRATION_ERROR return @@ -355,6 +366,7 @@ class ConfigEntry: "title": self.title, "data": self.data, "options": self.options, + "system_options": self.system_options.as_dict(), "source": self.source, "connection_class": self.connection_class, } @@ -376,6 +388,7 @@ class ConfigEntries: self._hass_config = hass_config self._entries = [] # type: List[ConfigEntry] self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + EntityRegistryDisabledHandler(hass).async_setup() @callback def async_domains(self) -> List[str]: @@ -457,6 +470,8 @@ class ConfigEntries: connection_class=entry.get("connection_class", CONN_CLASS_UNKNOWN), # New in 0.89 options=entry.get("options"), + # New in 0.98 + system_options=entry.get("system_options", {}), ) for entry in config["entries"] ] @@ -513,7 +528,9 @@ class ConfigEntries: return await self.async_setup(entry_id) @callback - def async_update_entry(self, entry, *, data=_UNDEF, options=_UNDEF): + def async_update_entry( + self, entry, *, data=_UNDEF, options=_UNDEF, system_options=_UNDEF + ): """Update a config entry.""" if data is not _UNDEF: entry.data = data @@ -521,10 +538,12 @@ class ConfigEntries: if options is not _UNDEF: entry.options = options - if data is not _UNDEF or options is not _UNDEF: - for listener_ref in entry.update_listeners: - listener = listener_ref() - self.hass.async_create_task(listener(self.hass, entry)) + if system_options is not _UNDEF: + entry.system_options.update(**system_options) + + for listener_ref in entry.update_listeners: + listener = listener_ref() + self.hass.async_create_task(listener(self.hass, entry)) self._async_schedule_save() @@ -580,6 +599,7 @@ class ConfigEntries: title=result["title"], data=result["data"], options={}, + system_options={}, source=flow.context["source"], connection_class=flow.CONNECTION_CLASS, ) @@ -656,8 +676,20 @@ async def _old_conf_migrator(old_config): class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" + def __init_subclass__(cls, domain=None, **kwargs): + """Initialize a subclass, register if possible.""" + super().__init_subclass__(**kwargs) # type: ignore + if domain is not None: + HANDLERS.register(domain)(cls) + CONNECTION_CLASS = CONN_CLASS_UNKNOWN + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + raise data_entry_flow.UnknownHandler + @callback def _async_current_entries(self): """Return current entries.""" @@ -691,7 +723,11 @@ class OptionsFlowManager: entry = self.hass.config_entries.async_get_entry(entry_id) if entry is None: return - flow = HANDLERS[entry.domain].async_get_options_flow(entry.data, entry.options) + + if entry.domain not in HANDLERS: + raise data_entry_flow.UnknownHandler + + flow = HANDLERS[entry.domain].async_get_options_flow(entry) return flow async def _async_finish_flow(self, flow, result): @@ -706,3 +742,112 @@ class OptionsFlowManager: result["result"] = True return result + + +class OptionsFlow(data_entry_flow.FlowHandler): + """Base class for config option flows.""" + + pass + + +@attr.s(slots=True) +class SystemOptions: + """Config entry system options.""" + + disable_new_entities = attr.ib(type=bool, default=False) + + def update(self, *, disable_new_entities): + """Update properties.""" + self.disable_new_entities = disable_new_entities + + def as_dict(self): + """Return dictionary version of this config entrys system options.""" + return {"disable_new_entities": self.disable_new_entities} + + +class EntityRegistryDisabledHandler: + """Handler to handle when entities related to config entries updating disabled_by.""" + + RELOAD_AFTER_UPDATE_DELAY = 30 + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the handler.""" + self.hass = hass + self.registry: Optional[entity_registry.EntityRegistry] = None + self.changed: Set[str] = set() + self._remove_call_later: Optional[Callable[[], None]] = None + + @callback + def async_setup(self) -> None: + """Set up the disable handler.""" + self.hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, self._handle_entry_updated + ) + + async def _handle_entry_updated(self, event): + """Handle entity registry entry update.""" + if ( + event.data["action"] != "update" + or "disabled_by" not in event.data["changes"] + ): + return + + if self.registry is None: + self.registry = await entity_registry.async_get_registry(self.hass) + + entity_entry = self.registry.async_get(event.data["entity_id"]) + + if ( + # Stop if no entry found + entity_entry is None + # Stop if entry not connected to config entry + or entity_entry.config_entry_id is None + # Stop if the entry got disabled. In that case the entity handles it + # themselves. + or entity_entry.disabled_by + ): + return + + config_entry = self.hass.config_entries.async_get_entry( + entity_entry.config_entry_id + ) + + if config_entry.entry_id not in self.changed and await support_entry_unload( + self.hass, config_entry.domain + ): + self.changed.add(config_entry.entry_id) + + if not self.changed: + return + + # We are going to delay reloading on *every* entity registry change so that + # if a user is happily clicking along, it will only reload at the end. + + if self._remove_call_later: + self._remove_call_later() + + self._remove_call_later = self.hass.helpers.event.async_call_later( + self.RELOAD_AFTER_UPDATE_DELAY, self._handle_reload + ) + + async def _handle_reload(self, _now): + """Handle a reload.""" + self._remove_call_later = None + to_reload = self.changed + self.changed = set() + + _LOGGER.info( + "Reloading config entries because disabled_by changed in entity registry: %s", + ", ".join(self.changed), + ) + + await asyncio.gather( + *[self.hass.config_entries.async_reload(entry_id) for entry_id in to_reload] + ) + + +async def support_entry_unload(hass: HomeAssistant, domain: str) -> bool: + """Test if a domain supports entry unloading.""" + integration = await loader.async_get_integration(hass, domain) + component = integration.get_component() + return hasattr(component, "async_unload_entry") diff --git a/homeassistant/const.py b/homeassistant/const.py index 7078ee62d07..9a9b098aabb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 97 -PATCH_VERSION = "2" +MINOR_VERSION = 98 +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 0) diff --git a/homeassistant/core.py b/homeassistant/core.py index a205aa401a6..e8e33a0479e 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -276,7 +276,7 @@ class HomeAssistant: self.state = CoreState.running _async_create_timer(self) - def add_job(self, target: Callable[..., None], *args: Any) -> None: + def add_job(self, target: Callable[..., Any], *args: Any) -> None: """Add job to the executor pool. target: target to call. diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 789093cc49c..0af6677dceb 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -175,11 +175,11 @@ class FlowHandler: """Handle the configuration flow of a component.""" # Set by flow manager - flow_id = None - hass = None + flow_id: Optional[str] = None + hass: Optional[HomeAssistant] = None handler = None - cur_step = None - context = None # type: Optional[Dict] + cur_step: Optional[Dict[str, str]] = None + context: Dict # Set by _async_create_flow callback init_step = "init" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 827b946f776..de665ecf5a6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -16,6 +16,7 @@ FLOWS = [ "emulated_roku", "esphome", "geofency", + "geonetnz_quakes", "gpslogger", "hangouts", "heos", @@ -51,6 +52,7 @@ FLOWS = [ "tellduslive", "toon", "tplink", + "traccar", "tradfri", "twentemilieu", "twilio", diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index a2b4a8580f9..bc39d5d5720 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -5,7 +5,7 @@ from typing import List import attr import voluptuous as vol -from homeassistant import loader, requirements +from homeassistant import loader from homeassistant.core import HomeAssistant from homeassistant.config import ( CONF_CORE, @@ -18,13 +18,16 @@ from homeassistant.config import ( extract_domain_configs, config_per_platform, ) +from homeassistant.requirements import ( + async_get_integration_with_requirements, + RequirementsNotFound, +) import homeassistant.util.yaml.loader as yaml_loader from homeassistant.exceptions import HomeAssistantError -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-warn-return-any +# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any CheckConfigError = namedtuple("CheckConfigError", "message domain config") @@ -101,29 +104,15 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig # Process and validate config for domain in components: try: - integration = await loader.async_get_integration(hass, domain) - except loader.IntegrationNotFound: - result.add_error("Integration not found: {}".format(domain)) - continue - - if ( - not hass.config.skip_pip - and integration.requirements - and not await requirements.async_process_requirements( - hass, integration.domain, integration.requirements - ) - ): - result.add_error( - "Unable to install all requirements: {}".format( - ", ".join(integration.requirements) - ) - ) + integration = await async_get_integration_with_requirements(hass, domain) + except (RequirementsNotFound, loader.IntegrationNotFound) as ex: + result.add_error("Component error: {} - {}".format(domain, ex)) continue try: component = integration.get_component() - except ImportError: - result.add_error("Component not found: {}".format(domain)) + except ImportError as ex: + result.add_error("Component error: {} - {}".format(domain, ex)) continue config_schema = getattr(component, "CONFIG_SCHEMA", None) @@ -161,32 +150,16 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig continue try: - p_integration = await loader.async_get_integration(hass, p_name) - except loader.IntegrationNotFound: - result.add_error( - "Integration {} not found when trying to verify its {} " - "platform.".format(p_name, domain) + p_integration = await async_get_integration_with_requirements( + hass, p_name ) - continue - - if ( - not hass.config.skip_pip - and p_integration.requirements - and not await requirements.async_process_requirements( - hass, p_integration.domain, p_integration.requirements - ) - ): - result.add_error( - "Unable to install all requirements: {}".format( - ", ".join(integration.requirements) - ) - ) - continue - - try: platform = p_integration.get_platform(domain) - except ImportError: - result.add_error("Platform not found: {}.{}".format(domain, p_name)) + except ( + loader.IntegrationNotFound, + RequirementsNotFound, + ImportError, + ) as ex: + result.add_error("Platform error {}.{} - {}".format(domain, p_name, ex)) continue # Validate platform specific schema diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index b8e08e1ac97..40465f83728 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -321,8 +321,24 @@ def sun( before_offset = before_offset or timedelta(0) after_offset = after_offset or timedelta(0) - sunrise = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today) - sunset = get_astral_event_date(hass, SUN_EVENT_SUNSET, today) + sunrise_today = get_astral_event_date(hass, SUN_EVENT_SUNRISE, today) + sunset_today = get_astral_event_date(hass, SUN_EVENT_SUNSET, today) + + sunrise = sunrise_today + sunset = sunset_today + if today > dt_util.as_local( + cast(datetime, sunrise_today) + ).date() and SUN_EVENT_SUNRISE in (before, after): + tomorrow = dt_util.as_local(utcnow + timedelta(days=1)).date() + sunrise_tomorrow = get_astral_event_date(hass, SUN_EVENT_SUNRISE, tomorrow) + sunrise = sunrise_tomorrow + + if today > dt_util.as_local( + cast(datetime, sunset_today) + ).date() and SUN_EVENT_SUNSET in (before, after): + tomorrow = dt_util.as_local(utcnow + timedelta(days=1)).date() + sunset_tomorrow = get_astral_event_date(hass, SUN_EVENT_SUNSET, tomorrow) + sunset = sunset_tomorrow if sunrise is None and SUN_EVENT_SUNRISE in (before, after): # There is no sunrise today diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 28b2fb495d0..922878fb324 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,28 +1,11 @@ """Helpers for data entry flows for config entries.""" -from functools import partial - +from typing import Callable, Awaitable, Union from homeassistant import config_entries +from .typing import HomeAssistantType +# mypy: allow-untyped-defs -# mypy: allow-incomplete-defs, allow-untyped-defs - - -def register_discovery_flow(domain, title, discovery_function, connection_class): - """Register flow for discovered integrations that not require auth.""" - config_entries.HANDLERS.register(domain)( - partial( - DiscoveryFlowHandler, domain, title, discovery_function, connection_class - ) - ) - - -def register_webhook_flow(domain, title, description_placeholder, allow_multiple=False): - """Register flow for webhook integrations.""" - config_entries.HANDLERS.register(domain)( - partial( - WebhookFlowHandler, domain, title, description_placeholder, allow_multiple - ) - ) +DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] class DiscoveryFlowHandler(config_entries.ConfigFlow): @@ -30,7 +13,13 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): VERSION = 1 - def __init__(self, domain, title, discovery_function, connection_class): + def __init__( + self, + domain: str, + title: str, + discovery_function: DiscoveryFunctionType, + connection_class: str, + ) -> None: """Initialize the discovery config flow.""" self._domain = domain self._title = title @@ -90,12 +79,35 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): return self.async_create_entry(title=self._title, data={}) +def register_discovery_flow( + domain: str, + title: str, + discovery_function: DiscoveryFunctionType, + connection_class: str, +) -> None: + """Register flow for discovered integrations that not require auth.""" + + class DiscoveryFlow(DiscoveryFlowHandler): + """Discovery flow handler.""" + + def __init__(self) -> None: + super().__init__(domain, title, discovery_function, connection_class) + + config_entries.HANDLERS.register(domain)(DiscoveryFlow) + + class WebhookFlowHandler(config_entries.ConfigFlow): """Handle a webhook config flow.""" VERSION = 1 - def __init__(self, domain, title, description_placeholder, allow_multiple): + def __init__( + self, + domain: str, + title: str, + description_placeholder: dict, + allow_multiple: bool, + ) -> None: """Initialize the discovery config flow.""" self._domain = domain self._title = title @@ -130,7 +142,23 @@ class WebhookFlowHandler(config_entries.ConfigFlow): ) -async def webhook_async_remove_entry(hass, entry) -> None: +def register_webhook_flow( + domain: str, title: str, description_placeholder: dict, allow_multiple: bool = False +) -> None: + """Register flow for webhook integrations.""" + + class WebhookFlow(WebhookFlowHandler): + """Webhook flow handler.""" + + def __init__(self) -> None: + super().__init__(domain, title, description_placeholder, allow_multiple) + + config_entries.HANDLERS.register(domain)(WebhookFlow) + + +async def webhook_async_remove_entry( + hass: HomeAssistantType, entry: config_entries.ConfigEntry +) -> None: """Remove a webhook config entry.""" if not entry.data.get("cloudhook") or "cloud" not in hass.config.components: return diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index ece6b0753a2..db96f4a2d02 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -11,7 +11,7 @@ from datetime import ( ) from socket import _GLOBAL_DEFAULT_TIMEOUT from numbers import Number -from typing import Any, Union, TypeVar, Callable, Sequence, Dict, Optional +from typing import Any, Union, TypeVar, Callable, List, Dict, Optional from urllib.parse import urlparse from uuid import UUID @@ -191,7 +191,7 @@ def isdir(value: Any) -> str: return dir_in -def ensure_list(value: Union[T, Sequence[T], None]) -> Sequence[T]: +def ensure_list(value: Union[T, List[T], None]) -> List[T]: """Wrap value in list if it is not one.""" if value is None: return [] @@ -207,7 +207,7 @@ def entity_id(value: Any) -> str: raise vol.Invalid("Entity ID {} is an invalid entity id".format(value)) -def entity_ids(value: Union[str, Sequence]) -> Sequence[str]: +def entity_ids(value: Union[str, List]) -> List[str]: """Validate Entity IDs.""" if value is None: raise vol.Invalid("Entity IDs can not be None") @@ -234,7 +234,7 @@ def entity_domain(domain: str): def entities_domain(domain: str): """Validate that entities belong to domain.""" - def validate(values: Union[str, Sequence]) -> Sequence[str]: + def validate(values: Union[str, List]) -> List[str]: """Test if entity domain is domain.""" values = entity_ids(values) for ent_id in values: @@ -261,7 +261,7 @@ def icon(value): if ":" in value: return value - raise vol.Invalid('Icons should be specifed on the form "prefix:name"') + raise vol.Invalid('Icons should be specified in the form "prefix:name"') time_period_dict = vol.All( @@ -370,7 +370,7 @@ def positive_timedelta(value: timedelta) -> timedelta: return value -def remove_falsy(value: Sequence[T]) -> Sequence[T]: +def remove_falsy(value: List[T]) -> List[T]: """Remove falsy values from a list.""" return [v for v in value if v] @@ -562,7 +562,7 @@ def uuid4_hex(value): return result.hex -def ensure_list_csv(value: Any) -> Sequence: +def ensure_list_csv(value: Any) -> List: """Ensure that input is a list or make one from comma-separated string.""" if isinstance(value, str): return [member.strip() for member in value.split(",")] diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index a4d83c4eba9..19b4a1333b6 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -13,7 +13,7 @@ from homeassistant.loader import bind_hass from .typing import HomeAssistantType -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any _LOGGER = logging.getLogger(__name__) @@ -84,7 +84,9 @@ class DeviceRegistry: return self.devices.get(device_id) @callback - def async_get_device(self, identifiers: set, connections: set): + def async_get_device( + self, identifiers: set, connections: set + ) -> Optional[DeviceEntry]: """Check if device is registered.""" for device in self.devices.values(): if any(iden in device.identifiers for iden in identifiers) or any( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index fa4352822ed..bd96e1bafdb 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -3,7 +3,7 @@ from datetime import timedelta import logging import functools as ft from timeit import default_timer as timer -from typing import Optional, List, Iterable +from typing import Any, Optional, List, Iterable from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -34,8 +34,7 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util import dt as dt_util -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs -# mypy: no-warn-return-any +# mypy: allow-untyped-defs, no-check-untyped-defs, no-warn-return-any _LOGGER = logging.getLogger(__name__) SLOW_UPDATE_WARNING = 10 @@ -100,6 +99,9 @@ class Entity: # If we reported if this entity was slow _slow_reported = False + # If we reported this entity is updated while disabled + _disabled_reported = False + # Protect for multiple updates _update_staged = False @@ -217,6 +219,11 @@ class Entity: """Time that a context is considered recent.""" return timedelta(seconds=5) + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return True + # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they # are used to perform a very specific function. Overwriting these may @@ -269,6 +276,16 @@ class Entity: @callback def _async_write_ha_state(self): """Write the state to the state machine.""" + if self.registry_entry and self.registry_entry.disabled_by: + if not self._disabled_reported: + self._disabled_reported = True + _LOGGER.warning( + "Entity %s is incorrectly being triggered for updates while it is disabled. This is a bug in the %s integration.", + self.entity_id, + self.platform.platform_name, + ) + return + start = timer() attr = {} @@ -486,6 +503,10 @@ class Entity: old = self.registry_entry self.registry_entry = ent_reg.async_get(data["entity_id"]) + if self.registry_entry.disabled_by is not None: + await self.async_remove() + return + if self.registry_entry.entity_id == old.entity_id: self.async_write_ha_state() return @@ -532,7 +553,7 @@ class ToggleEntity(Entity): """Return True if entity is on.""" raise NotImplementedError() - def turn_on(self, **kwargs) -> None: + def turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" raise NotImplementedError() @@ -543,7 +564,7 @@ class ToggleEntity(Entity): """ return self.hass.async_add_job(ft.partial(self.turn_on, **kwargs)) - def turn_off(self, **kwargs) -> None: + def turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" raise NotImplementedError() @@ -554,7 +575,7 @@ class ToggleEntity(Entity): """ return self.hass.async_add_job(ft.partial(self.turn_off, **kwargs)) - def toggle(self, **kwargs) -> None: + def toggle(self, **kwargs: Any) -> None: """Toggle the entity.""" if self.is_on: self.turn_off(**kwargs) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index ea71828f21a..4a6a3038fd0 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -8,6 +8,7 @@ from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.util.async_ import run_callback_threadsafe, run_coroutine_threadsafe +from .entity_registry import DISABLED_INTEGRATION from .event import async_track_time_interval, async_call_later @@ -333,16 +334,24 @@ class EntityPlatform: if device: device_id = device.id + disabled_by: Optional[str] = None + if not entity.entity_registry_enabled_default: + disabled_by = DISABLED_INTEGRATION + entry = entity_registry.async_get_or_create( self.domain, self.platform_name, entity.unique_id, suggested_object_id=suggested_object_id, - config_entry_id=config_entry_id, + config_entry=self.config_entry, device_id=device_id, known_object_ids=self.entities.keys(), + disabled_by=disabled_by, ) + entity.registry_entry = entry + entity.entity_id = entry.entity_id + if entry.disabled: self.logger.info( "Not adding entity %s because it's disabled", @@ -352,9 +361,6 @@ class EntityPlatform: ) return - entity.registry_entry = entry - entity.entity_id = entry.entity_id - # We won't generate an entity ID if the platform has already set one # We will however make sure that platform cannot pick a registered ID elif entity.entity_id is not None and entity_registry.async_is_registered( diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index bb546ba7b83..7d81f62fa1c 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -24,7 +24,7 @@ from homeassistant.util.yaml import load_yaml from .typing import HomeAssistantType -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs, no-warn-return-any PATH_REGISTRY = "entity_registry.yaml" @@ -33,8 +33,10 @@ EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" SAVE_DELAY = 10 _LOGGER = logging.getLogger(__name__) _UNDEF = object() +DISABLED_CONFIG_ENTRY = "config_entry" DISABLED_HASS = "hass" DISABLED_USER = "user" +DISABLED_INTEGRATION = "integration" STORAGE_VERSION = 1 STORAGE_KEY = "core.entity_registry" @@ -53,7 +55,15 @@ class RegistryEntry: disabled_by = attr.ib( type=str, default=None, - validator=attr.validators.in_((DISABLED_HASS, DISABLED_USER, None)), + validator=attr.validators.in_( + ( + DISABLED_HASS, + DISABLED_USER, + DISABLED_INTEGRATION, + DISABLED_CONFIG_ENTRY, + None, + ) + ), ) # type: Optional[str] domain = attr.ib(type=str, init=False, repr=False) @@ -91,7 +101,9 @@ class EntityRegistry: return self.entities.get(entity_id) @callback - def async_get_entity_id(self, domain: str, platform: str, unique_id: str): + def async_get_entity_id( + self, domain: str, platform: str, unique_id: str + ) -> Optional[str]: """Check if an entity_id is currently registered.""" for entity in self.entities.values(): if ( @@ -127,12 +139,18 @@ class EntityRegistry: unique_id, *, suggested_object_id=None, - config_entry_id=None, + config_entry=None, device_id=None, known_object_ids=None, + disabled_by=None, ): """Get entity. Create if it doesn't exist.""" + config_entry_id = None + if config_entry: + config_entry_id = config_entry.entry_id + entity_id = self.async_get_entity_id(domain, platform, unique_id) + if entity_id: return self._async_update_entity( entity_id, @@ -153,12 +171,20 @@ class EntityRegistry: known_object_ids, ) + if ( + disabled_by is None + and config_entry + and config_entry.system_options.disable_new_entities + ): + disabled_by = DISABLED_INTEGRATION + entity = RegistryEntry( entity_id=entity_id, config_entry_id=config_entry_id, device_id=device_id, unique_id=unique_id, platform=platform, + disabled_by=disabled_by, ) self.entities[entity_id] = entity _LOGGER.info("Registered new %s.%s entity: %s", domain, platform, entity_id) @@ -194,7 +220,13 @@ class EntityRegistry: @callback def async_update_entity( - self, entity_id, *, name=_UNDEF, new_entity_id=_UNDEF, new_unique_id=_UNDEF + self, + entity_id, + *, + name=_UNDEF, + new_entity_id=_UNDEF, + new_unique_id=_UNDEF, + disabled_by=_UNDEF, ): """Update properties of an entity.""" return self._async_update_entity( @@ -202,6 +234,7 @@ class EntityRegistry: name=name, new_entity_id=new_entity_id, new_unique_id=new_unique_id, + disabled_by=disabled_by, ) @callback @@ -214,20 +247,21 @@ class EntityRegistry: new_entity_id=_UNDEF, device_id=_UNDEF, new_unique_id=_UNDEF, + disabled_by=_UNDEF, ): """Private facing update properties method.""" old = self.entities[entity_id] changes = {} - if name is not _UNDEF and name != old.name: - changes["name"] = name - - if config_entry_id is not _UNDEF and config_entry_id != old.config_entry_id: - changes["config_entry_id"] = config_entry_id - - if device_id is not _UNDEF and device_id != old.device_id: - changes["device_id"] = device_id + for attr_name, value in ( + ("name", name), + ("config_entry_id", config_entry_id), + ("device_id", device_id), + ("disabled_by", disabled_by), + ): + if value is not _UNDEF and value != getattr(old, attr_name): + changes[attr_name] = value if new_entity_id is not _UNDEF and new_entity_id != old.entity_id: if self.async_is_registered(new_entity_id): @@ -268,7 +302,7 @@ class EntityRegistry: self.async_schedule_save() - data = {"action": "update", "entity_id": entity_id} + data = {"action": "update", "entity_id": entity_id, "changes": list(changes)} if old.entity_id != entity_id: data["old_entity_id"] = old.entity_id diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index d702fce8d8b..f29d1885d1e 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -18,12 +18,13 @@ from homeassistant.exceptions import ( from homeassistant.helpers import template, typing 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.util.async_ import run_coroutine_threadsafe from homeassistant.helpers.typing import HomeAssistantType -# mypy: allow-incomplete-defs, allow-untyped-defs, no-check-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs CONF_SERVICE = "service" CONF_SERVICE_TEMPLATE = "service_template" @@ -161,7 +162,7 @@ async def async_extract_entity_ids(hass, service_call, expand_group=True): return extracted -async def _load_services_file(hass: HomeAssistantType, domain: str): +async def _load_services_file(hass: HomeAssistantType, domain: str) -> JSON_TYPE: """Load services file for an integration.""" integration = await async_get_integration(hass, domain) try: @@ -230,6 +231,20 @@ async def async_get_all_descriptions(hass): return descriptions +@ha.callback +@bind_hass +def async_set_service_schema(hass, domain, service, schema): + """Register a description for a service.""" + hass.data.setdefault(SERVICE_DESCRIPTION_CACHE, {}) + + description = { + "description": schema.get("description") or "", + "fields": schema.get("fields") or {}, + } + + hass.data[SERVICE_DESCRIPTION_CACHE]["{}.{}".format(domain, service)] = description + + @bind_hass async def entity_service_call( hass, platforms, func, call, service_name="", required_features=None diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index fe608ce8b6c..60aceee110f 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -4,7 +4,7 @@ import datetime as dt import json import logging from collections import defaultdict -from types import TracebackType +from types import ModuleType, TracebackType from typing import ( # noqa: F401 pylint: disable=unused-import Awaitable, Dict, @@ -16,7 +16,7 @@ from typing import ( # noqa: F401 pylint: disable=unused-import Union, ) -from homeassistant.loader import bind_hass +from homeassistant.loader import bind_hass, async_get_integration, IntegrationNotFound import homeassistant.util.dt as dt_util from homeassistant.components.notify import ATTR_MESSAGE, SERVICE_NOTIFY from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON @@ -152,13 +152,27 @@ async def async_reproduce_state( for state in states: to_call[state.domain].append(state) - async def worker(domain: str, data: List[State]) -> None: - component = getattr(hass.components, domain) - if hasattr(component, "async_reproduce_states"): - await component.async_reproduce_states(data, context=context) + async def worker(domain: str, states_by_domain: List[State]) -> None: + try: + integration = await async_get_integration(hass, domain) + except IntegrationNotFound: + _LOGGER.warning( + "Trying to reproduce state for unknown integration: %s", domain + ) + return + + try: + platform: Optional[ModuleType] = integration.get_platform("reproduce_state") + except ImportError: + platform = None + + if platform: + await platform.async_reproduce_states( # type: ignore + hass, states_by_domain, context=context + ) else: await async_reproduce_state_legacy( - hass, domain, data, blocking=blocking, context=context + hass, domain, states_by_domain, blocking=blocking, context=context ) if to_call: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 5bb912adafe..368753cd626 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,14 +6,13 @@ import os from typing import Dict, List, Optional, Callable, Union, Any, Type from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback from homeassistant.loader import bind_hass from homeassistant.util import json as json_util from homeassistant.helpers.event import async_call_later -# mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs -# mypy: no-warn-return-any +# mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any STORAGE_DIR = ".storage" _LOGGER = logging.getLogger(__name__) @@ -59,7 +58,7 @@ class Store: def __init__( self, - hass, + hass: HomeAssistant, version: int, key: str, private: bool = False, @@ -94,6 +93,7 @@ class Store: """ if self._load_task is None: self._load_task = self.hass.async_add_job(self._async_load()) + assert self._load_task is not None return await self._load_task @@ -138,7 +138,7 @@ class Store: @callback def async_delay_save( self, data_func: Callable[[], Dict], delay: Optional[int] = None - ): + ) -> None: """Save data with an optional delay.""" self._data = {"version": self.version, "key": self.key, "data_func": data_func} @@ -201,7 +201,7 @@ class Store: except (json_util.SerializationError, json_util.WriteError) as err: _LOGGER.error("Error writing config for %s: %s", self.key, err) - def _write_data(self, path: str, data: Dict): + def _write_data(self, path: str, data: Dict) -> None: """Write the data.""" if not os.path.isdir(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index c591fe218f1..ca320cb1c33 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -714,6 +714,41 @@ def tangent(value): return value +def arc_sine(value): + """Filter to get arc sine of the value.""" + try: + return math.asin(float(value)) + except (ValueError, TypeError): + return value + + +def arc_cosine(value): + """Filter to get arc cosine of the value.""" + try: + return math.acos(float(value)) + except (ValueError, TypeError): + return value + + +def arc_tangent(value): + """Filter to get arc tangent of the value.""" + try: + return math.atan(float(value)) + except (ValueError, TypeError): + return value + + +def arc_tangent2(*args): + """Filter to calculate four quadrant arc tangent of y / x.""" + try: + if len(args) == 1 and isinstance(args[0], (list, tuple)): + args = args[0] + + return math.atan2(float(args[0]), float(args[1])) + except (ValueError, TypeError): + return args + + def square_root(value): """Filter to get square root of the value.""" try: @@ -872,6 +907,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["sin"] = sine self.filters["cos"] = cosine self.filters["tan"] = tangent + self.filters["asin"] = arc_sine + self.filters["acos"] = arc_cosine + self.filters["atan"] = arc_tangent + self.filters["atan2"] = arc_tangent2 self.filters["sqrt"] = square_root self.filters["as_timestamp"] = forgiving_as_timestamp self.filters["timestamp_custom"] = timestamp_custom @@ -899,6 +938,10 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.globals["pi"] = math.pi self.globals["tau"] = math.pi * 2 self.globals["e"] = math.e + self.globals["asin"] = arc_sine + self.globals["acos"] = arc_cosine + self.globals["atan"] = arc_tangent + self.globals["atan2"] = arc_tangent2 self.globals["float"] = forgiving_float self.globals["now"] = dt_util.now self.globals["utcnow"] = dt_util.utcnow diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a1148063aee..a1ffd515c5b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,20 +10,20 @@ certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 -hass-nabucasa==0.16 -home-assistant-frontend==20190805.0 -importlib-metadata==0.18 +hass-nabucasa==0.17 +home-assistant-frontend==20190828.0 +importlib-metadata==0.19 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.2 -pytz>=2019.01 -pyyaml==5.1.1 +python-slugify==3.0.3 +pytz>=2019.02 +pyyaml==5.1.2 requests==2.22.0 -ruamel.yaml==0.15.99 -sqlalchemy==1.3.5 -voluptuous-serialize==2.1.0 -voluptuous==0.11.5 +ruamel.yaml==0.15.100 +sqlalchemy==1.3.7 +voluptuous-serialize==2.2.0 +voluptuous==0.11.7 zeroconf==0.23.0 pycryptodome>=3.6.6 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 09cdee2c35a..bdc7798e4f8 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -5,8 +5,10 @@ import logging import os from typing import Any, Dict, List, Optional +from homeassistant.exceptions import HomeAssistantError import homeassistant.util.package as pkg_util from homeassistant.core import HomeAssistant +from homeassistant.loader import async_get_integration, Integration DATA_PIP_LOCK = "pip_lock" DATA_PKG_CACHE = "pkg_cache" @@ -15,12 +17,44 @@ PROGRESS_FILE = ".pip_progress" _LOGGER = logging.getLogger(__name__) +class RequirementsNotFound(HomeAssistantError): + """Raised when a component is not found.""" + + def __init__(self, domain: str, requirements: List) -> None: + """Initialize a component not found error.""" + super().__init__( + "Requirements for {} not found: {}.".format(domain, requirements) + ) + self.domain = domain + self.requirements = requirements + + +async def async_get_integration_with_requirements( + hass: HomeAssistant, domain: str +) -> Integration: + """Get an integration with installed requirements. + + This can raise IntegrationNotFound if manifest or integration + is invalid, RequirementNotFound if there was some type of + failure to install requirements. + """ + integration = await async_get_integration(hass, domain) + + if hass.config.skip_pip or not integration.requirements: + return integration + + await async_process_requirements(hass, integration.domain, integration.requirements) + + return integration + + async def async_process_requirements( hass: HomeAssistant, name: str, requirements: List[str] -) -> bool: +) -> None: """Install the requirements for a component or platform. - This method is a coroutine. + This method is a coroutine. It will raise RequirementsNotFound + if an requirement can't be satisfied. """ pip_lock = hass.data.get(DATA_PIP_LOCK) if pip_lock is None: @@ -36,14 +70,7 @@ async def async_process_requirements( ret = await hass.async_add_executor_job(_install, hass, req, kwargs) if not ret: - _LOGGER.error( - "Not initializing %s because could not install " "requirement %s", - name, - req, - ) - return False - - return True + raise RequirementsNotFound(name, [req]) def _install(hass: HomeAssistant, req: str, kwargs: Dict) -> bool: diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index e8493822771..28734b30fcc 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -28,7 +28,7 @@ MOCKS = { } # type: Dict[str, Tuple[str, Callable]] SILENCE = ("homeassistant.scripts.check_config.yaml_loader.clear_secret_cache",) -PATCHES = {} +PATCHES: Dict[str, Any] = {} C_HEAD = "bold" ERROR_STR = "General Errors" diff --git a/homeassistant/setup.py b/homeassistant/setup.py index dd29bf3ab09..78bcb2e6505 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -283,14 +283,10 @@ async def async_process_deps_reqs( ): raise HomeAssistantError("Could not set up all dependencies.") - if ( - not hass.config.skip_pip - and integration.requirements - and not await requirements.async_process_requirements( + if not hass.config.skip_pip and integration.requirements: + await requirements.async_process_requirements( hass, integration.domain, integration.requirements ) - ): - raise HomeAssistantError("Could not install all requirements.") processed.add(integration.domain) diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 04a98a84f03..70f447a3b0d 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -40,6 +40,7 @@ def _include_yaml( Example: device_tracker: !include device_tracker.yaml + """ if constructor.name is None: raise HomeAssistantError( @@ -88,7 +89,8 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: """Load a YAML file.""" if round_trip: yaml = YAML(typ="rt") - yaml.preserve_quotes = True + # type ignore: https://bitbucket.org/ruamel/yaml/pull-requests/42 + yaml.preserve_quotes = True # type: ignore else: if ExtSafeConstructor.name is None: ExtSafeConstructor.name = fname diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index f5dfdb933f2..eda3f12905d 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -113,9 +113,13 @@ def _include_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: Example: device_tracker: !include device_tracker.yaml + """ fname = os.path.join(os.path.dirname(loader.name), node.value) - return _add_reference(load_yaml(fname), loader, node) + try: + return _add_reference(load_yaml(fname), loader, node) + except FileNotFoundError: + raise HomeAssistantError(f"{node.start_mark}: Unable to read file {fname}.") def _is_file_valid(name: str) -> bool: diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 507e46b583d..00000000000 --- a/mypy.ini +++ /dev/null @@ -1,16 +0,0 @@ -[mypy] -python_version = 3.6 -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_untyped_calls = true -disallow_untyped_defs = true -follow_imports = silent -ignore_missing_imports = true -no_implicit_optional = true -strict_equality = true -warn_incomplete_stub = true -warn_redundant_casts = true -warn_return_any = true -warn_unreachable = true -warn_unused_configs = true -warn_unused_ignores = true diff --git a/mypyrc b/mypyrc index ece4678abe6..f3866f40e57 100644 --- a/mypyrc +++ b/mypyrc @@ -1,5 +1,32 @@ homeassistant/*.py homeassistant/auth/ +homeassistant/components/*.py +homeassistant/components/automation/ +homeassistant/components/binary_sensor/ +homeassistant/components/calendar/ +homeassistant/components/camera/ +homeassistant/components/cover/ +homeassistant/components/frontend/ +homeassistant/components/geo_location/ +homeassistant/components/history/ +homeassistant/components/http/ +homeassistant/components/image_processing/ +homeassistant/components/integration/ +homeassistant/components/light/ +homeassistant/components/lock/ +homeassistant/components/mailbox/ +homeassistant/components/media_player/ +homeassistant/components/notify/ +homeassistant/components/proximity/ +homeassistant/components/remote/ +homeassistant/components/scene/ +homeassistant/components/sensor/ +homeassistant/components/switch/ +homeassistant/components/systemmonitor/ +homeassistant/components/tts/ +homeassistant/components/vacuum/ +homeassistant/components/water_heater/ +homeassistant/components/weather/ homeassistant/helpers/ homeassistant/scripts/ homeassistant/util/ diff --git a/requirements_all.txt b/requirements_all.txt index 423409b7054..e749340a8cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -6,18 +6,18 @@ attrs==19.1.0 bcrypt==3.1.7 certifi>=2019.6.16 contextvars==2.4;python_version<"3.7" -importlib-metadata==0.18 +importlib-metadata==0.19 jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.7 pip>=8.0.3 -python-slugify==3.0.2 -pytz>=2019.01 -pyyaml==5.1.1 +python-slugify==3.0.3 +pytz>=2019.02 +pyyaml==5.1.2 requests==2.22.0 -ruamel.yaml==0.15.99 -voluptuous==0.11.5 -voluptuous-serialize==2.1.0 +ruamel.yaml==0.15.100 +voluptuous==0.11.7 +voluptuous-serialize==2.2.0 # homeassistant.components.nuimo_controller --only-binary=all nuimo==0.1.0 @@ -38,13 +38,13 @@ Adafruit-SHT31==1.0.2 HAP-python==2.5.0 # homeassistant.components.mastodon -Mastodon.py==1.4.5 +Mastodon.py==1.4.6 # homeassistant.components.orangepi_gpio OPi.GPIO==0.3.6 # homeassistant.components.essent -PyEssent==0.12 +PyEssent==0.13 # homeassistant.components.github PyGithub==1.43.5 @@ -75,7 +75,7 @@ PyRMVtransport==0.1.3 PyTransportNSW==0.1.1 # homeassistant.components.xiaomi_aqara -PyXiaomiGateway==0.12.3 +PyXiaomiGateway==0.12.4 # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio @@ -114,8 +114,11 @@ adguardhome==0.2.1 # homeassistant.components.frontier_silicon afsapi==0.0.4 +# homeassistant.components.geonetnz_quakes +aio_geojson_geonetnz_quakes==0.9 + # homeassistant.components.ambient_station -aioambient==0.3.1 +aioambient==0.3.2 # homeassistant.components.asuswrt aioasuswrt==1.1.21 @@ -191,7 +194,7 @@ ambiclimate==0.2.0 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.18 +androidtv==0.0.24 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 @@ -257,13 +260,13 @@ batinfo==0.4.2 # homeassistant.components.linksys_ap # homeassistant.components.scrape # homeassistant.components.sytadin -beautifulsoup4==4.7.1 +beautifulsoup4==4.8.0 # homeassistant.components.zha -bellows-homeassistant==0.9.0 +bellows-homeassistant==0.9.1 # homeassistant.components.bmw_connected_drive -bimmer_connected==0.5.3 +bimmer_connected==0.6.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 @@ -415,9 +418,6 @@ ebusdpy==0.0.16 # homeassistant.components.ecoal_boiler ecoaliface==0.4.0 -# homeassistant.components.edp_redy -edp_redy==0.0.3 - # homeassistant.components.ee_brightbox eebrightbox==0.0.4 @@ -437,7 +437,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.20 +env_canada==0.0.24 # homeassistant.components.envirophat # envirophat==0.0.6 @@ -455,7 +455,7 @@ epson-projector==0.1.3 epsonprinter==0.0.9 # homeassistant.components.netgear_lte -eternalegypt==0.0.8 +eternalegypt==0.0.10 # homeassistant.components.keyboard_remote # evdev==0.6.1 @@ -518,7 +518,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.5.4 +geniushub-client==0.6.5 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed @@ -538,6 +538,7 @@ georss_ign_sismologia_client==0.2 georss_qld_bushfire_alert_client==0.3 # homeassistant.components.braviatv +# homeassistant.components.huawei_lte # homeassistant.components.nmap_tracker getmac==0.8.1 @@ -559,9 +560,6 @@ google-cloud-pubsub==0.39.1 # homeassistant.components.google_cloud google-cloud-texttospeech==0.4.0 -# homeassistant.components.googlehome -googledevices==1.0.2 - # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -586,6 +584,9 @@ ha-ffmpeg==2.0 # homeassistant.components.philips_js ha-philipsjs==0.0.8 +# homeassistant.components.plugwise +haanna==0.10.1 + # homeassistant.components.habitica habitipy==0.2.0 @@ -593,7 +594,7 @@ habitipy==0.2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.16 +hass-nabucasa==0.17 # homeassistant.components.mqtt hbmqtt==0.9.4 @@ -617,13 +618,13 @@ hkavr==0.0.5 hlk-sw16==0.0.7 # homeassistant.components.pi_hole -hole==0.3.0 +hole==0.5.0 # homeassistant.components.workday holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190805.0 +home-assistant-frontend==20190828.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 @@ -688,6 +689,9 @@ jsonrpc-async==0.6 # homeassistant.components.kodi jsonrpc-websocket==0.6 +# homeassistant.components.keba +keba-kecontact==0.2.0 + # homeassistant.scripts.keyring keyring==17.1.1 @@ -710,13 +714,13 @@ libpurecool==0.5.0 libpyfoscam==1.0 # homeassistant.components.mikrotik -librouteros==2.2.0 +librouteros==2.3.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 # homeassistant.components.life360 -life360==4.0.1 +life360==4.1.1 # homeassistant.components.lifx_legacy liffylights==0.9.4 @@ -749,7 +753,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.2 +luftdaten==0.6.3 # homeassistant.components.lupusec lupupy==0.0.17 @@ -790,6 +794,9 @@ miflora==0.4.0 # homeassistant.components.mill millheater==0.3.4 +# homeassistant.components.minio +minio==4.0.9 + # homeassistant.components.mitemp_bt mitemp_bt==0.0.1 @@ -849,7 +856,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.16.4 +numpy==1.17.0 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -887,6 +894,9 @@ openwrt-luci-rpc==1.1.0 # homeassistant.components.orvibo orvibo==1.1.1 +# homeassistant.components.luci +packaging==19.1 + # homeassistant.components.mqtt # homeassistant.components.shiftr paho-mqtt==1.4.0 @@ -924,7 +934,7 @@ pilight==0.1.1 # homeassistant.components.proxy # homeassistant.components.qrcode # homeassistant.components.tensorflow -pillow==5.4.1 +pillow==6.1.0 # homeassistant.components.dominos pizzapi==0.0.3 @@ -958,7 +968,7 @@ prezzibenzina-py==1.1.4 proliphix==0.4.1 # homeassistant.components.prometheus -prometheus_client==0.2.0 +prometheus_client==0.7.1 # homeassistant.components.tensorflow protobuf==3.6.1 @@ -1064,7 +1074,7 @@ pyblackbird==0.5 pybotvac==0.0.15 # homeassistant.components.nissan_leaf -pycarwings2==2.8 +pycarwings2==2.9 # homeassistant.components.cloudflare pycfdns==0.0.1 @@ -1179,7 +1189,7 @@ pygtt==1.1.2 pyhaversion==3.0.2 # homeassistant.components.heos -pyheos==0.5.2 +pyheos==0.6.0 # homeassistant.components.hikvision pyhik==0.2.3 @@ -1224,7 +1234,7 @@ pykira==0.1.1 pykwb==0.0.8 # homeassistant.components.lacrosse -pylacrosse==0.3.1 +pylacrosse==0.4.0 # homeassistant.components.lastfm pylast==3.1.0 @@ -1251,7 +1261,7 @@ pyloopenergy==0.1.3 pylutron-caseta==0.5.0 # homeassistant.components.lutron -pylutron==0.2.1 +pylutron==0.2.2 # homeassistant.components.mailgun pymailgunner==1.4 @@ -1349,7 +1359,7 @@ pyqwikswitch==0.93 pyrail==0.0.3 # homeassistant.components.rainbird -pyrainbird==0.1.6 +pyrainbird==0.2.1 # homeassistant.components.recswitch pyrecswitch==1.0.2 @@ -1357,9 +1367,6 @@ pyrecswitch==1.0.2 # homeassistant.components.repetier pyrepetier==3.0.5 -# homeassistant.components.ruter -pyruter==1.1.0 - # homeassistant.components.sabnzbd pysabnzbd==1.1.0 @@ -1382,7 +1389,7 @@ pysesame2==1.0.1 pysher==1.0.1 # homeassistant.components.sma -pysma==0.3.2 +pysma==0.3.4 # homeassistant.components.smartthings pysmartapp==0.3.2 @@ -1394,10 +1401,10 @@ pysmartthings==0.6.9 pysmarty==0.8 # homeassistant.components.snmp -pysnmp==4.4.9 +pysnmp==4.4.11 # homeassistant.components.sonos -pysonos==0.0.22 +pysonos==0.0.23 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -1415,7 +1422,7 @@ pysuez==0.1.17 pysupla==0.0.3 # homeassistant.components.syncthru -pysyncthru==0.4.2 +pysyncthru==0.4.3 # homeassistant.components.tautulli pytautulli==0.5.0 @@ -1566,7 +1573,7 @@ pyuptimerobot==0.0.5 # pyuserinput==0.1.11 # homeassistant.components.vera -pyvera==0.3.3 +pyvera==0.3.4 # homeassistant.components.vesync pyvesync==1.1.0 @@ -1620,7 +1627,7 @@ recollect-waste==1.0.1 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0b8 +restrictedpython==4.0 # homeassistant.components.idteck_prox rfk101py==0.0.1 @@ -1686,16 +1693,16 @@ sense_energy==0.7.0 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.13.0 +shodan==1.15.0 # homeassistant.components.simplepush simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==4.2.0 +simplisafe-python==4.3.0 # homeassistant.components.sisyphus -sisyphus-control==2.2 +sisyphus-control==2.2.1 # homeassistant.components.skybell skybellpy==0.4.0 @@ -1761,7 +1768,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.5 +sqlalchemy==1.3.7 # homeassistant.components.srp_energy srpenergy==1.0.6 @@ -1816,6 +1823,9 @@ temescal==0.1 # homeassistant.components.temper temperusb==1.5.3 +# homeassistant.components.tensorflow +# tensorflow==1.13.2 + # homeassistant.components.tesla teslajsonpy==0.0.25 @@ -1847,7 +1857,7 @@ tplink==0.2.1 transmissionrpc==0.11 # homeassistant.components.tuya -tuyaha==0.0.2 +tuyaha==0.0.3 # homeassistant.components.twentemilieu twentemilieu==0.1.0 @@ -1955,7 +1965,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.07.16 +youtube_dl==2019.08.13 # homeassistant.components.zengge zengge==0.2 @@ -1964,7 +1974,7 @@ zengge==0.2 zeroconf==0.23.0 # homeassistant.components.zha -zha-quirks==0.0.20 +zha-quirks==0.0.22 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -1973,13 +1983,16 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.2.1 +zigpy-deconz==0.2.2 # homeassistant.components.zha -zigpy-homeassistant==0.7.0 +zigpy-homeassistant==0.7.1 # homeassistant.components.zha zigpy-xbee-homeassistant==0.4.0 +# homeassistant.components.zha +zigpy-zigate==0.1.0 + # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test.txt b/requirements_test.txt index 7589c9ec27d..bfe459b0cfb 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,17 +6,16 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -coveralls==1.2.0 -flake8-docstrings==1.3.0 +flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 -pydocstyle==3.0.0 +pre-commit==1.18.2 +pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.0.1 +pytest==5.1.1 requests_mock==1.6.0 -pre-commit==1.17.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26630c11f67..1aad0450390 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,20 +7,19 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -coveralls==1.2.0 -flake8-docstrings==1.3.0 +flake8-docstrings==1.3.1 flake8==3.7.8 mock-open==1.3.1 mypy==0.720 -pydocstyle==3.0.0 +pre-commit==1.18.2 +pydocstyle==4.0.1 pylint==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.0.1 +pytest==5.1.1 requests_mock==1.6.0 -pre-commit==1.17.0 # homeassistant.components.homekit @@ -42,8 +41,11 @@ YesssSMS==0.2.3 # homeassistant.components.adguard adguardhome==0.2.1 +# homeassistant.components.geonetnz_quakes +aio_geojson_geonetnz_quakes==0.9 + # homeassistant.components.ambient_station -aioambient==0.3.1 +aioambient==0.3.2 # homeassistant.components.automatic aioautomatic==0.6.5 @@ -89,7 +91,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.9.0 +bellows-homeassistant==0.9.1 # homeassistant.components.caldav caldav==0.6.1 @@ -147,6 +149,11 @@ georss_ign_sismologia_client==0.2 # homeassistant.components.qld_bushfire georss_qld_bushfire_alert_client==0.3 +# homeassistant.components.braviatv +# homeassistant.components.huawei_lte +# homeassistant.components.nmap_tracker +getmac==0.8.1 + # homeassistant.components.google google-api-python-client==1.6.4 @@ -157,7 +164,7 @@ ha-ffmpeg==2.0 hangups==0.4.9 # homeassistant.components.cloud -hass-nabucasa==0.16 +hass-nabucasa==0.17 # homeassistant.components.mqtt hbmqtt==0.9.4 @@ -169,7 +176,7 @@ hdate==0.9.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20190805.0 +home-assistant-frontend==20190828.0 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 @@ -197,7 +204,7 @@ libpurecool==0.5.0 libsoundtouch==0.7.2 # homeassistant.components.luftdaten -luftdaten==0.6.2 +luftdaten==0.6.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 @@ -205,6 +212,9 @@ mbddns==0.1.2 # homeassistant.components.mfi mficlient==0.3.0 +# homeassistant.components.minio +minio==4.0.9 + # homeassistant.components.discovery # homeassistant.components.ssdp netdisco==2.6.0 @@ -213,7 +223,7 @@ netdisco==2.6.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.16.4 +numpy==1.17.0 # homeassistant.components.google oauth2client==4.0.0 @@ -236,7 +246,7 @@ pilight==0.1.1 pmsensor==0.4 # homeassistant.components.prometheus -prometheus_client==0.2.0 +prometheus_client==0.7.1 # homeassistant.components.ptvsd ptvsd==4.2.8 @@ -267,7 +277,7 @@ pydeconz==62 pydispatcher==2.0.5 # homeassistant.components.heos -pyheos==0.5.2 +pyheos==0.6.0 # homeassistant.components.homematic pyhomematic==0.1.60 @@ -301,6 +311,9 @@ pyps4-homeassistant==0.8.7 # homeassistant.components.qwikswitch pyqwikswitch==0.93 +# homeassistant.components.sma +pysma==0.3.4 + # homeassistant.components.smartthings pysmartapp==0.3.2 @@ -308,7 +321,7 @@ pysmartapp==0.3.2 pysmartthings==0.6.9 # homeassistant.components.sonos -pysonos==0.0.22 +pysonos==0.0.23 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -338,7 +351,7 @@ pywebpush==1.9.2 regenmaschine==1.5.1 # homeassistant.components.python_script -restrictedpython==4.0b8 +restrictedpython==4.0 # homeassistant.components.rflink rflink==0.0.46 @@ -350,7 +363,7 @@ ring_doorbell==0.2.3 rxv==0.6.0 # homeassistant.components.simplisafe -simplisafe-python==4.2.0 +simplisafe-python==4.3.0 # homeassistant.components.sleepiq sleepyq==0.7 @@ -363,7 +376,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.5 +sqlalchemy==1.3.7 # homeassistant.components.srp_energy srpenergy==1.0.6 @@ -395,4 +408,4 @@ wakeonlan==1.1.6 zeroconf==0.23.0 # homeassistant.components.zha -zigpy-homeassistant==0.7.0 +zigpy-homeassistant==0.7.1 diff --git a/script/check_format b/script/check_format index ec403c723b3..bed35ec63e4 100755 --- a/script/check_format +++ b/script/check_format @@ -7,4 +7,4 @@ black \ --check \ --fast \ --quiet \ - homeassistant tests script + homeassistant tests script *.py diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 965f881b9c7..6643fcf7aa9 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -38,11 +38,13 @@ COMMENT_REQUIREMENTS = ( "rpi-rf", "RPi.GPIO", "smbus-cffi", + "tensorflow", ) TEST_REQUIREMENTS = ( "adguardhome", "ambiclimate", + "aio_geojson_geonetnz_quakes", "aioambient", "aioautomatic", "aiobotocore", @@ -73,6 +75,7 @@ TEST_REQUIREMENTS = ( "georss_generic_client", "georss_ign_sismologia_client", "georss_qld_bushfire_alert_client", + "getmac", "google-api-python-client", "gTTS-token", "ha-ffmpeg", @@ -96,6 +99,7 @@ TEST_REQUIREMENTS = ( "pyMetno", "mbddns", "mficlient", + "minio", "netdisco", "numpy", "oauth2client", @@ -120,6 +124,7 @@ TEST_REQUIREMENTS = ( "pyopenuv", "pyotp", "pyps4-homeassistant", + "pysma", "pysmartapp", "pysmartthings", "pysonos", @@ -215,7 +220,7 @@ def core_requirements(): """Gather core requirements out of setup.py.""" with open("setup.py") as inp: reqs_raw = re.search(r"REQUIRES = \[(.*?)\]", inp.read(), re.S).group(1) - return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)] + return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)] def gather_recursive_requirements(domain, seen=None): diff --git a/script/hassfest/codeowners.py b/script/hassfest/codeowners.py index 07a1d827b33..1341bd75d1b 100755 --- a/script/hassfest/codeowners.py +++ b/script/hassfest/codeowners.py @@ -15,10 +15,6 @@ homeassistant/*.py @home-assistant/core homeassistant/helpers/* @home-assistant/core homeassistant/util/* @home-assistant/core -# Virtualization -Dockerfile @home-assistant/docker -virtualization/Docker/* @home-assistant/docker - # Other code homeassistant/scripts/check_config.py @kellerza diff --git a/script/test b/script/test index 14fc357eb12..8c4688a4d65 100755 --- a/script/test +++ b/script/test @@ -3,4 +3,4 @@ cd "$(dirname "$0")/.." -tox -e py35 +tox -e py36 diff --git a/setup.cfg b/setup.cfg index c6ff25bb362..49f738cf969 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,3 +52,20 @@ default_section = THIRDPARTY known_first_party = homeassistant,tests forced_separate = tests combine_as_imports = true + +[mypy] +python_version = 3.6 +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_calls = true +disallow_untyped_defs = true +follow_imports = silent +ignore_missing_imports = true +no_implicit_optional = true +strict_equality = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true diff --git a/setup.py b/setup.py index da50b5f988c..5ab8d74c64c 100755 --- a/setup.py +++ b/setup.py @@ -38,19 +38,19 @@ REQUIRES = [ "bcrypt==3.1.7", "certifi>=2019.6.16", 'contextvars==2.4;python_version<"3.7"', - "importlib-metadata==0.18", + "importlib-metadata==0.19", "jinja2>=2.10.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==2.7", "pip>=8.0.3", - "python-slugify==3.0.2", - "pytz>=2019.01", - "pyyaml==5.1.1", + "python-slugify==3.0.3", + "pytz>=2019.02", + "pyyaml==5.1.2", "requests==2.22.0", - "ruamel.yaml==0.15.99", - "voluptuous==0.11.5", - "voluptuous-serialize==2.1.0", + "ruamel.yaml==0.15.100", + "voluptuous==0.11.7", + "voluptuous-serialize==2.2.0", ] MIN_PY_VERSION = ".".join(map(str, hass_const.REQUIRED_PYTHON_VER)) diff --git a/tests/common.py b/tests/common.py index 3ca4046db85..0e2f701c210 100644 --- a/tests/common.py +++ b/tests/common.py @@ -665,6 +665,7 @@ class MockConfigEntry(config_entries.ConfigEntry): title="Mock Title", state=None, options={}, + system_options={}, connection_class=config_entries.CONN_CLASS_UNKNOWN, ): """Initialize a mock config entry.""" @@ -672,6 +673,7 @@ class MockConfigEntry(config_entries.ConfigEntry): "entry_id": entry_id or uuid.uuid4().hex, "domain": domain, "data": data or {}, + "system_options": system_options, "options": options, "version": version, "title": title, @@ -908,6 +910,11 @@ class MockEntity(entity.Entity): """Info how it links to a device.""" return self._handle("device_info") + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return self._handle("entity_registry_enabled_default") + def _handle(self, attr): """Return attribute value.""" if attr in self._values: @@ -971,6 +978,8 @@ async def flush_store(store): if store._data is None: return + store._async_cleanup_stop_listener() + store._async_cleanup_delay_listener() await store._async_handle_write_data() diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 9f745792e59..f6bb4c9cc29 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -5,7 +5,7 @@ from . import TEST_URL, DEFAULT_CONFIG async def test_report_state(hass, aioclient_mock): """Test proactive state reports.""" - aioclient_mock.post(TEST_URL, json={"data": "is irrelevant"}) + aioclient_mock.post(TEST_URL, json={"data": "is irrelevant"}, status=202) hass.states.async_set( "binary_sensor.test_contact", diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index e04d1599e4c..2668ac97053 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -617,3 +617,227 @@ async def test_if_action_before_and_after_during(hass, calls): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert 3 == len(calls) + + +async def test_if_action_before_sunrise_no_offset_kotzebue(hass, calls): + """ + Test if action was before sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + After sunrise is true from sunrise until midnight, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local + # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # now = sunrise + 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 15, 17, 25, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 0 == len(calls) + + # now = sunrise -> 'before sunrise' true + now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + +async def test_if_action_after_sunrise_no_offset_kotzebue(hass, calls): + """ + Test if action was after sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + Before sunrise is true from midnight until sunrise, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNRISE}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local + # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # now = sunrise -> 'after sunrise' true + now = datetime(2015, 7, 24, 15, 17, 24, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = sunrise - 1s -> 'after sunrise' not true + now = datetime(2015, 7, 24, 15, 17, 23, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight -> 'after sunrise' not true + now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight - 1s -> 'after sunrise' true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + +async def test_if_action_before_sunset_no_offset_kotzebue(hass, calls): + """ + Test if action was before sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + Before sunset is true from midnight until sunset, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "before": SUN_EVENT_SUNSET}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local + # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # now = sunrise + 1s -> 'before sunrise' not true + now = datetime(2015, 7, 25, 11, 16, 28, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 0 == len(calls) + + # now = sunrise -> 'before sunrise' true + now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight -> 'before sunrise' true + now = datetime(2015, 7, 24, 8, 0, 0, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + # now = local midnight - 1s -> 'before sunrise' not true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) + + +async def test_if_action_after_sunset_no_offset_kotzebue(hass, calls): + """ + Test if action was after sunrise. + + Local timezone: Alaska time + Location: Kotzebue, which has a very skewed local timezone with sunrise + at 7 AM and sunset at 3AM during summer + After sunset is true from sunset until midnight, local time. + """ + tz = dt_util.get_time_zone("America/Anchorage") + dt_util.set_default_time_zone(tz) + hass.config.latitude = 66.5 + hass.config.longitude = 162.4 + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "event", "event_type": "test_event"}, + "condition": {"condition": "sun", "after": SUN_EVENT_SUNSET}, + "action": {"service": "test.automation"}, + } + }, + ) + + # sunrise: 2015-07-24 07:17:24 local, sunset: 2015-07-25 03:16:27 local + # sunrise: 2015-07-24 15:17:24 UTC, sunset: 2015-07-25 11:16:27 UTC + # now = sunset -> 'after sunset' true + now = datetime(2015, 7, 25, 11, 16, 27, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = sunset - 1s -> 'after sunset' not true + now = datetime(2015, 7, 25, 11, 16, 26, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight -> 'after sunset' not true + now = datetime(2015, 7, 24, 8, 0, 1, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 1 == len(calls) + + # now = local midnight - 1s -> 'after sunset' true + now = datetime(2015, 7, 24, 7, 59, 59, tzinfo=dt_util.UTC) + with patch("homeassistant.util.dt.utcnow", return_value=now): + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert 2 == len(calls) diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 6a58812cbaf..8e5a6f9675d 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -57,6 +57,7 @@ async def setup_device(hass): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, options=ENTRY_OPTIONS, ) device = axis.AxisNetworkDevice(hass, config_entry) diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index f4871357d0e..027dc42748e 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -41,6 +41,7 @@ async def setup_device(hass): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, options=ENTRY_OPTIONS, ) device = axis.AxisNetworkDevice(hass, config_entry) diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index a7d67e39f44..5ec3f933e9e 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -39,6 +39,8 @@ async def test_flow_works(hass): mock_device.side_effect = mock_constructor mock_device.vapix.params.system_serialnumber = "serialnumber" mock_device.vapix.params.prodnbr = "prodnbr" + mock_device.vapix.params.prodtype = "prodtype" + mock_device.vapix.params.firmware_version = "firmware_version" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "user"} @@ -220,6 +222,10 @@ async def test_zeroconf_flow_known_device(hass): return mock_device mock_device.side_effect = mock_constructor + mock_device.vapix.params.system_serialnumber = "serialnumber" + mock_device.vapix.params.prodnbr = "prodnbr" + mock_device.vapix.params.prodtype = "prodtype" + mock_device.vapix.params.firmware_version = "firmware_version" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, @@ -338,6 +344,8 @@ async def test_import_flow_works(hass): mock_device.side_effect = mock_constructor mock_device.vapix.params.system_serialnumber = "serialnumber" mock_device.vapix.params.prodnbr = "prodnbr" + mock_device.vapix.params.prodtype = "prodtype" + mock_device.vapix.params.firmware_version = "firmware_version" result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 6595f172ef0..3469106c436 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -59,6 +59,7 @@ async def setup_device(hass): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, options=ENTRY_OPTIONS, ) device = axis.AxisNetworkDevice(hass, config_entry) diff --git a/tests/components/climate/test_reproduce_state.py b/tests/components/climate/test_reproduce_state.py index 293cebdb778..fe995868840 100644 --- a/tests/components/climate/test_reproduce_state.py +++ b/tests/components/climate/test_reproduce_state.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.climate import async_reproduce_states +from homeassistant.components.climate.reproduce_state import async_reproduce_states from homeassistant.components.climate.const import ( ATTR_AUX_HEAT, ATTR_HUMIDITY, diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index 688d69c16f1..22d8c64c3b0 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -52,16 +52,17 @@ def patch_sync_helper(): to_update = [] to_remove = [] + async def sync_helper(to_upd, to_rem): + to_update.extend([ent_id for ent_id in to_upd if ent_id not in to_update]) + to_remove.extend([ent_id for ent_id in to_rem if ent_id not in to_remove]) + return True + with patch("homeassistant.components.cloud.alexa_config.SYNC_DELAY", 0), patch( "homeassistant.components.cloud.alexa_config.AlexaConfig._sync_helper", - side_effect=mock_coro, - ) as mock_helper: + side_effect=sync_helper, + ): yield to_update, to_remove - actual_to_update, actual_to_remove = mock_helper.mock_calls[0][1] - to_update.extend(actual_to_update) - to_remove.extend(actual_to_remove) - async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs): """Test Alexa config responds to updating exposed entities.""" diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 563fc639b76..3d22d3ac1a7 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -37,65 +37,61 @@ def client(hass, hass_client): yield hass.loop.run_until_complete(hass_client()) -@HANDLERS.register("comp1") -class Comp1ConfigFlow: - """Config flow with options flow.""" - - @staticmethod - @callback - def async_get_options_flow(config, options): - """Get options flow.""" - pass - - -@HANDLERS.register("comp2") -class Comp2ConfigFlow: - """Config flow without options flow.""" - - def __init__(self): - """Init.""" - pass - - async def test_get_entries(hass, client): """Test get entries.""" - MockConfigEntry( - domain="comp1", - title="Test 1", - source="bla", - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, - ).add_to_hass(hass) - MockConfigEntry( - domain="comp2", - title="Test 2", - source="bla2", - state=core_ce.ENTRY_STATE_LOADED, - connection_class=core_ce.CONN_CLASS_ASSUMED, - ).add_to_hass(hass) + with patch.dict(HANDLERS, clear=True): - resp = await client.get("/api/config/config_entries/entry") - assert resp.status == 200 - data = await resp.json() - for entry in data: - entry.pop("entry_id") - assert data == [ - { - "domain": "comp1", - "title": "Test 1", - "source": "bla", - "state": "not_loaded", - "connection_class": "local_poll", - "supports_options": True, - }, - { - "domain": "comp2", - "title": "Test 2", - "source": "bla2", - "state": "loaded", - "connection_class": "assumed", - "supports_options": False, - }, - ] + @HANDLERS.register("comp1") + class Comp1ConfigFlow: + """Config flow with options flow.""" + + @staticmethod + @callback + def async_get_options_flow(config, options): + """Get options flow.""" + pass + + hass.helpers.config_entry_flow.register_discovery_flow( + "comp2", "Comp 2", lambda: None, core_ce.CONN_CLASS_ASSUMED + ) + + MockConfigEntry( + domain="comp1", + title="Test 1", + source="bla", + connection_class=core_ce.CONN_CLASS_LOCAL_POLL, + ).add_to_hass(hass) + MockConfigEntry( + domain="comp2", + title="Test 2", + source="bla2", + state=core_ce.ENTRY_STATE_LOADED, + connection_class=core_ce.CONN_CLASS_ASSUMED, + ).add_to_hass(hass) + + resp = await client.get("/api/config/config_entries/entry") + assert resp.status == 200 + data = await resp.json() + for entry in data: + entry.pop("entry_id") + assert data == [ + { + "domain": "comp1", + "title": "Test 1", + "source": "bla", + "state": "not_loaded", + "connection_class": "local_poll", + "supports_options": True, + }, + { + "domain": "comp2", + "title": "Test 2", + "source": "bla2", + "state": "loaded", + "connection_class": "assumed", + "supports_options": False, + }, + ] @asyncio.coroutine @@ -377,40 +373,46 @@ async def test_continue_flow_unauth(hass, client, hass_admin_user): assert resp.status == 401 -@asyncio.coroutine -def test_get_progress_index(hass, client): +async def test_get_progress_index(hass, hass_ws_client): """Test querying for the flows that are in progress.""" + assert await async_setup_component(hass, "config", {}) mock_entity_platform(hass, "config_flow.test", None) + ws_client = await hass_ws_client(hass) class TestFlow(core_ce.ConfigFlow): VERSION = 5 - @asyncio.coroutine - def async_step_hassio(self, info): - return (yield from self.async_step_account()) + async def async_step_hassio(self, info): + return await self.async_step_account() - @asyncio.coroutine - def async_step_account(self, user_input=None): + async def async_step_account(self, user_input=None): return self.async_show_form(step_id="account") with patch.dict(HANDLERS, {"test": TestFlow}): - form = yield from hass.config_entries.flow.async_init( + form = await hass.config_entries.flow.async_init( "test", context={"source": "hassio"} ) - resp = yield from client.get("/api/config/config_entries/flow") - assert resp.status == 200 - data = yield from resp.json() - assert data == [ + await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"}) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == [ {"flow_id": form["flow_id"], "handler": "test", "context": {"source": "hassio"}} ] -async def test_get_progress_index_unauth(hass, client, hass_admin_user): +async def test_get_progress_index_unauth(hass, hass_ws_client, hass_admin_user): """Test we can't get flows that are in progress.""" + assert await async_setup_component(hass, "config", {}) hass_admin_user.groups = [] - resp = await client.get("/api/config/config_entries/flow") - assert resp.status == 401 + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 5, "type": "config_entries/flow/progress"}) + response = await ws_client.receive_json() + + assert not response["success"] + assert response["error"]["code"] == "unauthorized" @asyncio.coroutine @@ -488,12 +490,8 @@ async def test_options_flow(hass, client): class TestFlow(core_ce.ConfigFlow): @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): class OptionsFlowHandler(data_entry_flow.FlowHandler): - def __init__(self, config, options): - self.config = config - self.options = options - async def async_step_init(self, user_input=None): schema = OrderedDict() schema[vol.Required("enabled")] = bool @@ -503,7 +501,7 @@ async def test_options_flow(hass, client): description_placeholders={"enabled": "Set to true to be true"}, ) - return OptionsFlowHandler(config, options) + return OptionsFlowHandler() MockConfigEntry( domain="test", @@ -514,7 +512,7 @@ async def test_options_flow(hass, client): entry = hass.config_entries._entries[0] with patch.dict(HANDLERS, {"test": TestFlow}): - url = "/api/config/config_entries/entry/option/flow" + url = "/api/config/config_entries/options/flow" resp = await client.post(url, json={"handler": entry.entry_id}) assert resp.status == 200 @@ -538,12 +536,8 @@ async def test_two_step_options_flow(hass, client): class TestFlow(core_ce.ConfigFlow): @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): class OptionsFlowHandler(data_entry_flow.FlowHandler): - def __init__(self, config, options): - self.config = config - self.options = options - async def async_step_init(self, user_input=None): return self.async_show_form( step_id="finish", data_schema=vol.Schema({"enabled": bool}) @@ -554,7 +548,7 @@ async def test_two_step_options_flow(hass, client): title="Enable disable", data=user_input ) - return OptionsFlowHandler(config, options) + return OptionsFlowHandler() MockConfigEntry( domain="test", @@ -565,7 +559,7 @@ async def test_two_step_options_flow(hass, client): entry = hass.config_entries._entries[0] with patch.dict(HANDLERS, {"test": TestFlow}): - url = "/api/config/config_entries/entry/option/flow" + url = "/api/config/config_entries/options/flow" resp = await client.post(url, json={"handler": entry.entry_id}) assert resp.status == 200 @@ -596,3 +590,47 @@ async def test_two_step_options_flow(hass, client): "description": None, "description_placeholders": None, } + + +async def test_list_system_options(hass, hass_ws_client): + """Test that we can list an entries system options.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + entry = MockConfigEntry(domain="demo") + entry.add_to_hass(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/system_options/list", + "entry_id": entry.entry_id, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"] == {"disable_new_entities": False} + + +async def test_update_system_options(hass, hass_ws_client): + """Test that we can update system options.""" + assert await async_setup_component(hass, "config", {}) + ws_client = await hass_ws_client(hass) + + entry = MockConfigEntry(domain="demo") + entry.add_to_hass(hass) + + await ws_client.send_json( + { + "id": 5, + "type": "config_entries/system_options/update", + "entry_id": entry.entry_id, + "disable_new_entities": True, + } + ) + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"]["disable_new_entities"] + assert entry.system_options.disable_new_entities diff --git a/tests/components/config/test_entity_registry.py b/tests/components/config/test_entity_registry.py index 5f8c6f51acb..9472d888254 100644 --- a/tests/components/config/test_entity_registry.py +++ b/tests/components/config/test_entity_registry.py @@ -105,9 +105,9 @@ async def test_get_entity(hass, client): } -async def test_update_entity_name(hass, client): - """Test updating entity name.""" - mock_registry( +async def test_update_entity(hass, client): + """Test updating entity.""" + registry = mock_registry( hass, { "test_domain.world": RegistryEntry( @@ -127,6 +127,7 @@ async def test_update_entity_name(hass, client): assert state is not None assert state.name == "before update" + # UPDATE NAME await client.send_json( { "id": 6, @@ -150,6 +151,42 @@ async def test_update_entity_name(hass, client): state = hass.states.get("test_domain.world") assert state.name == "after update" + # UPDATE DISABLED_BY TO USER + await client.send_json( + { + "id": 7, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "disabled_by": "user", + } + ) + + msg = await client.receive_json() + + assert hass.states.get("test_domain.world") is None + assert registry.entities["test_domain.world"].disabled_by == "user" + + # UPDATE DISABLED_BY TO NONE + await client.send_json( + { + "id": 8, + "type": "config/entity_registry/update", + "entity_id": "test_domain.world", + "disabled_by": None, + } + ) + + msg = await client.receive_json() + + assert msg["result"] == { + "config_entry_id": None, + "device_id": None, + "disabled_by": None, + "platform": "test_platform", + "entity_id": "test_domain.world", + "name": "after update", + } + async def test_update_entity_no_changes(hass, client): """Test update entity with no changes.""" diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 9eb408ba4f1..acf06728d0d 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -59,7 +59,8 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, - ENTRY_OPTIONS, + system_options={}, + options=ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) @@ -115,6 +116,7 @@ async def test_add_new_sensor(hass): sensor.name = "name" sensor.type = "ZHAPresence" sensor.BINARY = True + sensor.uniqueid = "1" sensor.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 264c3b8761f..f4972564a8e 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -71,7 +71,8 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, - ENTRY_OPTIONS, + system_options={}, + options=ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(hass.loop, session, **config_entry.data) @@ -178,6 +179,7 @@ async def test_add_new_climate_device(hass): sensor = Mock() sensor.name = "name" sensor.type = "ZHAThermostat" + sensor.uniqueid = "1" sensor.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index c6f3a4b12c8..7230ff4fb7b 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -16,7 +16,7 @@ SUPPORTED_COVERS = { "id": "Cover 1 id", "name": "Cover 1 name", "type": "Level controllable output", - "state": {"bri": 255, "reachable": True}, + "state": {"bri": 255, "on": False, "reachable": True}, "modelid": "Not zigbee spec", "uniqueid": "00:00:00:00:00:00:00:00-00", }, @@ -24,7 +24,7 @@ SUPPORTED_COVERS = { "id": "Cover 2 id", "name": "Cover 2 name", "type": "Window covering device", - "state": {"bri": 255, "reachable": True}, + "state": {"bri": 255, "on": True, "reachable": True}, "modelid": "lumi.curtain", }, } @@ -63,6 +63,7 @@ async def setup_gateway(hass, data): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) @@ -106,7 +107,7 @@ async def test_cover(hass): cover_1 = hass.states.get("cover.cover_1_name") assert cover_1 is not None - assert cover_1.state == "closed" + assert cover_1.state == "open" gateway.api.lights["1"].async_update({}) @@ -132,6 +133,7 @@ async def test_add_new_cover(hass): cover = Mock() cover.name = "name" cover.type = "Level controllable output" + cover.uniqueid = "1" cover.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("light"), [cover]) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 77e983e34b4..afe7ca445e5 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -90,7 +90,8 @@ async def setup_gateway(hass, data, allow_deconz_groups=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, - ENTRY_OPTIONS, + system_options={}, + options=ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) @@ -190,6 +191,7 @@ async def test_add_new_light(hass): gateway = await setup_gateway(hass, {}) light = Mock() light.name = "name" + light.uniqueid = "1" light.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("light"), [light]) await hass.async_block_till_done() diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index 48287d5200a..074e943548d 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -46,6 +46,7 @@ async def setup_gateway(hass, data): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 9c03f3e9a90..fa1ba175ed5 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -103,7 +103,8 @@ async def setup_gateway(hass, data, allow_clip_sensor=True): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, - ENTRY_OPTIONS, + system_options={}, + options=ENTRY_OPTIONS, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) @@ -158,6 +159,7 @@ async def test_add_new_sensor(hass): sensor = Mock() sensor.name = "name" sensor.type = "ZHATemperature" + sensor.uniqueid = "1" sensor.BINARY = False sensor.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor]) diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 8275e91ecf9..746d1b6342c 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -67,6 +67,7 @@ async def setup_gateway(hass, data): ENTRY_CONFIG, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) gateway = deconz.DeconzGateway(hass, config_entry) gateway.api = DeconzSession(loop, session, **config_entry.data) @@ -140,6 +141,7 @@ async def test_add_new_switch(hass): switch = Mock() switch.name = "name" switch.type = "Smart plug" + switch.uniqueid = "1" switch.register_async_callback = Mock() async_dispatcher_send(hass, gateway.async_event_new_device("light"), [switch]) await hass.async_block_till_done() diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 25e563ebf9b..18a03ff2603 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -1,5 +1,6 @@ """The tests for the Dialogflow component.""" import json +import copy from unittest.mock import Mock import pytest @@ -90,108 +91,16 @@ async def fixture(hass, aiohttp_client): return await aiohttp_client(hass.http.app), webhook_id -async def test_intent_action_incomplete(fixture): - """Test when action is not completed.""" - mock_client, webhook_id = fixture - data = { +class _Data: + _v1 = { "id": REQUEST_ID, "timestamp": REQUEST_TIMESTAMP, "result": { "source": "agent", "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": True, - "parameters": {"ZodiacSign": "virgo"}, - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } - - response = await mock_client.post( - "/api/webhook/{}".format(webhook_id), data=json.dumps(data) - ) - assert 200 == response.status - assert "" == await response.text() - - -async def test_intent_slot_filling(fixture): - """Test when Dialogflow asks for slot-filling return none.""" - mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": True, - "parameters": {"ZodiacSign": ""}, - "contexts": [ - { - "name": CONTEXT_NAME, - "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, - "lifespan": 2, - }, - { - "name": "tests_ha_dialog_context", - "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, - "lifespan": 2, - }, - { - "name": "tests_ha_dialog_params_zodiacsign", - "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, - "lifespan": 1, - }, - ], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "true", - "intentName": INTENT_NAME, - }, - "fulfillment": { - "speech": "What is the ZodiacSign?", - "messages": [{"type": 0, "speech": "What is the ZodiacSign?"}], - }, - "score": 0.77, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } - - response = await mock_client.post( - "/api/webhook/{}".format(webhook_id), data=json.dumps(data) - ) - assert 200 == response.status - assert "" == await response.text() - - -async def test_intent_request_with_parameters(fixture): - """Test a request with parameters.""" - mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", "action": "GetZodiacHoroscopeIntent", "actionIncomplete": False, "parameters": {"ZodiacSign": "virgo"}, - "contexts": [], "metadata": { "intentId": INTENT_ID, "webhookUsed": "true", @@ -205,6 +114,122 @@ async def test_intent_request_with_parameters(fixture): "sessionId": SESSION_ID, "originalRequest": None, } + + _v2 = { + "responseId": REQUEST_ID, + "timestamp": REQUEST_TIMESTAMP, + "queryResult": { + "queryText": "my zodiac sign is virgo", + "action": "GetZodiacHoroscopeIntent", + "allRequiredParamsPresent": True, + "parameters": {"ZodiacSign": "virgo"}, + "intent": { + "name": INTENT_ID, + "webhookState": "true", + "displayName": INTENT_NAME, + }, + "fulfillment": {"text": "", "messages": [{"type": 0, "speech": ""}]}, + "intentDetectionConfidence": 1, + }, + "status": {"code": 200, "errorType": "success"}, + "session": SESSION_ID, + "originalDetectIntentRequest": None, + } + + @property + def v1(self): + return copy.deepcopy(self._v1) + + @property + def v2(self): + return copy.deepcopy(self._v2) + + +Data = _Data() + + +async def test_v1_data(): + """Test for version 1 api based on message.""" + assert dialogflow.get_api_version(Data.v1) == 1 + + +async def test_v2_data(): + """Test for version 2 api based on message.""" + assert dialogflow.get_api_version(Data.v2) == 2 + + +async def test_intent_action_incomplete_v1(fixture): + """Test when action is not completed.""" + mock_client, webhook_id = fixture + data = Data.v1 + data["result"]["actionIncomplete"] = True + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + assert "" == await response.text() + + +async def test_intent_action_incomplete_v2(fixture): + """Test when action is not completed.""" + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"]["allRequiredParamsPresent"] = False + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + assert "" == await response.text() + + +async def test_intent_slot_filling_v1(fixture): + """Test when Dialogflow asks for slot-filling return none.""" + mock_client, webhook_id = fixture + + data = Data.v1 + data["result"].update( + resolvedQuery="my zodiac sign is", + speech="", + actionIncomplete=True, + parameters={"ZodiacSign": ""}, + contexts=[ + { + "name": CONTEXT_NAME, + "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, + "lifespan": 2, + }, + { + "name": "tests_ha_dialog_context", + "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, + "lifespan": 2, + }, + { + "name": "tests_ha_dialog_params_zodiacsign", + "parameters": {"ZodiacSign.original": "", "ZodiacSign": ""}, + "lifespan": 1, + }, + ], + fulfillment={ + "speech": "What is the ZodiacSign?", + "messages": [{"type": 0, "speech": "What is the ZodiacSign?"}], + }, + score=0.77, + ) + data["result"]["metadata"].update(webhookForSlotFillingUsed="true") + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + assert "" == await response.text() + + +async def test_intent_request_with_parameters_v1(fixture): + """Test a request with parameters.""" + mock_client, webhook_id = fixture + data = Data.v1 response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) @@ -213,33 +238,23 @@ async def test_intent_request_with_parameters(fixture): assert "You told us your sign is virgo." == text -async def test_intent_request_with_parameters_but_empty(fixture): +async def test_intent_request_with_parameters_v2(fixture): + """Test a request with parameters.""" + mock_client, webhook_id = fixture + data = Data.v2 + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "You told us your sign is virgo." == text + + +async def test_intent_request_with_parameters_but_empty_v1(fixture): """Test a request with parameters but empty value.""" mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "GetZodiacHoroscopeIntent", - "actionIncomplete": False, - "parameters": {"ZodiacSign": ""}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + data["result"].update(parameters={"ZodiacSign": ""}) response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) @@ -248,33 +263,30 @@ async def test_intent_request_with_parameters_but_empty(fixture): assert "You told us your sign is ." == text -async def test_intent_request_without_slots(hass, fixture): +async def test_intent_request_with_parameters_but_empty_v2(fixture): + """Test a request with parameters but empty value.""" + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"].update(parameters={"ZodiacSign": ""}) + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "You told us your sign is ." == text + + +async def test_intent_request_without_slots_v1(hass, fixture): """Test a request without slots.""" mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "where are we", - "speech": "", - "action": "WhereAreWeIntent", - "actionIncomplete": False, - "parameters": {}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + data["result"].update( + resolvedQuery="where are we", + action="WhereAreWeIntent", + parameters={}, + contexts=[], + ) + response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) @@ -294,37 +306,45 @@ async def test_intent_request_without_slots(hass, fixture): assert "You are both home, you silly" == text -async def test_intent_request_calling_service(fixture, calls): +async def test_intent_request_without_slots_v2(hass, fixture): + """Test a request without slots.""" + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"].update( + queryText="where are we", + action="WhereAreWeIntent", + parameters={}, + outputContexts=[], + ) + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + + assert "Anne Therese is at unknown and Paulus is at unknown" == text + + hass.states.async_set("device_tracker.paulus", "home") + hass.states.async_set("device_tracker.anne_therese", "home") + + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "You are both home, you silly" == text + + +async def test_intent_request_calling_service_v1(fixture, calls): """Test a request for calling a service. If this request is done async the test could finish before the action has been executed. Hard to test because it will be a race condition. """ mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "CallServiceIntent", - "actionIncomplete": False, - "parameters": {"ZodiacSign": "virgo"}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + data["result"]["action"] = "CallServiceIntent" call_count = len(calls) response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) @@ -338,33 +358,34 @@ async def test_intent_request_calling_service(fixture, calls): assert "virgo" == call.data.get("hello") -async def test_intent_with_no_action(fixture): +async def test_intent_request_calling_service_v2(fixture, calls): + """Test a request for calling a service. + + If this request is done async the test could finish before the action + has been executed. Hard to test because it will be a race condition. + """ + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"]["action"] = "CallServiceIntent" + call_count = len(calls) + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + assert call_count + 1 == len(calls) + call = calls[-1] + assert "test" == call.domain + assert "dialogflow" == call.service + assert ["switch.test"] == call.data.get("entity_id") + assert "virgo" == call.data.get("hello") + + +async def test_intent_with_no_action_v1(fixture): """Test an intent with no defined action.""" mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "", - "actionIncomplete": False, - "parameters": {"ZodiacSign": ""}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + del data["result"]["action"] + assert "action" not in data["result"] response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) @@ -373,36 +394,41 @@ async def test_intent_with_no_action(fixture): assert "You have not defined an action in your Dialogflow intent." == text -async def test_intent_with_unknown_action(fixture): +async def test_intent_with_no_action_v2(fixture): + """Test an intent with no defined action.""" + mock_client, webhook_id = fixture + data = Data.v2 + del data["queryResult"]["action"] + assert "action" not in data["queryResult"] + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "You have not defined an action in your Dialogflow intent." == text + + +async def test_intent_with_unknown_action_v1(fixture): """Test an intent with an action not defined in the conf.""" mock_client, webhook_id = fixture - data = { - "id": REQUEST_ID, - "timestamp": REQUEST_TIMESTAMP, - "result": { - "source": "agent", - "resolvedQuery": "my zodiac sign is virgo", - "speech": "", - "action": "unknown", - "actionIncomplete": False, - "parameters": {"ZodiacSign": ""}, - "contexts": [], - "metadata": { - "intentId": INTENT_ID, - "webhookUsed": "true", - "webhookForSlotFillingUsed": "false", - "intentName": INTENT_NAME, - }, - "fulfillment": {"speech": "", "messages": [{"type": 0, "speech": ""}]}, - "score": 1, - }, - "status": {"code": 200, "errorType": "success"}, - "sessionId": SESSION_ID, - "originalRequest": None, - } + data = Data.v1 + data["result"]["action"] = "unknown" response = await mock_client.post( "/api/webhook/{}".format(webhook_id), data=json.dumps(data) ) assert 200 == response.status text = (await response.json()).get("speech") assert "This intent is not yet configured within Home Assistant." == text + + +async def test_intent_with_unknown_action_v2(fixture): + """Test an intent with an action not defined in the conf.""" + mock_client, webhook_id = fixture + data = Data.v2 + data["queryResult"]["action"] = "unknown" + response = await mock_client.post( + "/api/webhook/{}".format(webhook_id), data=json.dumps(data) + ) + assert 200 == response.status + text = (await response.json()).get("fulfillmentText") + assert "This intent is not yet configured within Home Assistant." == text diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 24938e52621..d6c40ddf9ab 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -131,6 +131,7 @@ class TestEcobee(unittest.TestCase): self.ecobee["equipmentStatus"] = "heatPump2" assert { "fan": "off", + "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "heatPump2", } == self.thermostat.device_state_attributes @@ -138,18 +139,21 @@ class TestEcobee(unittest.TestCase): self.ecobee["equipmentStatus"] = "auxHeat2" assert { "fan": "off", + "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "auxHeat2", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "compCool1" assert { "fan": "off", + "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "compCool1", } == self.thermostat.device_state_attributes self.ecobee["equipmentStatus"] = "" assert { "fan": "off", + "climate_mode": "Climate1", "fan_min_on_time": 10, "equipment_running": "", } == self.thermostat.device_state_attributes @@ -157,6 +161,15 @@ class TestEcobee(unittest.TestCase): self.ecobee["equipmentStatus"] = "Unknown" assert { "fan": "off", + "climate_mode": "Climate1", + "fan_min_on_time": 10, + "equipment_running": "Unknown", + } == self.thermostat.device_state_attributes + + self.ecobee["program"]["currentClimateRef"] = "c2" + assert { + "fan": "off", + "climate_mode": "Climate2", "fan_min_on_time": 10, "equipment_running": "Unknown", } == self.thermostat.device_state_attributes diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 57f29a4ef61..02f24f5afba 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -128,6 +128,9 @@ def hass_hue(loop, hass): kitchen_light_entity.entity_id, kitchen_light_entity.state, attributes=attrs ) + # create a lamp without brightness support + hass.states.async_set("light.no_brightness", "on", {}) + # Ceiling Fan is explicitly excluded from being exposed ceiling_fan_entity = hass.states.get("fan.ceiling_fan") attrs = dict(ceiling_fan_entity.attributes) @@ -218,6 +221,17 @@ def test_discover_lights(hue_client): assert "climate.ecobee" not in devices +@asyncio.coroutine +def test_light_without_brightness_supported(hass_hue, hue_client): + """Test that light without brightness is supported.""" + light_without_brightness_json = yield from perform_get_light_state( + hue_client, "light.no_brightness", 200 + ) + + assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True + assert light_without_brightness_json["type"] == "On/off light" + + @asyncio.coroutine def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index bef0b66c433..6288e0699fd 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -59,6 +59,31 @@ class TestFilterSensor(unittest.TestCase): assert setup_component(self.hass, "sensor", config) def test_chain(self): + """Test if filter chaining works.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + {"filter": "lowpass", "time_constant": 10, "precision": 2}, + {"filter": "throttle", "window_size": 1}, + ], + } + } + + with assert_setup_component(1, "sensor"): + assert setup_component(self.hass, "sensor", config) + + for value in self.values: + self.hass.states.set(config["sensor"]["entity_id"], value.state) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + assert "18.05" == state.state + + def test_chain_history(self, missing=False): """Test if filter chaining works.""" self.init_recorder() config = { @@ -78,13 +103,16 @@ class TestFilterSensor(unittest.TestCase): t_1 = dt_util.utcnow() - timedelta(minutes=2) t_2 = dt_util.utcnow() - timedelta(minutes=3) - fake_states = { - "sensor.test_monitored": [ - ha.State("sensor.test_monitored", 18.0, last_changed=t_0), - ha.State("sensor.test_monitored", 19.0, last_changed=t_1), - ha.State("sensor.test_monitored", 18.2, last_changed=t_2), - ] - } + if missing: + fake_states = {} + else: + fake_states = { + "sensor.test_monitored": [ + ha.State("sensor.test_monitored", 18.0, last_changed=t_0), + ha.State("sensor.test_monitored", 19.0, last_changed=t_1), + ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + ] + } with patch( "homeassistant.components.history." "state_changes_during_period", @@ -102,7 +130,52 @@ class TestFilterSensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("sensor.test") - assert "17.05" == state.state + if missing: + assert "18.05" == state.state + else: + assert "17.05" == state.state + + def test_chain_history_missing(self): + """Test if filter chaining works when recorder is enabled but the source is not recorded.""" + return self.test_chain_history(missing=True) + + def test_history_time(self): + """Test loading from history based on a time window.""" + self.init_recorder() + config = { + "history": {}, + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [{"filter": "time_throttle", "window_size": "00:01"}], + }, + } + t_0 = dt_util.utcnow() - timedelta(minutes=1) + t_1 = dt_util.utcnow() - timedelta(minutes=2) + t_2 = dt_util.utcnow() - timedelta(minutes=3) + + fake_states = { + "sensor.test_monitored": [ + ha.State("sensor.test_monitored", 18.0, last_changed=t_0), + ha.State("sensor.test_monitored", 19.0, last_changed=t_1), + ha.State("sensor.test_monitored", 18.2, last_changed=t_2), + ] + } + with patch( + "homeassistant.components.history." "state_changes_during_period", + return_value=fake_states, + ): + with patch( + "homeassistant.components.history." "get_last_state_changes", + return_value=fake_states, + ): + with assert_setup_component(1, "sensor"): + assert setup_component(self.hass, "sensor", config) + + self.hass.block_till_done() + state = self.hass.states.get("sensor.test") + assert "18.0" == state.state def test_outlier(self): """Test if outlier filter works.""" diff --git a/tests/components/geonetnz_quakes/__init__.py b/tests/components/geonetnz_quakes/__init__.py new file mode 100644 index 00000000000..95c50679338 --- /dev/null +++ b/tests/components/geonetnz_quakes/__init__.py @@ -0,0 +1 @@ +"""Tests for the geonetnz_quakes component.""" diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py new file mode 100644 index 00000000000..2d8e3750648 --- /dev/null +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -0,0 +1,135 @@ +"""Define tests for the GeoNet NZ Quakes config flow.""" +from datetime import timedelta + +import pytest +from asynctest import patch, CoroutineMock + +from homeassistant import data_entry_flow +from homeassistant.components.geonetnz_quakes import ( + async_setup_entry, + config_flow, + CONF_MMI, + CONF_MINIMUM_MAGNITUDE, + DOMAIN, + async_unload_entry, + FEED, +) +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_RADIUS, + CONF_UNIT_SYSTEM, + CONF_SCAN_INTERVAL, +) +from tests.common import MockConfigEntry + + +@pytest.fixture +def config_entry(): + """Create a mock GeoNet NZ Quakes config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + CONF_MMI: 4, + CONF_MINIMUM_MAGNITUDE: 0.0, + }, + title="-41.2, 174.7", + ) + + +async def test_duplicate_error(hass, config_entry): + """Test that errors are shown when duplicates are added.""" + conf = {CONF_LATITUDE: -41.2, CONF_LONGITUDE: 174.7, CONF_RADIUS: 25} + + config_entry.add_to_hass(hass) + flow = config_flow.GeonetnzQuakesFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "identifier_exists"} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.GeonetnzQuakesFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_UNIT_SYSTEM: "metric", + CONF_MMI: 2, + CONF_SCAN_INTERVAL: timedelta(minutes=4), + CONF_MINIMUM_MAGNITUDE: 2.5, + } + + flow = config_flow.GeonetnzQuakesFlowHandler() + flow.hass = hass + + result = await flow.async_step_import(import_config=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_MMI: 2, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 240.0, + CONF_MINIMUM_MAGNITUDE: 2.5, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + hass.config.latitude = -41.2 + hass.config.longitude = 174.7 + conf = {CONF_RADIUS: 25, CONF_MMI: 4} + + flow = config_flow.GeonetnzQuakesFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "-41.2, 174.7" + assert result["data"] == { + CONF_LATITUDE: -41.2, + CONF_LONGITUDE: 174.7, + CONF_RADIUS: 25, + CONF_MMI: 4, + CONF_UNIT_SYSTEM: "metric", + CONF_SCAN_INTERVAL: 300.0, + CONF_MINIMUM_MAGNITUDE: 0.0, + } + + +async def test_component_unload_config_entry(hass, config_entry): + """Test that loading and unloading of a config entry works.""" + config_entry.add_to_hass(hass) + with patch( + "aio_geojson_geonetnz_quakes.GeonetnzQuakesFeedManager.update", + new_callable=CoroutineMock, + ) as mock_feed_manager_update: + # Load config entry. + assert await async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + assert mock_feed_manager_update.call_count == 1 + assert hass.data[DOMAIN][FEED][config_entry.entry_id] is not None + # Unload config entry. + assert await async_unload_entry(hass, config_entry) + await hass.async_block_till_done() + assert hass.data[DOMAIN][FEED].get(config_entry.entry_id) is None diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py new file mode 100644 index 00000000000..c5b7282f320 --- /dev/null +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -0,0 +1,223 @@ +"""The tests for the GeoNet NZ Quakes Feed integration.""" +import datetime +from unittest.mock import MagicMock + +from asynctest import patch, CoroutineMock + +from homeassistant.components import geonetnz_quakes +from homeassistant.components.geo_location import ATTR_SOURCE +from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL +from homeassistant.components.geonetnz_quakes.geo_location import ( + ATTR_EXTERNAL_ID, + ATTR_MAGNITUDE, + ATTR_LOCALITY, + ATTR_MMI, + ATTR_DEPTH, + ATTR_QUALITY, +) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_START, + CONF_RADIUS, + ATTR_LATITUDE, + ATTR_LONGITUDE, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + ATTR_ATTRIBUTION, + ATTR_TIME, + ATTR_ICON, +) +from homeassistant.setup import async_setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from tests.common import async_fire_time_changed +import homeassistant.util.dt as dt_util + +CONFIG = {geonetnz_quakes.DOMAIN: {CONF_RADIUS: 200}} + + +def _generate_mock_feed_entry( + external_id, + title, + distance_to_home, + coordinates, + attribution=None, + depth=None, + magnitude=None, + mmi=None, + locality=None, + quality=None, + time=None, +): + """Construct a mock feed entry for testing purposes.""" + feed_entry = MagicMock() + feed_entry.external_id = external_id + feed_entry.title = title + feed_entry.distance_to_home = distance_to_home + feed_entry.coordinates = coordinates + feed_entry.attribution = attribution + feed_entry.depth = depth + feed_entry.magnitude = magnitude + feed_entry.mmi = mmi + feed_entry.locality = locality + feed_entry.quality = quality + feed_entry.time = time + return feed_entry + + +async def test_setup(hass): + """Test the general setup of the integration.""" + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry( + "1234", + "Title 1", + 15.5, + (38.0, -3.0), + locality="Locality 1", + attribution="Attribution 1", + time=datetime.datetime(2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc), + magnitude=5.7, + mmi=5, + depth=10.5, + quality="best", + ) + mock_entry_2 = _generate_mock_feed_entry( + "2345", "Title 2", 20.5, (38.1, -3.1), magnitude=4.6 + ) + mock_entry_3 = _generate_mock_feed_entry( + "3456", "Title 3", 25.5, (38.2, -3.2), locality="Locality 3" + ) + mock_entry_4 = _generate_mock_feed_entry("4567", "Title 4", 12.5, (38.3, -3.3)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + ) as mock_feed_update: + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_2, mock_entry_3] + assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) + # Artificially trigger update. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + # Collect events. + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 3 + + state = hass.states.get("geo_location.title_1") + assert state is not None + assert state.name == "Title 1" + assert state.attributes == { + ATTR_EXTERNAL_ID: "1234", + ATTR_LATITUDE: 38.0, + ATTR_LONGITUDE: -3.0, + ATTR_FRIENDLY_NAME: "Title 1", + ATTR_LOCALITY: "Locality 1", + ATTR_ATTRIBUTION: "Attribution 1", + ATTR_TIME: datetime.datetime( + 2018, 9, 22, 8, 0, tzinfo=datetime.timezone.utc + ), + ATTR_MAGNITUDE: 5.7, + ATTR_DEPTH: 10.5, + ATTR_MMI: 5, + ATTR_QUALITY: "best", + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "geonetnz_quakes", + ATTR_ICON: "mdi:pulse", + } + assert float(state.state) == 15.5 + + state = hass.states.get("geo_location.title_2") + assert state is not None + assert state.name == "Title 2" + assert state.attributes == { + ATTR_EXTERNAL_ID: "2345", + ATTR_LATITUDE: 38.1, + ATTR_LONGITUDE: -3.1, + ATTR_FRIENDLY_NAME: "Title 2", + ATTR_MAGNITUDE: 4.6, + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "geonetnz_quakes", + ATTR_ICON: "mdi:pulse", + } + assert float(state.state) == 20.5 + + state = hass.states.get("geo_location.title_3") + assert state is not None + assert state.name == "Title 3" + assert state.attributes == { + ATTR_EXTERNAL_ID: "3456", + ATTR_LATITUDE: 38.2, + ATTR_LONGITUDE: -3.2, + ATTR_FRIENDLY_NAME: "Title 3", + ATTR_LOCALITY: "Locality 3", + ATTR_UNIT_OF_MEASUREMENT: "km", + ATTR_SOURCE: "geonetnz_quakes", + ATTR_ICON: "mdi:pulse", + } + assert float(state.state) == 25.5 + + # Simulate an update - one existing, one new entry, + # one outdated entry + mock_feed_update.return_value = "OK", [mock_entry_1, mock_entry_4, mock_entry_3] + async_fire_time_changed(hass, utcnow + DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 3 + + # Simulate an update - empty data, but successful update, + # so no changes to entities. + mock_feed_update.return_value = "OK_NO_DATA", None + async_fire_time_changed(hass, utcnow + 2 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 3 + + # Simulate an update - empty data, removes all entities + mock_feed_update.return_value = "ERROR", None + async_fire_time_changed(hass, utcnow + 3 * DEFAULT_SCAN_INTERVAL) + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 0 + + +async def test_setup_imperial(hass): + """Test the setup of the integration using imperial unit system.""" + hass.config.units = IMPERIAL_SYSTEM + # Set up some mock feed entries for this test. + mock_entry_1 = _generate_mock_feed_entry("1234", "Title 1", 15.5, (38.0, -3.0)) + + # Patching 'utcnow' to gain more control over the timed update. + utcnow = dt_util.utcnow() + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "aio_geojson_client.feed.GeoJsonFeed.update", new_callable=CoroutineMock + ) as mock_feed_update, patch( + "aio_geojson_client.feed.GeoJsonFeed.__init__", new_callable=CoroutineMock + ) as mock_feed_init: + mock_feed_update.return_value = "OK", [mock_entry_1] + assert await async_setup_component(hass, geonetnz_quakes.DOMAIN, CONFIG) + # Artificially trigger update. + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + # Collect events. + await hass.async_block_till_done() + + all_states = hass.states.async_all() + assert len(all_states) == 1 + + # Test conversion of 200 miles to kilometers. + assert mock_feed_init.call_args[1].get("filter_radius") == 321.8688 + + state = hass.states.get("geo_location.title_1") + assert state is not None + assert state.name == "Title 1" + assert state.attributes == { + ATTR_EXTERNAL_ID: "1234", + ATTR_LATITUDE: 38.0, + ATTR_LONGITUDE: -3.0, + ATTR_FRIENDLY_NAME: "Title 1", + ATTR_UNIT_OF_MEASUREMENT: "mi", + ATTR_SOURCE: "geonetnz_quakes", + ATTR_ICON: "mdi:pulse", + } + assert float(state.state) == 9.6 diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index 3592f2c39ff..502ea9e51fc 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -2,7 +2,7 @@ from asyncio import Future from unittest.mock import patch -from homeassistant.components.group import async_reproduce_states +from homeassistant.components.group.reproduce_state import async_reproduce_states from homeassistant.core import Context, State diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index 0d834ccc770..df021fea55d 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for the Heos config flow module.""" -import asyncio +from pyheos import HeosError from homeassistant import data_entry_flow from homeassistant.components.heos.config_flow import HeosFlowHandler @@ -31,18 +31,15 @@ async def test_cannot_connect_shows_error_form(hass, controller): """Test form is shown with error when cannot connect.""" flow = HeosFlowHandler() flow.hass = hass - - errors = [ConnectionError, asyncio.TimeoutError] - for error in errors: - controller.connect.side_effect = error - result = await flow.async_step_user({CONF_HOST: "127.0.0.1"}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"][CONF_HOST] == "connection_failure" - assert controller.connect.call_count == 1 - assert controller.disconnect.call_count == 1 - controller.connect.reset_mock() - controller.disconnect.reset_mock() + controller.connect.side_effect = HeosError() + result = await flow.async_step_user({CONF_HOST: "127.0.0.1"}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"][CONF_HOST] == "connection_failure" + assert controller.connect.call_count == 1 + assert controller.disconnect.call_count == 1 + controller.connect.reset_mock() + controller.disconnect.reset_mock() async def test_create_entry_when_host_valid(hass, controller): diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index 728e65b81f5..7b2645cb8ec 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -2,7 +2,7 @@ import asyncio from asynctest import Mock, patch -from pyheos import CommandError, const +from pyheos import CommandFailedError, HeosError, const import pytest from homeassistant.components.heos import ( @@ -117,31 +117,27 @@ async def test_async_setup_entry_not_signed_in_loads_platforms( async def test_async_setup_entry_connect_failure(hass, config_entry, controller): """Connection failure raises ConfigEntryNotReady.""" config_entry.add_to_hass(hass) - errors = [ConnectionError, asyncio.TimeoutError] - for error in errors: - controller.connect.side_effect = error - with pytest.raises(ConfigEntryNotReady): - await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - assert controller.connect.call_count == 1 - assert controller.disconnect.call_count == 1 - controller.connect.reset_mock() - controller.disconnect.reset_mock() + controller.connect.side_effect = HeosError() + with pytest.raises(ConfigEntryNotReady): + await async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + assert controller.connect.call_count == 1 + assert controller.disconnect.call_count == 1 + controller.connect.reset_mock() + controller.disconnect.reset_mock() async def test_async_setup_entry_player_failure(hass, config_entry, controller): """Failure to retrieve players/sources raises ConfigEntryNotReady.""" config_entry.add_to_hass(hass) - errors = [ConnectionError, asyncio.TimeoutError] - for error in errors: - controller.get_players.side_effect = error - with pytest.raises(ConfigEntryNotReady): - await async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - assert controller.connect.call_count == 1 - assert controller.disconnect.call_count == 1 - controller.connect.reset_mock() - controller.disconnect.reset_mock() + controller.get_players.side_effect = HeosError() + with pytest.raises(ConfigEntryNotReady): + await async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + assert controller.connect.call_count == 1 + assert controller.disconnect.call_count == 1 + controller.connect.reset_mock() + controller.disconnect.reset_mock() async def test_unload_entry(hass, config_entry, controller): @@ -167,7 +163,7 @@ async def test_update_sources_retry(hass, config_entry, config, controller, capl source_manager = hass.data[DOMAIN][DATA_SOURCE_MANAGER] source_manager.retry_delay = 0 source_manager.max_retry_attempts = 1 - controller.get_favorites.side_effect = CommandError("Test", "test", 0) + controller.get_favorites.side_effect = CommandFailedError("Test", "test", 0) controller.dispatcher.send( const.SIGNAL_CONTROLLER_EVENT, const.EVENT_SOURCES_CHANGED, {} ) diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index de062757803..0f9bf2d8b3e 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the Heos Media Player platform.""" import asyncio -from pyheos import CommandError, const +from pyheos import CommandFailedError, const from homeassistant.components.heos import media_player from homeassistant.components.heos.const import ( @@ -179,7 +179,7 @@ async def test_updates_from_connection_event( event.clear() player.reset_mock() controller.load_players.reset_mock() - controller.load_players.side_effect = CommandError(None, "Failure", 1) + controller.load_players.side_effect = CommandFailedError(None, "Failure", 1) player.available = True player.heos.dispatcher.send(const.SIGNAL_HEOS_EVENT, const.EVENT_CONNECTED) await event.wait() @@ -313,7 +313,7 @@ async def test_clear_playlist(hass, config_entry, config, controller, caplog): ) assert player.clear_queue.call_count == 1 player.clear_queue.reset_mock() - player.clear_queue.side_effect = CommandError(None, "Failure", 1) + player.clear_queue.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to clear playlist: Failure (1)" in caplog.text @@ -331,7 +331,7 @@ async def test_pause(hass, config_entry, config, controller, caplog): ) assert player.pause.call_count == 1 player.pause.reset_mock() - player.pause.side_effect = CommandError(None, "Failure", 1) + player.pause.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to pause: Failure (1)" in caplog.text @@ -349,7 +349,7 @@ async def test_play(hass, config_entry, config, controller, caplog): ) assert player.play.call_count == 1 player.play.reset_mock() - player.play.side_effect = CommandError(None, "Failure", 1) + player.play.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to play: Failure (1)" in caplog.text @@ -367,7 +367,7 @@ async def test_previous_track(hass, config_entry, config, controller, caplog): ) assert player.play_previous.call_count == 1 player.play_previous.reset_mock() - player.play_previous.side_effect = CommandError(None, "Failure", 1) + player.play_previous.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to move to previous track: Failure (1)" in caplog.text @@ -385,7 +385,7 @@ async def test_next_track(hass, config_entry, config, controller, caplog): ) assert player.play_next.call_count == 1 player.play_next.reset_mock() - player.play_next.side_effect = CommandError(None, "Failure", 1) + player.play_next.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to move to next track: Failure (1)" in caplog.text @@ -403,7 +403,7 @@ async def test_stop(hass, config_entry, config, controller, caplog): ) assert player.stop.call_count == 1 player.stop.reset_mock() - player.stop.side_effect = CommandError(None, "Failure", 1) + player.stop.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to stop: Failure (1)" in caplog.text @@ -421,7 +421,7 @@ async def test_volume_mute(hass, config_entry, config, controller, caplog): ) assert player.set_mute.call_count == 1 player.set_mute.reset_mock() - player.set_mute.side_effect = CommandError(None, "Failure", 1) + player.set_mute.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to set mute: Failure (1)" in caplog.text @@ -439,7 +439,7 @@ async def test_shuffle_set(hass, config_entry, config, controller, caplog): ) player.set_play_mode.assert_called_once_with(player.repeat, True) player.set_play_mode.reset_mock() - player.set_play_mode.side_effect = CommandError(None, "Failure", 1) + player.set_play_mode.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to set shuffle: Failure (1)" in caplog.text @@ -457,7 +457,7 @@ async def test_volume_set(hass, config_entry, config, controller, caplog): ) player.set_volume.assert_called_once_with(100) player.set_volume.reset_mock() - player.set_volume.side_effect = CommandError(None, "Failure", 1) + player.set_volume.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to set volume level: Failure (1)" in caplog.text @@ -516,7 +516,7 @@ async def test_select_radio_favorite_command_error( player = controller.players[1] # Test set radio preset favorite = favorites[2] - player.play_favorite.side_effect = CommandError(None, "Failure", 1) + player.play_favorite.side_effect = CommandFailedError(None, "Failure", 1) await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, @@ -575,7 +575,7 @@ async def test_select_input_command_error( await setup_platform(hass, config_entry, config) player = controller.players[1] input_source = input_sources[0] - player.play_input_source.side_effect = CommandError(None, "Failure", 1) + player.play_input_source.side_effect = CommandFailedError(None, "Failure", 1) await hass.services.async_call( MEDIA_PLAYER_DOMAIN, SERVICE_SELECT_SOURCE, @@ -615,7 +615,7 @@ async def test_play_media_url(hass, config_entry, config, controller, caplog): ) player.play_url.assert_called_once_with(url) player.play_url.reset_mock() - player.play_url.side_effect = CommandError(None, "Failure", 1) + player.play_url.side_effect = CommandFailedError(None, "Failure", 1) assert "Unable to play media: Failure (1)" in caplog.text diff --git a/tests/components/heos/test_services.py b/tests/components/heos/test_services.py index 0e1cbc8ea2e..5a835cf7303 100644 --- a/tests/components/heos/test_services.py +++ b/tests/components/heos/test_services.py @@ -1,5 +1,5 @@ """Tests for the services module.""" -from pyheos import CommandError, const +from pyheos import CommandFailedError, HeosError, const from homeassistant.components.heos.const import ( ATTR_PASSWORD, @@ -51,7 +51,7 @@ async def test_sign_in_not_connected(hass, config_entry, controller, caplog): async def test_sign_in_failed(hass, config_entry, controller, caplog): """Test sign-in service logs error when not connected.""" await setup_component(hass, config_entry) - controller.sign_in.side_effect = CommandError("", "Invalid credentials", 6) + controller.sign_in.side_effect = CommandFailedError("", "Invalid credentials", 6) await hass.services.async_call( DOMAIN, @@ -67,7 +67,7 @@ async def test_sign_in_failed(hass, config_entry, controller, caplog): async def test_sign_in_unknown_error(hass, config_entry, controller, caplog): """Test sign-in service logs error for failure.""" await setup_component(hass, config_entry) - controller.sign_in.side_effect = ConnectionError + controller.sign_in.side_effect = HeosError() await hass.services.async_call( DOMAIN, @@ -103,7 +103,7 @@ async def test_sign_out_not_connected(hass, config_entry, controller, caplog): async def test_sign_out_unknown_error(hass, config_entry, controller, caplog): """Test the sign-out service.""" await setup_component(hass, config_entry) - controller.sign_out.side_effect = ConnectionError + controller.sign_out.side_effect = HeosError() await hass.services.async_call(DOMAIN, SERVICE_SIGN_OUT, {}, blocking=True) diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index 526884f86d4..2e1cfd7a77d 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -74,6 +74,7 @@ class FakePairing: if char.iid != cid: continue char.set_value(new_val) + return {} class FakeController: diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index e17ad2a8c73..13d844e0162 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -4,6 +4,7 @@ from tests.components.homekit_controller.common import FakeService, setup_test_c TEMPERATURE = ("temperature", "temperature.current") HUMIDITY = ("humidity", "relative-humidity.current") LIGHT_LEVEL = ("light", "light-level.current") +CARBON_DIOXIDE_LEVEL = ("carbon-dioxide", "carbon-dioxide.level") def create_temperature_sensor_service(): @@ -36,6 +37,16 @@ def create_light_level_sensor_service(): return service +def create_carbon_dioxide_level_sensor_service(): + """Define carbon dioxide level characteristics.""" + service = FakeService("public.hap.service.sensor.carbon-dioxide") + + cur_state = service.add_characteristic("carbon-dioxide.level") + cur_state.value = 0 + + return service + + async def test_temperature_sensor_read_state(hass, utcnow): """Test reading the state of a HomeKit temperature sensor accessory.""" sensor = create_temperature_sensor_service() @@ -76,3 +87,17 @@ async def test_light_level_sensor_read_state(hass, utcnow): helper.characteristics[LIGHT_LEVEL].value = 20 state = await helper.poll_and_get_state() assert state.state == "20" + + +async def test_carbon_dioxide_level_sensor_read_state(hass, utcnow): + """Test reading the state of a HomeKit carbon dioxide sensor accessory.""" + sensor = create_carbon_dioxide_level_sensor_service() + helper = await setup_test_component(hass, [sensor], suffix="co2") + + helper.characteristics[CARBON_DIOXIDE_LEVEL].value = 10 + state = await helper.poll_and_get_state() + assert state.state == "10" + + helper.characteristics[CARBON_DIOXIDE_LEVEL].value = 20 + state = await helper.poll_and_get_state() + assert state.state == "20" diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index 3c7bd4c8e2c..4fcf035ae48 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -97,6 +97,7 @@ async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): pairing_data, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) assert hkid in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 5d160108eb5..d77d4a7e5b2 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -68,7 +68,13 @@ async def test_setup_entry_successful(hass): ) entry.add_to_hass(hass) with patch.object(hmipc, "HomematicipHAP") as mock_hap: - mock_hap.return_value.async_setup.return_value = mock_coro(True) + instance = mock_hap.return_value + instance.async_setup.return_value = mock_coro(True) + instance.home.id = "1" + instance.home.modelType = "mock-type" + instance.home.name = "mock-name" + instance.home.currentAPVersion = "mock-ap-version" + assert ( await async_setup_component( hass, @@ -129,7 +135,13 @@ async def test_unload_entry(hass): entry.add_to_hass(hass) with patch.object(hmipc, "HomematicipHAP") as mock_hap: - mock_hap.return_value.async_setup.return_value = mock_coro(True) + instance = mock_hap.return_value + instance.async_setup.return_value = mock_coro(True) + instance.home.id = "1" + instance.home.modelType = "mock-type" + instance.home.name = "mock-name" + instance.home.currentAPVersion = "mock-ap-version" + assert await async_setup_component(hass, hmipc.DOMAIN, {}) is True assert len(mock_hap.return_value.mock_calls) >= 1 diff --git a/tests/components/huawei_lte/test_init.py b/tests/components/huawei_lte/test_init.py index 46cec10963a..70a00b02b4e 100644 --- a/tests/components/huawei_lte/test_init.py +++ b/tests/components/huawei_lte/test_init.py @@ -9,7 +9,7 @@ from homeassistant.components import huawei_lte @pytest.fixture(autouse=True) def routerdata(): """Set up a router data for testing.""" - rd = huawei_lte.RouterData(Mock()) + rd = huawei_lte.RouterData(Mock(), "de:ad:be:ef:00:00") rd.device_information = {"SoftwareVersion": "1.0", "nested": {"foo": "bar"}} return rd diff --git a/tests/components/huawei_lte/test_sensor.py b/tests/components/huawei_lte/test_sensor.py new file mode 100644 index 00000000000..f9834349750 --- /dev/null +++ b/tests/components/huawei_lte/test_sensor.py @@ -0,0 +1,14 @@ +"""Huawei LTE sensor tests.""" + +import pytest + +from homeassistant.components.huawei_lte import sensor + + +@pytest.mark.parametrize( + ("value", "expected"), + (("-71 dBm", (-71, "dBm")), ("15dB", (15, "dB")), (">=-51dBm", (-51, "dBm"))), +) +def test_format_default(value, expected): + """Test that default formatter copes with expected values.""" + assert sensor.format_default(value) == expected diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 2228c2dcfbf..54082464a7c 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -255,6 +255,7 @@ async def test_import_with_existing_config(hass): """Test importing a host with an existing config file.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} bridge = Mock() bridge.username = "username-abc" @@ -280,6 +281,7 @@ async def test_import_with_no_config(hass): """Test importing a host without an existing config file.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} with patch.object( config_flow, "get_bridge", side_effect=errors.AuthenticationRequired @@ -294,6 +296,7 @@ async def test_import_with_existing_but_invalid_config(hass): """Test importing a host with a config file with invalid username.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} with patch.object( config_flow, "_find_username_from_config", return_value="mock-user" @@ -310,6 +313,7 @@ async def test_import_cannot_connect(hass): """Test importing a host that we cannot conncet to.""" flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} with patch.object(config_flow, "get_bridge", side_effect=errors.CannotConnect): result = await flow.async_step_import({"host": "0.0.0.0"}) @@ -337,6 +341,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge(hass): flow = config_flow.HueFlowHandler() flow.hass = hass + flow.context = {} bridge = Mock() bridge.username = "username-abc" diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 4038f4f9e87..1c891b9c840 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -221,6 +221,7 @@ async def setup_bridge(hass, mock_bridge): {"host": "mock-host"}, "test", config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "light") # To flush out the service call to update the group diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index c78192d2572..72ac816483a 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -306,6 +306,7 @@ async def setup_bridge(hass, mock_bridge, hostname=None): {"host": hostname}, "test", config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.config_entries.async_forward_entry_setup(config_entry, "sensor") diff --git a/tests/components/input_boolean/test_reproduce_state.py b/tests/components/input_boolean/test_reproduce_state.py new file mode 100644 index 00000000000..7ce4f4c1fd1 --- /dev/null +++ b/tests/components/input_boolean/test_reproduce_state.py @@ -0,0 +1,41 @@ +"""Test reproduce state for input boolean.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + + +async def test_reproducing_states(hass): + """Test reproducing input_boolean states.""" + assert await async_setup_component( + hass, + "input_boolean", + { + "input_boolean": { + "initial_on": {"initial": True}, + "initial_off": {"initial": False}, + } + }, + ) + await hass.helpers.state.async_reproduce_state( + [ + State("input_boolean.initial_on", "off"), + State("input_boolean.initial_off", "on"), + # Should not raise + State("input_boolean.non_existing", "on"), + ], + blocking=True, + ) + assert hass.states.get("input_boolean.initial_off").state == "on" + assert hass.states.get("input_boolean.initial_on").state == "off" + + await hass.helpers.state.async_reproduce_state( + [ + # Test invalid state + State("input_boolean.initial_on", "invalid_state"), + # Set to state it already is. + State("input_boolean.initial_off", "on"), + ], + blocking=True, + ) + + assert hass.states.get("input_boolean.initial_on").state == "off" + assert hass.states.get("input_boolean.initial_off").state == "on" diff --git a/tests/components/media_player/test_reproduce_state.py b/tests/components/media_player/test_reproduce_state.py index 3741b6e392a..ddc5d6cf0ca 100644 --- a/tests/components/media_player/test_reproduce_state.py +++ b/tests/components/media_player/test_reproduce_state.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.media_player import async_reproduce_states +from homeassistant.components.media_player.reproduce_state import async_reproduce_states from homeassistant.components.media_player.const import ( ATTR_INPUT_SOURCE, ATTR_MEDIA_CONTENT_ID, diff --git a/tests/components/minio/__init__.py b/tests/components/minio/__init__.py new file mode 100644 index 00000000000..273de09788e --- /dev/null +++ b/tests/components/minio/__init__.py @@ -0,0 +1 @@ +"""Tests for the minio component.""" diff --git a/tests/components/minio/common.py b/tests/components/minio/common.py new file mode 100644 index 00000000000..4719fc79e49 --- /dev/null +++ b/tests/components/minio/common.py @@ -0,0 +1,48 @@ +"""Minio Test event.""" +TEST_EVENT = { + "Records": [ + { + "eventVersion": "2.0", + "eventSource": "minio:s3", + "awsRegion": "", + "eventTime": "2019-05-02T11:05:07Z", + "eventName": "s3:ObjectCreated:Put", + "userIdentity": {"principalId": "SO9KNO6YT9OGE39PQCZW"}, + "requestParameters": { + "accessKey": "SO9KNO6YT9OGE39PQCZW", + "region": "", + "sourceIPAddress": "172.27.0.1", + }, + "responseElements": { + "x-amz-request-id": "159AD8E6F6805783", + "x-minio-deployment-id": "90b265b8-bac5-413a-b12a-8915469fd769", + "x-minio-origin-endpoint": "http://172.27.0.2:9000", + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "Config", + "bucket": { + "name": "test", + "ownerIdentity": {"principalId": "SO9KNO6YT9OGE39PQCZW"}, + "arn": "arn:aws:s3:::test", + }, + "object": { + "key": "5jJkTAo.jpg", + "size": 108368, + "eTag": "1af324731637228cbbb0b2e8c07d4e50", + "contentType": "image/jpeg", + "userMetadata": {"content-type": "image/jpeg"}, + "versionId": "1", + "sequencer": "159AD8E6F76DD9C4", + }, + }, + "source": { + "host": "", + "port": "", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) " + "AppleWebKit/605.1.15 (KHTML, like Gecko) " + "Version/12.0.3 Safari/605.1.15", + }, + } + ] +} diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py new file mode 100644 index 00000000000..836b456dc9b --- /dev/null +++ b/tests/components/minio/test_minio.py @@ -0,0 +1,190 @@ +"""Tests for Minio Hass related code.""" +import asyncio +import json +from unittest.mock import MagicMock + +import pytest +from asynctest import patch, call + +from homeassistant.components.minio import ( + QueueListener, + DOMAIN, + CONF_HOST, + CONF_PORT, + CONF_ACCESS_KEY, + CONF_SECRET_KEY, + CONF_SECURE, + CONF_LISTEN, + CONF_LISTEN_BUCKET, +) +from homeassistant.core import callback +from homeassistant.setup import async_setup_component + +from tests.components.minio.common import TEST_EVENT + + +@pytest.fixture(name="minio_client") +def minio_client_fixture(): + """Patch Minio client.""" + with patch("homeassistant.components.minio.minio_helper.Minio") as minio_mock: + minio_client_mock = minio_mock.return_value + + yield minio_client_mock + + +@pytest.fixture(name="minio_client_event") +def minio_client_event_fixture(): + """Patch helper function for minio notification stream.""" + with patch("homeassistant.components.minio.minio_helper.Minio") as minio_mock: + minio_client_mock = minio_mock.return_value + + response_mock = MagicMock() + stream_mock = MagicMock() + + stream_mock.__next__.side_effect = [ + "", + "", + bytearray(json.dumps(TEST_EVENT), "utf-8"), + ] + + response_mock.stream.return_value = stream_mock + minio_client_mock._url_open.return_value = response_mock + + yield minio_client_mock + + +async def test_minio_services(hass, caplog, minio_client): + """Test Minio services.""" + hass.config.whitelist_external_dirs = set("/tmp") + + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_HOST: "localhost", + CONF_PORT: "9000", + CONF_ACCESS_KEY: "abcdef", + CONF_SECRET_KEY: "0123456789", + CONF_SECURE: "true", + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert "Setup of domain minio took" in caplog.text + + # Call services + await hass.services.async_call( + DOMAIN, + "put", + {"file_path": "/tmp/some_file", "key": "some_key", "bucket": "some_bucket"}, + blocking=True, + ) + assert minio_client.fput_object.call_args == call( + "some_bucket", "some_key", "/tmp/some_file" + ) + minio_client.reset_mock() + + await hass.services.async_call( + DOMAIN, + "get", + {"file_path": "/tmp/some_file", "key": "some_key", "bucket": "some_bucket"}, + blocking=True, + ) + assert minio_client.fget_object.call_args == call( + "some_bucket", "some_key", "/tmp/some_file" + ) + minio_client.reset_mock() + + await hass.services.async_call( + DOMAIN, "remove", {"key": "some_key", "bucket": "some_bucket"}, blocking=True + ) + assert minio_client.remove_object.call_args == call("some_bucket", "some_key") + minio_client.reset_mock() + + +async def test_minio_listen(hass, caplog, minio_client_event): + """Test minio listen on notifications.""" + minio_client_event.presigned_get_object.return_value = "http://url" + + events = [] + + @callback + def event_callback(event): + """Handle event callbback.""" + events.append(event) + + hass.bus.async_listen("minio", event_callback) + + await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_HOST: "localhost", + CONF_PORT: "9000", + CONF_ACCESS_KEY: "abcdef", + CONF_SECRET_KEY: "0123456789", + CONF_SECURE: "true", + CONF_LISTEN: [{CONF_LISTEN_BUCKET: "test"}], + } + }, + ) + + await hass.async_start() + await hass.async_block_till_done() + + assert "Setup of domain minio took" in caplog.text + + while not events: + await asyncio.sleep(0) + + assert 1 == len(events) + event = events[0] + + assert DOMAIN == event.event_type + assert "s3:ObjectCreated:Put" == event.data["event_name"] + assert "5jJkTAo.jpg" == event.data["file_name"] + assert "test" == event.data["bucket"] + assert "5jJkTAo.jpg" == event.data["key"] + assert "http://url" == event.data["presigned_url"] + assert 0 == len(event.data["metadata"]) + + +async def test_queue_listener(): + """Tests QueueListener firing events on Hass event bus.""" + hass = MagicMock() + + queue_listener = QueueListener(hass) + queue_listener.start() + + queue_entry = { + "event_name": "s3:ObjectCreated:Put", + "bucket": "some_bucket", + "key": "some_dir/some_file.jpg", + "presigned_url": "http://host/url?signature=secret", + "metadata": {}, + } + + queue_listener.queue.put(queue_entry) + + queue_listener.stop() + + call_domain, call_event = hass.bus.fire.call_args[0] + + expected_event = { + "event_name": "s3:ObjectCreated:Put", + "file_name": "some_file.jpg", + "bucket": "some_bucket", + "key": "some_dir/some_file.jpg", + "presigned_url": "http://host/url?signature=secret", + "metadata": {}, + } + + assert DOMAIN == call_domain + assert json.dumps(expected_event, sort_keys=True) == json.dumps( + call_event, sort_keys=True + ) diff --git a/tests/components/modbus/__init__.py b/tests/components/modbus/__init__.py new file mode 100644 index 00000000000..86f5641c475 --- /dev/null +++ b/tests/components/modbus/__init__.py @@ -0,0 +1 @@ +"""The tests for Modbus platforms.""" diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py new file mode 100644 index 00000000000..82d0b4bd5f0 --- /dev/null +++ b/tests/components/modbus/test_modbus_sensor.py @@ -0,0 +1,361 @@ +"""The tests for the Modbus sensor component.""" +import pytest +from datetime import timedelta +from unittest import mock + +from homeassistant.const import ( + CONF_NAME, + CONF_OFFSET, + CONF_PLATFORM, + CONF_SCAN_INTERVAL, +) +from homeassistant.components.modbus import DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN +from homeassistant.components.modbus.sensor import ( + CONF_COUNT, + CONF_DATA_TYPE, + CONF_PRECISION, + CONF_REGISTER, + CONF_REGISTER_TYPE, + CONF_REGISTERS, + CONF_REVERSE_ORDER, + CONF_SCALE, + DATA_TYPE_FLOAT, + DATA_TYPE_INT, + DATA_TYPE_UINT, + REGISTER_TYPE_HOLDING, + REGISTER_TYPE_INPUT, +) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util +from tests.common import MockModule, mock_integration, async_fire_time_changed + + +@pytest.fixture() +def mock_hub(hass): + """Mock hub.""" + mock_integration(hass, MockModule(MODBUS_DOMAIN)) + hub = mock.MagicMock() + hub.name = "hub" + hass.data[MODBUS_DOMAIN] = {DEFAULT_HUB: hub} + return hub + + +common_register_config = {CONF_NAME: "test-config", CONF_REGISTER: 1234} + + +class ReadResult: + """Storage class for register read results.""" + + def __init__(self, register_words): + """Init.""" + self.registers = register_words + + +async def run_test(hass, mock_hub, register_config, register_words, expected): + """Run test for given config and check that sensor outputs expected result.""" + + # Full sensor configuration + sensor_name = "modbus_test_sensor" + scan_interval = 5 + config = { + SENSOR_DOMAIN: { + CONF_PLATFORM: "modbus", + CONF_SCAN_INTERVAL: scan_interval, + CONF_REGISTERS: [ + dict(**{CONF_NAME: sensor_name, CONF_REGISTER: 1234}, **register_config) + ], + } + } + + # Setup inputs for the sensor + read_result = ReadResult(register_words) + if register_config.get(CONF_REGISTER_TYPE) == REGISTER_TYPE_INPUT: + mock_hub.read_input_registers.return_value = read_result + else: + mock_hub.read_holding_registers.return_value = read_result + + # Initialize sensor + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + + # Trigger update call with time_changed event + now += timedelta(seconds=scan_interval + 1) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + # Check state + entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" + state = hass.states.get(entity_id).state + assert state == expected + + +async def test_simple_word_register(hass, mock_hub): + """Test conversion of single word register.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test(hass, mock_hub, register_config, register_words=[0], expected="0") + + +async def test_optional_conf_keys(hass, mock_hub): + """Test handling of optional configuration keys.""" + register_config = {} + await run_test( + hass, mock_hub, register_config, register_words=[0x8000], expected="-32768" + ) + + +async def test_offset(hass, mock_hub): + """Test offset calculation.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 13, + CONF_PRECISION: 0, + } + await run_test(hass, mock_hub, register_config, register_words=[7], expected="20") + + +async def test_scale_and_offset(hass, mock_hub): + """Test handling of scale and offset.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 3, + CONF_OFFSET: 13, + CONF_PRECISION: 0, + } + await run_test(hass, mock_hub, register_config, register_words=[7], expected="34") + + +async def test_ints_can_have_precision(hass, mock_hub): + """Test precision can be specified event if using integer values only.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 3, + CONF_OFFSET: 13, + CONF_PRECISION: 4, + } + await run_test( + hass, mock_hub, register_config, register_words=[7], expected="34.0000" + ) + + +async def test_floats_get_rounded_correctly(hass, mock_hub): + """Test that floating point values get rounded correctly.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1.5, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test(hass, mock_hub, register_config, register_words=[1], expected="2") + + +async def test_parameters_as_strings(hass, mock_hub): + """Test that scale, offset and precision can be given as strings.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: "1.5", + CONF_OFFSET: "5", + CONF_PRECISION: "1", + } + await run_test(hass, mock_hub, register_config, register_words=[9], expected="18.5") + + +async def test_floating_point_scale(hass, mock_hub): + """Test use of floating point scale.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 2.4, + CONF_OFFSET: 0, + CONF_PRECISION: 2, + } + await run_test(hass, mock_hub, register_config, register_words=[1], expected="2.40") + + +async def test_floating_point_offset(hass, mock_hub): + """Test use of floating point scale.""" + register_config = { + CONF_COUNT: 1, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: -10.3, + CONF_PRECISION: 1, + } + await run_test(hass, mock_hub, register_config, register_words=[2], expected="-8.3") + + +async def test_signed_two_word_register(hass, mock_hub): + """Test reading of signed register with two words.""" + register_config = { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_INT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected="-1985229329", + ) + + +async def test_unsigned_two_word_register(hass, mock_hub): + """Test reading of unsigned register with two words.""" + register_config = { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected=str(0x89ABCDEF), + ) + + +async def test_reversed(hass, mock_hub): + """Test handling of reversed register words.""" + register_config = { + CONF_COUNT: 2, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_REVERSE_ORDER: True, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected=str(0xCDEF89AB), + ) + + +async def test_four_word_register(hass, mock_hub): + """Test reading of 64-bit register.""" + register_config = { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF, 0x0123, 0x4567], + expected="9920249030613615975", + ) + + +async def test_four_word_register_precision_is_intact_with_int_params(hass, mock_hub): + """Test that precision is not lost when doing integer arithmetic for 64-bit register.""" + register_config = { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 2, + CONF_OFFSET: 3, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], + expected="163971058432973793", + ) + + +async def test_four_word_register_precision_is_lost_with_float_params(hass, mock_hub): + """Test that precision is affected when floating point conversion is done.""" + register_config = { + CONF_COUNT: 4, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 2.0, + CONF_OFFSET: 3.0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x0123, 0x4567, 0x89AB, 0xCDEF], + expected="163971058432973792", + ) + + +async def test_two_word_input_register(hass, mock_hub): + """Test reaging of input register.""" + register_config = { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: REGISTER_TYPE_INPUT, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected=str(0x89ABCDEF), + ) + + +async def test_two_word_holding_register(hass, mock_hub): + """Test reaging of holding register.""" + register_config = { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: REGISTER_TYPE_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_UINT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 0, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[0x89AB, 0xCDEF], + expected=str(0x89ABCDEF), + ) + + +async def test_float_data_type(hass, mock_hub): + """Test floating point register data type.""" + register_config = { + CONF_COUNT: 2, + CONF_REGISTER_TYPE: REGISTER_TYPE_HOLDING, + CONF_DATA_TYPE: DATA_TYPE_FLOAT, + CONF_SCALE: 1, + CONF_OFFSET: 0, + CONF_PRECISION: 5, + } + await run_test( + hass, + mock_hub, + register_config, + register_words=[16286, 1617], + expected="1.23457", + ) diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index d88ab8c4f70..860ef52a98a 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,7 +1,14 @@ """The tests for the MQTT discovery.""" +from pathlib import Path +import re + from unittest.mock import patch from homeassistant.components import mqtt +from homeassistant.components.mqtt.abbreviations import ( + ABBREVIATIONS, + DEVICE_ABBREVIATIONS, +) from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start from homeassistant.const import STATE_OFF, STATE_ON @@ -245,6 +252,59 @@ async def test_discovery_expansion(hass, mqtt_mock, caplog): assert state.state == STATE_ON +ABBREVIATIONS_WHITE_LIST = [ + # MQTT client/server settings + "CONF_BIRTH_MESSAGE", + "CONF_BROKER", + "CONF_CERTIFICATE", + "CONF_CLIENT_CERT", + "CONF_CLIENT_ID", + "CONF_CLIENT_KEY", + "CONF_DISCOVERY", + "CONF_DISCOVERY_PREFIX", + "CONF_EMBEDDED", + "CONF_KEEPALIVE", + "CONF_TLS_INSECURE", + "CONF_TLS_VERSION", + "CONF_WILL_MESSAGE", + # Undocumented device configuration + "CONF_DEPRECATED_VIA_HUB", + "CONF_VIA_DEVICE", + # Already short + "CONF_FAN_MODE_LIST", + "CONF_HOLD_LIST", + "CONF_HS", + "CONF_MODE_LIST", + "CONF_PRECISION", + "CONF_QOS", + "CONF_SCHEMA", + "CONF_SWING_MODE_LIST", + "CONF_TEMP_STEP", +] + + +async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): + """Check MQTT platforms for missing abbreviations.""" + missing = [] + regex = re.compile(r"(CONF_[a-zA-Z\d_]*) *= *[\'\"]([a-zA-Z\d_]*)[\'\"]") + for fil in Path(mqtt.__file__).parent.rglob("*.py"): + with open(fil) as file: + matches = re.findall(regex, file.read()) + for match in matches: + if ( + match[1] not in ABBREVIATIONS.values() + and match[1] not in DEVICE_ABBREVIATIONS.values() + and match[0] not in ABBREVIATIONS_WHITE_LIST + ): + missing.append( + "{}: no abbreviation for {} ({})".format( + fil, match[1], match[0] + ) + ) + + assert not missing + + async def test_implicit_state_topic_alarm(hass, mqtt_mock, caplog): """Test implicit state topic for alarm_control_panel.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index e9f92b2d6f6..9e313fd3694 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -2,17 +2,46 @@ import asyncio import pytest +from homeassistant.const import ENERGY_KILO_WATT_HOUR, DEVICE_CLASS_POWER + +from homeassistant import setup +from homeassistant.components import climate, sensor +from homeassistant.components.demo.sensor import DemoSensor from homeassistant.setup import async_setup_component import homeassistant.components.prometheus as prometheus @pytest.fixture -def prometheus_client(loop, hass, hass_client): +async def prometheus_client(loop, hass, hass_client): """Initialize an hass_client with Prometheus component.""" - assert loop.run_until_complete( - async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) + await async_setup_component(hass, prometheus.DOMAIN, {prometheus.DOMAIN: {}}) + + await setup.async_setup_component( + hass, sensor.DOMAIN, {"sensor": [{"platform": "demo"}]} ) - return loop.run_until_complete(hass_client()) + + await setup.async_setup_component( + hass, climate.DOMAIN, {"climate": [{"platform": "demo"}]} + ) + + sensor1 = DemoSensor("Television Energy", 74, None, ENERGY_KILO_WATT_HOUR, None) + sensor1.hass = hass + sensor1.entity_id = "sensor.television_energy" + await sensor1.async_update_ha_state() + + sensor2 = DemoSensor( + "Radio Energy", 14, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, None + ) + sensor2.hass = hass + sensor2.entity_id = "sensor.radio_energy" + await sensor2.async_update_ha_state() + + sensor3 = DemoSensor("Electricity price", 0.123, None, "SEK/kWh", None) + sensor3.hass = hass + sensor3.entity_id = "sensor.electricity_price" + await sensor3.async_update_ha_state() + + return await hass_client() @asyncio.coroutine @@ -25,11 +54,52 @@ def test_view(prometheus_client): # pylint: disable=redefined-outer-name body = yield from resp.text() body = body.split("\n") - assert len(body) > 3 # At least two comment lines and a metric - for line in body: - if line: - assert ( - line.startswith("# ") - or line.startswith("process_") - or line.startswith("python_info") - ) + assert len(body) > 3 + + assert "# HELP python_info Python platform information" in body + assert ( + "# HELP python_gc_objects_collected_total " + "Objects collected during gc" in body + ) + + assert ( + 'temperature_c{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 15.6' in body + ) + + assert ( + 'battery_level_percent{domain="sensor",' + 'entity="sensor.outside_temperature",' + 'friendly_name="Outside Temperature"} 12.0' in body + ) + + assert ( + 'current_temperature_c{domain="climate",' + 'entity="climate.heatpump",' + 'friendly_name="HeatPump"} 25.0' in body + ) + + assert ( + 'humidity_percent{domain="sensor",' + 'entity="sensor.outside_humidity",' + 'friendly_name="Outside Humidity"} 54.0' in body + ) + + assert ( + 'sensor_unit_kwh{domain="sensor",' + 'entity="sensor.television_energy",' + 'friendly_name="Television Energy"} 74.0' in body + ) + + assert ( + 'power_kwh{domain="sensor",' + 'entity="sensor.radio_energy",' + 'friendly_name="Radio Energy"} 14.0' in body + ) + + assert ( + 'sensor_unit_sek_per_kwh{domain="sensor",' + 'entity="sensor.electricity_price",' + 'friendly_name="Electricity price"} 0.123' in body + ) diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index a2eca90e60e..5b6d6f87cd5 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -142,7 +142,7 @@ async def test_config_flow_entry_migrate(hass): "media_player", "ps4", MOCK_UNIQUE_ID, - config_entry_id=MOCK_ENTRY_ID, + config_entry=mock_entry, device_id=MOCK_DEVICE_ID, ) assert len(mock_e_registry.entities) == 1 diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 145fd9e3e11..e4f2033c3cb 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -169,6 +169,11 @@ async def mock_ddp_response(hass, mock_status_data, games=None): await hass.async_block_till_done() +async def test_async_setup_platform_does_nothing(): + """Test setup platform does nothing (Uses config entries only).""" + await ps4.media_player.async_setup_platform(None, None, None) + + async def test_media_player_is_setup_correctly_with_entry(hass): """Test entity is setup correctly with entry correctly.""" mock_entity_id = await setup_mock_component(hass) @@ -187,7 +192,8 @@ async def test_state_off_is_set(hass): """Test that state is set to off.""" mock_entity_id = await setup_mock_component(hass) - await mock_ddp_response(hass, MOCK_STATUS_OFF) + with patch(MOCK_SAVE, side_effect=MagicMock()): + await mock_ddp_response(hass, MOCK_STATUS_OFF) assert hass.states.get(mock_entity_id).state == STATE_OFF @@ -212,7 +218,8 @@ async def test_state_idle_is_set(hass): """Test that state is set to idle.""" mock_entity_id = await setup_mock_component(hass) - await mock_ddp_response(hass, MOCK_STATUS_IDLE) + with patch(MOCK_SAVE, side_effect=MagicMock()): + await mock_ddp_response(hass, MOCK_STATUS_IDLE) assert hass.states.get(mock_entity_id).state == STATE_IDLE @@ -241,7 +248,6 @@ async def test_media_attributes_are_fetched(hass): with patch(mock_func, return_value=mock_coro(mock_result)) as mock_fetch, patch( MOCK_SAVE, side_effect=MagicMock() ): - await mock_ddp_response(hass, MOCK_STATUS_PLAYING) mock_state = hass.states.get(mock_entity_id) @@ -266,7 +272,9 @@ async def test_media_attributes_are_loaded(hass): "pyps4.Ps4Async.async_get_ps_store_data", ) - with patch(mock_func, return_value=mock_coro(None)) as mock_fetch: + with patch(mock_func, return_value=mock_coro(None)) as mock_fetch, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await mock_ddp_response(hass, MOCK_STATUS_PLAYING, mock_data) mock_state = hass.states.get(mock_entity_id) @@ -287,7 +295,9 @@ async def test_media_attributes_are_loaded(hass): async def test_device_info_is_set_from_status_correctly(hass): """Test that device info is set correctly from status update.""" mock_d_registry = mock_device_registry(hass) - with patch("pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF): + with patch( + "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF + ), patch(MOCK_SAVE, side_effect=MagicMock()): mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() @@ -330,7 +340,7 @@ async def test_device_info_is_assummed(hass): mock_unique_id = ps4.format_unique_id(MOCK_CREDS, MOCK_HOST_ID) mock_e_registry = mock_registry(hass) mock_e_registry.async_get_or_create( - "media_player", DOMAIN, mock_unique_id, config_entry_id=MOCK_ENTRY_ID + "media_player", DOMAIN, mock_unique_id, config_entry=MOCK_CONFIG ) mock_entity_id = mock_e_registry.async_get_entity_id( "media_player", DOMAIN, mock_unique_id @@ -369,7 +379,9 @@ async def test_turn_on(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.wakeup" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "turn_on", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -385,7 +397,9 @@ async def test_turn_off(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.standby" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "turn_off", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -401,7 +415,9 @@ async def test_media_pause(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "media_pause", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -417,7 +433,9 @@ async def test_media_stop(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( "media_player", "media_stop", {ATTR_ENTITY_ID: mock_entity_id} ) @@ -438,7 +456,9 @@ async def test_select_source(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title name. await hass.services.async_call( "media_player", @@ -462,7 +482,9 @@ async def test_select_source_caps(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title name in caps. await hass.services.async_call( "media_player", @@ -489,7 +511,9 @@ async def test_select_source_id(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.start_title" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): # Test with title ID. await hass.services.async_call( "media_player", @@ -508,7 +532,9 @@ async def test_ps4_send_command(hass): "homeassistant.components.ps4.media_player.", "pyps4.Ps4Async.remote_control" ) - with patch(mock_func, return_value=MagicMock()) as mock_call: + with patch(mock_func, return_value=MagicMock()) as mock_call, patch( + MOCK_SAVE, side_effect=MagicMock() + ): await hass.services.async_call( DOMAIN, "send_command", {ATTR_ENTITY_ID: mock_entity_id, ATTR_COMMAND: "ps"} ) diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index fcf1519d4c7..d7732c00f94 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -3,8 +3,11 @@ import asyncio import logging from unittest.mock import patch, mock_open +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component -from homeassistant.components.python_script import execute +from homeassistant.components.python_script import DOMAIN, execute, FOLDER + +from tests.common import patch_yaml_files @asyncio.coroutine @@ -289,6 +292,101 @@ def test_reload(hass): assert hass.services.has_service("python_script", "reload") +async def test_service_descriptions(hass): + """Test that service descriptions are loaded and reloaded correctly.""" + # Test 1: no user-provided services.yaml file + scripts1 = [ + "/some/config/dir/python_scripts/hello.py", + "/some/config/dir/python_scripts/world_beer.py", + ] + + service_descriptions1 = ( + "hello:\n" + " description: Description of hello.py.\n" + " fields:\n" + " fake_param:\n" + " description: Parameter used by hello.py.\n" + " example: 'This is a test of python_script.hello'" + ) + services_yaml1 = { + "{}/{}/services.yaml".format( + hass.config.config_dir, FOLDER + ): service_descriptions1 + } + + with patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts1 + ), patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), patch_yaml_files( + services_yaml1 + ): + await async_setup_component(hass, DOMAIN, {}) + + descriptions = await async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert descriptions[DOMAIN]["hello"]["description"] == "Description of hello.py." + assert ( + descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["description"] + == "Parameter used by hello.py." + ) + assert ( + descriptions[DOMAIN]["hello"]["fields"]["fake_param"]["example"] + == "This is a test of python_script.hello" + ) + + assert descriptions[DOMAIN]["world_beer"]["description"] == "" + assert bool(descriptions[DOMAIN]["world_beer"]["fields"]) is False + + # Test 2: user-provided services.yaml file + scripts2 = [ + "/some/config/dir/python_scripts/hello2.py", + "/some/config/dir/python_scripts/world_beer.py", + ] + + service_descriptions2 = ( + "hello2:\n" + " description: Description of hello2.py.\n" + " fields:\n" + " fake_param:\n" + " description: Parameter used by hello2.py.\n" + " example: 'This is a test of python_script.hello2'" + ) + services_yaml2 = { + "{}/{}/services.yaml".format( + hass.config.config_dir, FOLDER + ): service_descriptions2 + } + + with patch( + "homeassistant.components.python_script.os.path.isdir", return_value=True + ), patch( + "homeassistant.components.python_script.glob.iglob", return_value=scripts2 + ), patch( + "homeassistant.components.python_script.os.path.exists", return_value=True + ), patch_yaml_files( + services_yaml2 + ): + await hass.services.async_call(DOMAIN, "reload", {}, blocking=True) + descriptions = await async_get_all_descriptions(hass) + + assert len(descriptions) == 1 + + assert descriptions[DOMAIN]["hello2"]["description"] == "Description of hello2.py." + assert ( + descriptions[DOMAIN]["hello2"]["fields"]["fake_param"]["description"] + == "Parameter used by hello2.py." + ) + assert ( + descriptions[DOMAIN]["hello2"]["fields"]["fake_param"]["example"] + == "This is a test of python_script.hello2" + ) + + @asyncio.coroutine def test_sleep_warns_one(hass, caplog): """Test time.sleep warns once.""" diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py new file mode 100644 index 00000000000..1228f998618 --- /dev/null +++ b/tests/components/ring/common.py @@ -0,0 +1,14 @@ +"""Common methods used across the tests for ring devices.""" +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_SCAN_INTERVAL +from homeassistant.components.ring import DOMAIN +from homeassistant.setup import async_setup_component + + +async def setup_platform(hass, platform): + """Set up the ring platform and prerequisites.""" + config = { + DOMAIN: {CONF_USERNAME: "foo", CONF_PASSWORD: "bar", CONF_SCAN_INTERVAL: 1000}, + platform: {"platform": DOMAIN}, + } + assert await async_setup_component(hass, platform, config) + await hass.async_block_till_done() diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py new file mode 100644 index 00000000000..14a29f78aae --- /dev/null +++ b/tests/components/ring/conftest.py @@ -0,0 +1,54 @@ +"""Configuration for Ring tests.""" +import requests_mock +import pytest +from tests.common import load_fixture +from asynctest import patch + + +@pytest.fixture(name="ring_mock") +def ring_save_mock(): + """Fixture to mock a ring.""" + with patch("ring_doorbell._exists_cache", return_value=False): + with patch("ring_doorbell._save_cache", return_value=True) as save_mock: + yield save_mock + + +@pytest.fixture(name="requests_mock") +def requests_mock_fixture(ring_mock): + """Fixture to provide a requests mocker.""" + with requests_mock.mock() as mock: + # Note all devices have an id of 987652, but a different device_id. + # the device_id is used as our unique_id, but the id is what is sent + # to the APIs, which is why every mock uses that id. + + # Mocks the response for authenticating + mock.post( + "https://oauth.ring.com/oauth/token", text=load_fixture("ring_oauth.json") + ) + # Mocks the response for getting the login session + mock.post( + "https://api.ring.com/clients_api/session", + text=load_fixture("ring_session.json"), + ) + # Mocks the response for getting all the devices + mock.get( + "https://api.ring.com/clients_api/ring_devices", + text=load_fixture("ring_devices.json"), + ) + # Mocks the response for getting the history of a device + mock.get( + "https://api.ring.com/clients_api/doorbots/987652/history", + text=load_fixture("ring_doorbots.json"), + ) + # Mocks the response for getting the health of a device + mock.get( + "https://api.ring.com/clients_api/doorbots/987652/health", + text=load_fixture("ring_doorboot_health_attrs.json"), + ) + # Mocks the response for getting a chimes health + mock.get( + "https://api.ring.com/clients_api/chimes/999999/health", + text=load_fixture("ring_chime_health_attrs.json"), + ) + + yield mock diff --git a/tests/components/ring/test_light.py b/tests/components/ring/test_light.py new file mode 100644 index 00000000000..e07867c19b2 --- /dev/null +++ b/tests/components/ring/test_light.py @@ -0,0 +1,75 @@ +"""The tests for the Ring light platform.""" +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from tests.common import load_fixture +from .common import setup_platform + + +async def test_entity_registry(hass, requests_mock): + """Tests that the devices are registed in the entity registry.""" + await setup_platform(hass, LIGHT_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get("light.front_light") + assert entry.unique_id == "aacdef123" + + entry = entity_registry.async_get("light.internal_light") + assert entry.unique_id == "aacdef124" + + +async def test_light_off_reports_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be off is correct.""" + await setup_platform(hass, LIGHT_DOMAIN) + + state = hass.states.get("light.front_light") + assert state.state == "off" + assert state.attributes.get("friendly_name") == "Front light" + + +async def test_light_on_reports_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be on is correct.""" + await setup_platform(hass, LIGHT_DOMAIN) + + state = hass.states.get("light.internal_light") + assert state.state == "on" + assert state.attributes.get("friendly_name") == "Internal light" + + +async def test_light_can_be_turned_on(hass, requests_mock): + """Tests the light turns on correctly.""" + await setup_platform(hass, LIGHT_DOMAIN) + + # Mocks the response for turning a light on + requests_mock.put( + "https://api.ring.com/clients_api/doorbots/987652/floodlight_light_on", + text=load_fixture("ring_doorbot_siren_on_response.json"), + ) + + state = hass.states.get("light.front_light") + assert state.state == "off" + + await hass.services.async_call( + "light", "turn_on", {"entity_id": "light.front_light"}, blocking=True + ) + await hass.async_block_till_done() + + state = hass.states.get("light.front_light") + assert state.state == "on" + + +async def test_updates_work(hass, requests_mock): + """Tests the update service works correctly.""" + await setup_platform(hass, LIGHT_DOMAIN) + state = hass.states.get("light.front_light") + assert state.state == "off" + # Changes the return to indicate that the light is now on. + requests_mock.get( + "https://api.ring.com/clients_api/ring_devices", + text=load_fixture("ring_devices_updated.json"), + ) + + await hass.services.async_call("ring", "update", {}, blocking=True) + + await hass.async_block_till_done() + + state = hass.states.get("light.front_light") + assert state.state == "on" diff --git a/tests/components/ring/test_switch.py b/tests/components/ring/test_switch.py new file mode 100644 index 00000000000..864d16466da --- /dev/null +++ b/tests/components/ring/test_switch.py @@ -0,0 +1,76 @@ +"""The tests for the Ring switch platform.""" +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from tests.common import load_fixture +from .common import setup_platform + + +async def test_entity_registry(hass, requests_mock): + """Tests that the devices are registed in the entity registry.""" + await setup_platform(hass, SWITCH_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get("switch.front_siren") + assert entry.unique_id == "aacdef123-siren" + + entry = entity_registry.async_get("switch.internal_siren") + assert entry.unique_id == "aacdef124-siren" + + +async def test_siren_off_reports_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be off is correct.""" + await setup_platform(hass, SWITCH_DOMAIN) + + state = hass.states.get("switch.front_siren") + assert state.state == "off" + assert state.attributes.get("friendly_name") == "Front siren" + + +async def test_siren_on_reports_correctly(hass, requests_mock): + """Tests that the initial state of a device that should be on is correct.""" + await setup_platform(hass, SWITCH_DOMAIN) + + state = hass.states.get("switch.internal_siren") + assert state.state == "on" + assert state.attributes.get("friendly_name") == "Internal siren" + assert state.attributes.get("icon") == "mdi:alarm-bell" + + +async def test_siren_can_be_turned_on(hass, requests_mock): + """Tests the siren turns on correctly.""" + await setup_platform(hass, SWITCH_DOMAIN) + + # Mocks the response for turning a siren on + requests_mock.put( + "https://api.ring.com/clients_api/doorbots/987652/siren_on", + text=load_fixture("ring_doorbot_siren_on_response.json"), + ) + + state = hass.states.get("switch.front_siren") + assert state.state == "off" + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.front_siren"}, blocking=True + ) + + await hass.async_block_till_done() + state = hass.states.get("switch.front_siren") + assert state.state == "on" + + +async def test_updates_work(hass, requests_mock): + """Tests the update service works correctly.""" + await setup_platform(hass, SWITCH_DOMAIN) + state = hass.states.get("switch.front_siren") + assert state.state == "off" + # Changes the return to indicate that the siren is now on. + requests_mock.get( + "https://api.ring.com/clients_api/ring_devices", + text=load_fixture("ring_devices_updated.json"), + ) + + await hass.services.async_call("ring", "update", {}, blocking=True) + + await hass.async_block_till_done() + + state = hass.states.get("switch.front_siren") + assert state.state == "on" diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 7be682eff5e..d675034e744 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -17,6 +17,7 @@ from homeassistant.const import ( EVENT_SCRIPT_STARTED, ) from homeassistant.core import Context, callback, split_entity_id +from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass from homeassistant.setup import setup_component, async_setup_component from homeassistant.exceptions import ServiceNotFound @@ -244,6 +245,61 @@ class TestScriptComponent(unittest.TestCase): assert self.hass.services.has_service(script.DOMAIN, "test2") +async def test_service_descriptions(hass): + """Test that service descriptions are loaded and reloaded correctly.""" + # Test 1: has "description" but no "fields" + assert await async_setup_component( + hass, + "script", + { + "script": { + "test": { + "description": "test description", + "sequence": [{"delay": {"seconds": 5}}], + } + } + }, + ) + + descriptions = await async_get_all_descriptions(hass) + + assert descriptions[DOMAIN]["test"]["description"] == "test description" + assert not descriptions[DOMAIN]["test"]["fields"] + + # Test 2: has "fields" but no "description" + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + with patch( + "homeassistant.config.load_yaml_config_file", + return_value={ + "script": { + "test": { + "fields": { + "test_param": { + "description": "test_param description", + "example": "test_param example", + } + }, + "sequence": [{"delay": {"seconds": 5}}], + } + } + }, + ): + with patch("homeassistant.config.find_config_file", return_value=""): + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) + + descriptions = await async_get_all_descriptions(hass) + + assert descriptions[script.DOMAIN]["test"]["description"] == "" + assert ( + descriptions[script.DOMAIN]["test"]["fields"]["test_param"]["description"] + == "test_param description" + ) + assert ( + descriptions[script.DOMAIN]["test"]["fields"]["test_param"]["example"] + == "test_param example" + ) + + async def test_shared_context(hass): """Test that the shared context is passed down the chain.""" event = "test_event" @@ -306,3 +362,39 @@ async def test_turning_no_scripts_off(hass): await hass.services.async_call( DOMAIN, SERVICE_TURN_OFF, {"entity_id": []}, blocking=True ) + + +async def test_async_get_descriptions_script(hass): + """Test async_set_service_schema for the script integration.""" + script = hass.components.script + script_config = { + script.DOMAIN: { + "test1": {"sequence": [{"service": "homeassistant.restart"}]}, + "test2": { + "description": "test2", + "fields": { + "param": { + "description": "param_description", + "example": "param_example", + } + }, + "sequence": [{"service": "homeassistant.restart"}], + }, + } + } + + await async_setup_component(hass, script.DOMAIN, script_config) + descriptions = await hass.helpers.service.async_get_all_descriptions() + + assert descriptions[script.DOMAIN]["test1"]["description"] == "" + assert not descriptions[script.DOMAIN]["test1"]["fields"] + + assert descriptions[script.DOMAIN]["test2"]["description"] == "test2" + assert ( + descriptions[script.DOMAIN]["test2"]["fields"]["param"]["description"] + == "param_description" + ) + assert ( + descriptions[script.DOMAIN]["test2"]["fields"]["param"]["example"] + == "param_example" + ) diff --git a/tests/components/sma/__init__.py b/tests/components/sma/__init__.py new file mode 100644 index 00000000000..124f481135e --- /dev/null +++ b/tests/components/sma/__init__.py @@ -0,0 +1 @@ +"""SMA tests.""" diff --git a/tests/components/sma/test_sensor.py b/tests/components/sma/test_sensor.py new file mode 100644 index 00000000000..bee1743791c --- /dev/null +++ b/tests/components/sma/test_sensor.py @@ -0,0 +1,51 @@ +"""SMA sensor tests.""" +import logging + +from homeassistant.components.sensor import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component + +_LOGGER = logging.getLogger(__name__) +BASE_CFG = { + "platform": "sma", + "host": "1.1.1.1", + "password": "", + "custom": {"my_sensor": {"key": "1234567890123", "unit": "V"}}, +} + + +async def test_sma_config_old(hass): + """Test old config.""" + sensors = {"current_consumption": ["current_consumption"]} + + with assert_setup_component(1): + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)} + ) + + state = hass.states.get("sensor.current_consumption") + assert state + assert "unit_of_measurement" in state.attributes + assert "current_consumption" in state.attributes + + state = hass.states.get("sensor.my_sensor") + assert not state + + +async def test_sma_config(hass): + """Test new config.""" + sensors = ["current_consumption"] + + with assert_setup_component(1): + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: dict(BASE_CFG, sensors=sensors)} + ) + + state = hass.states.get("sensor.current_consumption") + assert state + assert "unit_of_measurement" in state.attributes + assert "current_consumption" not in state.attributes + + state = hass.states.get("sensor.my_sensor") + assert state diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 3f93c442985..b3b172e7606 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -56,6 +56,7 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, CONN_CLASS_CLOUD_PUSH, + system_options={}, ) broker = DeviceBroker( hass, config_entry, Mock(), Mock(), devices or [], scenes or [] diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index 0ff4b08c750..5724d7a3bac 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -6,6 +6,7 @@ from asynctest import Mock, patch from pysmartthings import APIResponseError from homeassistant import data_entry_flow +from homeassistant.setup import async_setup_component from homeassistant.components import cloud from homeassistant.components.smartthings import smartapp from homeassistant.components.smartthings.config_flow import SmartThingsFlowHandler @@ -288,6 +289,7 @@ async def test_multiple_config_entry_created_when_installed( hass, app, locations, installed_apps, smartthings_mock ): """Test a config entries are created for multiple installs.""" + assert await async_setup_component(hass, "persistent_notification", {}) flow = SmartThingsFlowHandler() flow.hass = hass flow.access_token = str(uuid4()) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 881be3b992d..15b556f1d83 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -6,6 +6,7 @@ from asynctest import Mock, patch from pysmartthings import InstalledAppStatus, OAuthToken import pytest +from homeassistant.setup import async_setup_component from homeassistant.components import cloud, smartthings from homeassistant.components.smartthings.const import ( CONF_CLOUDHOOK_URL, @@ -25,6 +26,7 @@ from tests.common import MockConfigEntry async def test_migration_creates_new_flow(hass, smartthings_mock, config_entry): """Test migration deletes app and creates new flow.""" + assert await async_setup_component(hass, "persistent_notification", {}) config_entry.version = 1 config_entry.add_to_hass(hass) @@ -50,6 +52,7 @@ async def test_unrecoverable_api_errors_create_new_flow( 403 (forbidden/not found): Occurs when the app or installed app could not be retrieved/found (likely deleted?) """ + assert await async_setup_component(hass, "persistent_notification", {}) config_entry.add_to_hass(hass) smartthings_mock.app.side_effect = ClientResponseError(None, None, status=401) diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 32aa2d56558..2a28876f552 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -60,7 +60,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.states.set("binary_sensor.test_monitored", value) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_count") + state = self.hass.states.get("sensor.test") assert str(len(values)) == state.state @@ -87,7 +87,7 @@ class TestStatisticsSensor(unittest.TestCase): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert str(self.mean) == state.state assert self.min == state.attributes.get("min_value") @@ -126,7 +126,7 @@ class TestStatisticsSensor(unittest.TestCase): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert 3.8 == state.attributes.get("min_value") assert 14 == state.attributes.get("max_value") @@ -155,7 +155,7 @@ class TestStatisticsSensor(unittest.TestCase): ) self.hass.block_till_done() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") # require only one data point assert self.values[-1] == state.attributes.get("min_value") @@ -206,7 +206,7 @@ class TestStatisticsSensor(unittest.TestCase): # insert the next value one minute later mock_data["return_time"] += timedelta(minutes=1) - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert 6 == state.attributes.get("min_value") assert 14 == state.attributes.get("max_value") @@ -248,7 +248,7 @@ class TestStatisticsSensor(unittest.TestCase): # insert the next value one minute later mock_data["return_time"] += timedelta(minutes=1) - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert datetime( 2017, 8, 2, 12, 23, 42, tzinfo=dt_util.UTC @@ -290,7 +290,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() # check if the result is as in test_sensor_source() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert str(self.mean) == state.state @pytest.mark.skip("Flaky in CI") @@ -355,7 +355,7 @@ class TestStatisticsSensor(unittest.TestCase): self.hass.block_till_done() # check if the result is as in test_sensor_source() - state = self.hass.states.get("sensor.test_mean") + state = self.hass.states.get("sensor.test") assert expected_min_age == state.attributes.get("min_age") # The max_age timestamp should be 1 hour before what we have right diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index c0b73f9c559..c8cec168d6e 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -166,6 +166,38 @@ class TestBinarySensorTemplate(unittest.TestCase): state = self.hass.states.get("binary_sensor.test_template_sensor") assert state.attributes["entity_picture"] == "/local/sensor.png" + def test_attribute_templates(self): + """Test attribute_templates template.""" + with assert_setup_component(1): + assert setup.setup_component( + self.hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "test_template_sensor": { + "value_template": "{{ states.sensor.xyz.state }}", + "attribute_templates": { + "test_attribute": "It {{ states.sensor.test_state.state }}." + }, + } + }, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + state = self.hass.states.get("binary_sensor.test_template_sensor") + assert state.attributes.get("test_attribute") == "It ." + + self.hass.states.set("sensor.test_state", "Works") + self.hass.block_till_done() + state = self.hass.states.get("binary_sensor.test_template_sensor") + assert state.attributes["test_attribute"] == "It Works." + @mock.patch( "homeassistant.components.template.binary_sensor." "BinarySensorTemplate._async_render" @@ -209,6 +241,7 @@ class TestBinarySensorTemplate(unittest.TestCase): MATCH_ALL, None, None, + None, ).result() assert not vs.should_poll assert "motion" == vs.device_class @@ -268,6 +301,7 @@ class TestBinarySensorTemplate(unittest.TestCase): MATCH_ALL, None, None, + None, ).result() mock_render.side_effect = TemplateError("foo") run_callback_threadsafe(self.hass.loop, vs.async_check_state).result() @@ -394,6 +428,36 @@ async def test_template_delay_off(hass): assert state.state == "on" +async def test_invalid_attribute_template(hass, caplog): + """Test that errors are logged if rendering template fails.""" + hass.states.async_set("binary_sensor.test_sensor", "true") + + await setup.async_setup_component( + hass, + "binary_sensor", + { + "binary_sensor": { + "platform": "template", + "sensors": { + "invalid_template": { + "value_template": "{{ states.binary_sensor.test_sensor }}", + "attribute_templates": { + "test_attribute": "{{ states.binary_sensor.unknown.attributes.picture }}" + }, + } + }, + } + }, + ) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 2 + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.invalid_template" + ) + + assert ("Error rendering attribute test_attribute") in caplog.text + + async def test_no_update_template_match_all(hass, caplog): """Test that we do not update sensors that match on all.""" hass.states.async_set("binary_sensor.test_sensor", "true") @@ -414,12 +478,16 @@ async def test_no_update_template_match_all(hass, caplog): "value_template": "{{ states.binary_sensor.test_sensor.state }}", "entity_picture_template": "{{ 1 + 1 }}", }, + "all_attribute": { + "value_template": "{{ states.binary_sensor.test_sensor.state }}", + "attribute_templates": {"test_attribute": "{{ 1 + 1 }}"}, + }, }, } }, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 5 assert ( "Template binary sensor all_state has no entity ids " "configured to track nor were we able to extract the entities to " @@ -435,10 +503,16 @@ async def test_no_update_template_match_all(hass, caplog): "configured to track nor were we able to extract the entities to " "track from the entity_picture template" ) in caplog.text + assert ( + "Template binary sensor all_attribute has no entity ids " + "configured to track nor were we able to extract the entities to " + "track from the test_attribute template" + ) in caplog.text assert hass.states.get("binary_sensor.all_state").state == "off" assert hass.states.get("binary_sensor.all_icon").state == "off" assert hass.states.get("binary_sensor.all_entity_picture").state == "off" + assert hass.states.get("binary_sensor.all_attribute").state == "off" hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -446,6 +520,7 @@ async def test_no_update_template_match_all(hass, caplog): assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "on" assert hass.states.get("binary_sensor.all_entity_picture").state == "on" + assert hass.states.get("binary_sensor.all_attribute").state == "on" hass.states.async_set("binary_sensor.test_sensor", "false") await hass.async_block_till_done() @@ -453,13 +528,18 @@ async def test_no_update_template_match_all(hass, caplog): assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "on" assert hass.states.get("binary_sensor.all_entity_picture").state == "on" + assert hass.states.get("binary_sensor.all_attribute").state == "on" await hass.helpers.entity_component.async_update_entity("binary_sensor.all_state") await hass.helpers.entity_component.async_update_entity("binary_sensor.all_icon") await hass.helpers.entity_component.async_update_entity( "binary_sensor.all_entity_picture" ) + await hass.helpers.entity_component.async_update_entity( + "binary_sensor.all_attribute" + ) assert hass.states.get("binary_sensor.all_state").state == "on" assert hass.states.get("binary_sensor.all_icon").state == "off" assert hass.states.get("binary_sensor.all_entity_picture").state == "off" + assert hass.states.get("binary_sensor.all_attribute").state == "off" diff --git a/tests/components/traccar/__init__.py b/tests/components/traccar/__init__.py new file mode 100644 index 00000000000..48c7818452f --- /dev/null +++ b/tests/components/traccar/__init__.py @@ -0,0 +1 @@ +"""Tests for the Traccar component.""" diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py new file mode 100644 index 00000000000..5a2dabcf6c2 --- /dev/null +++ b/tests/components/traccar/test_init.py @@ -0,0 +1,243 @@ +"""The tests the for Traccar device tracker platform.""" +from unittest.mock import Mock, patch + +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components import traccar, zone +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.traccar import DOMAIN, TRACKER_UPDATE +from homeassistant.const import ( + HTTP_OK, + HTTP_UNPROCESSABLE_ENTITY, + STATE_HOME, + STATE_NOT_HOME, +) +from homeassistant.helpers.dispatcher import DATA_DISPATCHER +from homeassistant.setup import async_setup_component + +HOME_LATITUDE = 37.239622 +HOME_LONGITUDE = -115.815811 + + +@pytest.fixture(autouse=True) +def mock_dev_track(mock_device_tracker_conf): + """Mock device tracker config loading.""" + pass + + +@pytest.fixture(name="client") +async def traccar_client(loop, hass, aiohttp_client): + """Mock client for Traccar (unauthenticated).""" + assert await async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + + await hass.async_block_till_done() + + with patch("homeassistant.components.device_tracker.legacy.update_config"): + return await aiohttp_client(hass.http.app) + + +@pytest.fixture(autouse=True) +async def setup_zones(loop, hass): + """Set up Zone config in HA.""" + assert await async_setup_component( + hass, + zone.DOMAIN, + { + "zone": { + "name": "Home", + "latitude": HOME_LATITUDE, + "longitude": HOME_LONGITUDE, + "radius": 100, + } + }, + ) + await hass.async_block_till_done() + + +@pytest.fixture(name="webhook_id") +async def webhook_id_fixture(hass, client): + """Initialize the Traccar component and get the webhook_id.""" + hass.config.api = Mock(base_url="http://example.com") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM, result + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + return result["result"].data["webhook_id"] + + +async def test_missing_data(hass, client, webhook_id): + """Test missing data.""" + url = "/api/webhook/{}".format(webhook_id) + data = {"lat": "1.0", "lon": "1.1", "id": "123"} + + # No data + req = await client.post(url) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + # No latitude + copy = data.copy() + del copy["lat"] + req = await client.post(url, params=copy) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + # No device + copy = data.copy() + del copy["id"] + req = await client.post(url, params=copy) + await hass.async_block_till_done() + assert req.status == HTTP_UNPROCESSABLE_ENTITY + + +async def test_enter_and_exit(hass, client, webhook_id): + """Test when there is a known zone.""" + url = "/api/webhook/{}".format(webhook_id) + data = {"lat": str(HOME_LATITUDE), "lon": str(HOME_LONGITUDE), "id": "123"} + + # Enter the Home + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get( + "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) + ).state + assert STATE_HOME == state_name + + # Enter Home again + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get( + "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) + ).state + assert STATE_HOME == state_name + + data["lon"] = 0 + data["lat"] = 0 + + # Enter Somewhere else + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get( + "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) + ).state + assert STATE_NOT_HOME == state_name + + dev_reg = await hass.helpers.device_registry.async_get_registry() + assert len(dev_reg.devices) == 1 + + ent_reg = await hass.helpers.entity_registry.async_get_registry() + assert len(ent_reg.entities) == 1 + + +async def test_enter_with_attrs(hass, client, webhook_id): + """Test when additional attributes are present.""" + url = "/api/webhook/{}".format(webhook_id) + data = { + "timestamp": 123456789, + "lat": "1.0", + "lon": "1.1", + "id": "123", + "accuracy": "10.5", + "batt": 10, + "speed": 100, + "bearing": "105.32", + "altitude": 102, + } + + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])) + assert state.state == STATE_NOT_HOME + assert state.attributes["gps_accuracy"] == 10.5 + assert state.attributes["battery_level"] == 10.0 + assert state.attributes["speed"] == 100.0 + assert state.attributes["bearing"] == 105.32 + assert state.attributes["altitude"] == 102.0 + + data = { + "lat": str(HOME_LATITUDE), + "lon": str(HOME_LONGITUDE), + "id": "123", + "accuracy": 123, + "batt": 23, + "speed": 23, + "bearing": 123, + "altitude": 123, + } + + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"])) + assert state.state == STATE_HOME + assert state.attributes["gps_accuracy"] == 123 + assert state.attributes["battery_level"] == 23 + assert state.attributes["speed"] == 23 + assert state.attributes["bearing"] == 123 + assert state.attributes["altitude"] == 123 + + +async def test_two_devices(hass, client, webhook_id): + """Test updating two different devices.""" + url = "/api/webhook/{}".format(webhook_id) + + data_device_1 = {"lat": "1.0", "lon": "1.1", "id": "device_1"} + + # Exit Home + req = await client.post(url, params=data_device_1) + await hass.async_block_till_done() + assert req.status == HTTP_OK + + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["id"])) + assert state.state == "not_home" + + # Enter Home + data_device_2 = dict(data_device_1) + data_device_2["lat"] = str(HOME_LATITUDE) + data_device_2["lon"] = str(HOME_LONGITUDE) + data_device_2["id"] = "device_2" + req = await client.post(url, params=data_device_2) + await hass.async_block_till_done() + assert req.status == HTTP_OK + + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_2["id"])) + assert state.state == "home" + state = hass.states.get("{}.{}".format(DEVICE_TRACKER_DOMAIN, data_device_1["id"])) + assert state.state == "not_home" + + +@pytest.mark.xfail( + reason="The device_tracker component does not support unloading yet." +) +async def test_load_unload_entry(hass, client, webhook_id): + """Test that the appropriate dispatch signals are added and removed.""" + url = "/api/webhook/{}".format(webhook_id) + data = {"lat": str(HOME_LATITUDE), "lon": str(HOME_LONGITUDE), "id": "123"} + + # Enter the Home + req = await client.post(url, params=data) + await hass.async_block_till_done() + assert req.status == HTTP_OK + state_name = hass.states.get( + "{}.{}".format(DEVICE_TRACKER_DOMAIN, data["id"]) + ).state + assert STATE_HOME == state_name + assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1 + + entry = hass.config_entries.async_entries(DOMAIN)[0] + + assert await traccar.async_unload_entry(hass, entry) + await hass.async_block_till_done() + assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE] diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index f92eebabd20..714db8604b2 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -37,9 +37,23 @@ ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} async def test_controller_setup(): """Successful setup.""" hass = Mock() - hass.data = {UNIFI_CONFIG: {}} + hass.data = { + UNIFI_CONFIG: [ + { + CONF_HOST: CONTROLLER_DATA[CONF_HOST], + CONF_SITE_ID: "nice name", + controller.CONF_BLOCK_CLIENT: [], + controller.CONF_TRACK_CLIENTS: True, + controller.CONF_TRACK_DEVICES: True, + controller.CONF_TRACK_WIRED_CLIENTS: True, + controller.CONF_DETECTION_TIME: 300, + controller.CONF_SSID_FILTER: [], + } + ] + } entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) @@ -89,6 +103,7 @@ async def test_controller_mac(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} client = Mock() client.ip = "1.2.3.4" client.mac = "00:11:22:33:44:55" @@ -111,6 +126,7 @@ async def test_controller_no_mac(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} client = Mock() client.ip = "5.6.7.8" api = Mock() @@ -182,6 +198,7 @@ async def test_reset_unloads_entry_if_setup(): hass.data = {UNIFI_CONFIG: {}} entry = Mock() entry.data = ENTRY_CONFIG + entry.options = {} api = Mock() api.initialize.return_value = mock_coro(True) api.sites.return_value = mock_coro(CONTROLLER_SITES) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index d5783e58818..30c2191625e 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -14,6 +14,7 @@ from homeassistant.components import unifi from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, + CONF_SSID_FILTER, UNIFI_CONFIG, ) from homeassistant.const import ( @@ -133,7 +134,7 @@ def mock_controller(hass): return controller -async def setup_controller(hass, mock_controller): +async def setup_controller(hass, mock_controller, options={}): """Load the UniFi switch platform with the provided controller.""" hass.config.components.add(unifi.DOMAIN) hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} @@ -145,6 +146,8 @@ async def setup_controller(hass, mock_controller): "test", config_entries.CONN_CLASS_LOCAL_POLL, entry_id=1, + system_options={}, + options=options, ) mock_controller.config_entry = config_entry @@ -181,9 +184,9 @@ async def test_tracked_devices(hass, mock_controller): """Test the update_items function with some clients.""" mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2, CLIENT_3]) mock_controller.mock_device_responses.append([DEVICE_1, DEVICE_2]) - mock_controller.unifi_config = {unifi_dt.CONF_SSID_FILTER: ["ssid"]} + options = {CONF_SSID_FILTER: ["ssid"]} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 5 @@ -233,7 +236,18 @@ async def test_restoring_client(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_2]) mock_controller.mock_device_responses.append({}) mock_controller.mock_client_all_responses.append([CLIENT_1]) - mock_controller.unifi_config = {unifi.CONF_BLOCK_CLIENT: True} + options = {unifi.CONF_BLOCK_CLIENT: True} + + config_entry = config_entries.ConfigEntry( + 1, + unifi.DOMAIN, + "Mock Title", + ENTRY_CONFIG, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + entry_id=1, + system_options={}, + ) registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( @@ -241,17 +255,17 @@ async def test_restoring_client(hass, mock_controller): unifi_dt.UNIFI_DOMAIN, "{}-mock-site".format(CLIENT_1["mac"]), suggested_object_id=CLIENT_1["hostname"], - config_entry_id=1, + config_entry=config_entry, ) registry.async_get_or_create( device_tracker.DOMAIN, unifi_dt.UNIFI_DOMAIN, "{}-mock-site".format(CLIENT_2["mac"]), suggested_object_id=CLIENT_2["hostname"], - config_entry_id=1, + config_entry=config_entry, ) - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 4 @@ -263,9 +277,9 @@ async def test_dont_track_clients(hass, mock_controller): """Test dont track clients config works.""" mock_controller.mock_client_responses.append([CLIENT_1]) mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_CLIENTS: True} + options = {unifi.controller.CONF_TRACK_CLIENTS: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 @@ -281,9 +295,9 @@ async def test_dont_track_devices(hass, mock_controller): """Test dont track devices config works.""" mock_controller.mock_client_responses.append([CLIENT_1]) mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_DEVICES: True} + options = {unifi.controller.CONF_TRACK_DEVICES: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 @@ -299,9 +313,9 @@ async def test_dont_track_wired_clients(hass, mock_controller): """Test dont track wired clients config works.""" mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) mock_controller.mock_device_responses.append({}) - mock_controller.unifi_config = {unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True} + options = {unifi.controller.CONF_TRACK_WIRED_CLIENTS: False} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 3 diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index c7f87579c08..b725e34f61d 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,5 +1,4 @@ """Test UniFi setup process.""" -from datetime import timedelta from unittest.mock import Mock, patch from homeassistant.components import unifi @@ -44,7 +43,7 @@ async def test_setup_with_config(hass): unifi.CONF_HOST: "1.2.3.4", unifi.CONF_SITE_ID: "My site", unifi.CONF_BLOCK_CLIENT: ["12:34:56:78:90:AB"], - unifi.CONF_DETECTION_TIME: timedelta(seconds=3), + unifi.CONF_DETECTION_TIME: 3, unifi.CONF_SSID_FILTER: ["ssid"], } ] diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index f84efa5dada..3ac9ddb17dc 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -250,7 +250,7 @@ def mock_controller(hass): return controller -async def setup_controller(hass, mock_controller): +async def setup_controller(hass, mock_controller, options={}): """Load the UniFi switch platform with the provided controller.""" hass.config.components.add(unifi.DOMAIN) hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} @@ -262,6 +262,8 @@ async def setup_controller(hass, mock_controller): "test", config_entries.CONN_CLASS_LOCAL_POLL, entry_id=1, + system_options={}, + options=options, ) mock_controller.config_entry = config_entry @@ -319,11 +321,9 @@ async def test_switches(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_4]) mock_controller.mock_device_responses.append([DEVICE_1]) mock_controller.mock_client_all_responses.append([BLOCKED, UNBLOCKED, CLIENT_1]) - mock_controller.unifi_config = { - unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]] - } + options = {unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]} - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 5 @@ -466,7 +466,18 @@ async def test_restoring_client(hass, mock_controller): mock_controller.mock_client_responses.append([CLIENT_2]) mock_controller.mock_device_responses.append([DEVICE_1]) mock_controller.mock_client_all_responses.append([CLIENT_1]) - mock_controller.unifi_config = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} + options = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} + + config_entry = config_entries.ConfigEntry( + 1, + unifi.DOMAIN, + "Mock Title", + ENTRY_CONFIG, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + entry_id=1, + system_options={}, + ) registry = await entity_registry.async_get_registry(hass) registry.async_get_or_create( @@ -474,17 +485,17 @@ async def test_restoring_client(hass, mock_controller): unifi.DOMAIN, "poe-{}".format(CLIENT_1["mac"]), suggested_object_id=CLIENT_1["hostname"], - config_entry_id=1, + config_entry=config_entry, ) registry.async_get_or_create( switch.DOMAIN, unifi.DOMAIN, "poe-{}".format(CLIENT_2["mac"]), suggested_object_id=CLIENT_2["hostname"], - config_entry_id=1, + config_entry=config_entry, ) - await setup_controller(hass, mock_controller) + await setup_controller(hass, mock_controller, options) assert len(mock_controller.mock_requests) == 3 assert len(hass.states.async_all()) == 3 diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index 0269f269027..014fb7b6f45 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -21,6 +21,7 @@ MOCK_DEV_VERSION = "10.0.dev0" MOCK_HUUID = "abcdefg" MOCK_RESPONSE = {"version": "0.15", "release-notes": "https://home-assistant.io"} MOCK_CONFIG = {updater.DOMAIN: {"reporting": True}} +RELEASE_NOTES = "test release notes" @pytest.fixture(autouse=True) @@ -44,56 +45,138 @@ def mock_get_uuid(): yield mock +@pytest.fixture +def mock_utcnow(): + """Fixture to mock utcnow.""" + with patch("homeassistant.components.updater.dt_util.utcnow") as mock: + yield mock + + @asyncio.coroutine -def test_new_version_shows_entity_after_hour( - hass, mock_get_uuid, mock_get_newest_version -): - """Test if new entity is created if new version is available.""" +def test_new_version_shows_entity_startup(hass, mock_get_uuid, mock_get_newest_version): + """Test if binary sensor is unavailable at first.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((NEW_VERSION, "")) + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" - with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) - yield from hass.async_block_till_done() - - assert hass.states.is_state(updater.ENTITY_ID, NEW_VERSION) + yield from hass.async_block_till_done() + assert hass.states.is_state("binary_sensor.updater", "unavailable") + assert "newest_version" not in hass.states.get("binary_sensor.updater").attributes + assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes @asyncio.coroutine -def test_same_version_not_show_entity(hass, mock_get_uuid, mock_get_newest_version): - """Test if new entity is created if new version is available.""" +def test_rename_entity(hass, mock_get_uuid, mock_get_newest_version): + """Test if renaming the binary sensor works correctly.""" + mock_get_uuid.return_value = MOCK_HUUID + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) + + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + + res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + assert res, "Updater failed to set up" + + yield from hass.async_block_till_done() + assert hass.states.is_state("binary_sensor.updater", "unavailable") + assert hass.states.get("binary_sensor.new_entity_id") is None + + entity_registry = yield from hass.helpers.entity_registry.async_get_registry() + entity_registry.async_update_entity( + "binary_sensor.updater", new_entity_id="binary_sensor.new_entity_id" + ) + + yield from hass.async_block_till_done() + assert hass.states.is_state("binary_sensor.new_entity_id", "unavailable") + assert hass.states.get("binary_sensor.updater") is None + + with patch("homeassistant.components.updater.current_version", MOCK_VERSION): + async_fire_time_changed(hass, later) + yield from hass.async_block_till_done() + + assert hass.states.is_state("binary_sensor.new_entity_id", "on") + assert hass.states.get("binary_sensor.updater") is None + + +@asyncio.coroutine +def test_new_version_shows_entity_true(hass, mock_get_uuid, mock_get_newest_version): + """Test if sensor is true if new version is available.""" + mock_get_uuid.return_value = MOCK_HUUID + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) + + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + + res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) + assert res, "Updater failed to set up" + + yield from hass.async_block_till_done() + with patch("homeassistant.components.updater.current_version", MOCK_VERSION): + async_fire_time_changed(hass, later) + yield from hass.async_block_till_done() + + assert hass.states.is_state("binary_sensor.updater", "on") + assert ( + hass.states.get("binary_sensor.updater").attributes["newest_version"] + == NEW_VERSION + ) + assert ( + hass.states.get("binary_sensor.updater").attributes["release_notes"] + == RELEASE_NOTES + ) + + +@asyncio.coroutine +def test_same_version_shows_entity_false(hass, mock_get_uuid, mock_get_newest_version): + """Test if sensor is false if no new version is available.""" mock_get_uuid.return_value = MOCK_HUUID mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" + yield from hass.async_block_till_done() with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) + async_fire_time_changed(hass, later) yield from hass.async_block_till_done() - assert hass.states.get(updater.ENTITY_ID) is None + assert hass.states.is_state("binary_sensor.updater", "off") + assert ( + hass.states.get("binary_sensor.updater").attributes["newest_version"] + == MOCK_VERSION + ) + assert "release_notes" not in hass.states.get("binary_sensor.updater").attributes @asyncio.coroutine def test_disable_reporting(hass, mock_get_uuid, mock_get_newest_version): - """Test if new entity is created if new version is available.""" + """Test we do not gather analytics when disable reporting is active.""" mock_get_uuid.return_value = MOCK_HUUID mock_get_newest_version.return_value = mock_coro((MOCK_VERSION, "")) + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + res = yield from async_setup_component( hass, updater.DOMAIN, {updater.DOMAIN: {"reporting": False}} ) assert res, "Updater failed to set up" + yield from hass.async_block_till_done() with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) + async_fire_time_changed(hass, later) yield from hass.async_block_till_done() - assert hass.states.get(updater.ENTITY_ID) is None + assert hass.states.is_state("binary_sensor.updater", "off") res = yield from updater.get_newest_version(hass, MOCK_HUUID, MOCK_CONFIG) call = mock_get_newest_version.mock_calls[0][1] assert call[0] is hass @@ -114,7 +197,7 @@ def test_get_newest_version_no_analytics_when_no_huuid(hass, aioclient_mock): @asyncio.coroutine def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): - """Test we do not gather analytics when no huuid is passed in.""" + """Test we gather analytics when huuid is passed in.""" aioclient_mock.post(updater.UPDATER_URL, json=MOCK_RESPONSE) with patch( @@ -127,7 +210,7 @@ def test_get_newest_version_analytics_when_huuid(hass, aioclient_mock): @asyncio.coroutine def test_error_fetching_new_version_timeout(hass): - """Test we do not gather analytics when no huuid is passed in.""" + """Test we handle timeout error while fetching new version.""" with patch( "homeassistant.helpers.system_info.async_get_system_info", Mock(return_value=mock_coro({"fake": "bla"})), @@ -138,7 +221,7 @@ def test_error_fetching_new_version_timeout(hass): @asyncio.coroutine def test_error_fetching_new_version_bad_json(hass, aioclient_mock): - """Test we do not gather analytics when no huuid is passed in.""" + """Test we handle json error while fetching new version.""" aioclient_mock.post(updater.UPDATER_URL, text="not json") with patch( @@ -151,7 +234,7 @@ def test_error_fetching_new_version_bad_json(hass, aioclient_mock): @asyncio.coroutine def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): - """Test we do not gather analytics when no huuid is passed in.""" + """Test we handle response error while fetching new version.""" aioclient_mock.post( updater.UPDATER_URL, json={ @@ -172,17 +255,29 @@ def test_error_fetching_new_version_invalid_response(hass, aioclient_mock): def test_new_version_shows_entity_after_hour_hassio( hass, mock_get_uuid, mock_get_newest_version ): - """Test if new entity is created if new version is available / hass.io.""" + """Test if binary sensor gets updated if new version is available / hass.io.""" mock_get_uuid.return_value = MOCK_HUUID - mock_get_newest_version.return_value = mock_coro((NEW_VERSION, "")) + mock_get_newest_version.return_value = mock_coro((NEW_VERSION, RELEASE_NOTES)) mock_component(hass, "hassio") hass.data["hassio_hass_version"] = "999.0" + now = dt_util.utcnow() + later = now + timedelta(hours=1) + mock_utcnow.return_value = now + res = yield from async_setup_component(hass, updater.DOMAIN, {updater.DOMAIN: {}}) assert res, "Updater failed to set up" + yield from hass.async_block_till_done() with patch("homeassistant.components.updater.current_version", MOCK_VERSION): - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(hours=1)) + async_fire_time_changed(hass, later) yield from hass.async_block_till_done() - assert hass.states.is_state(updater.ENTITY_ID, "999.0") + assert hass.states.is_state("binary_sensor.updater", "on") + assert ( + hass.states.get("binary_sensor.updater").attributes["newest_version"] == "999.0" + ) + assert ( + hass.states.get("binary_sensor.updater").attributes["release_notes"] + == RELEASE_NOTES + ) diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index a5b1c04cb35..5f17606146b 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -18,7 +18,10 @@ class MockDevice(Device): def __init__(self, udn): """Initializer.""" - super().__init__(MagicMock()) + device = MagicMock() + device.manufacturer = "mock-manuf" + device.name = "mock-name" + super().__init__(device) self._udn = udn self.added_port_mappings = [] self.removed_port_mappings = [] diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 506a45694c0..a39a0a0e7a6 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -389,3 +389,85 @@ async def test_subscribe_unsubscribe_events_state_changed( assert msg["type"] == "event" assert msg["event"]["event_type"] == "state_changed" assert msg["event"]["data"]["entity_id"] == "light.permitted" + + +async def test_render_template_renders_template( + hass, websocket_client, hass_admin_user +): + """Test simple template is rendered and updated.""" + hass.states.async_set("light.test", "on") + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": "State is: {{ states('light.test') }}", + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + hass.states.async_set("light.test", "off") + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: off"} + + +async def test_render_template_with_manual_entity_ids( + hass, websocket_client, hass_admin_user +): + """Test that updates to specified entity ids cause a template rerender.""" + hass.states.async_set("light.test", "on") + hass.states.async_set("light.test2", "on") + + await websocket_client.send_json( + { + "id": 5, + "type": "render_template", + "template": "State is: {{ states('light.test') }}", + "entity_ids": ["light.test2"], + } + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + hass.states.async_set("light.test2", "off") + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == "event" + event = msg["event"] + assert event == {"result": "State is: on"} + + +async def test_render_template_returns_with_match_all( + hass, websocket_client, hass_admin_user +): + """Test that a template that would match with all entities still return success.""" + await websocket_client.send_json( + {"id": 5, "type": "render_template", "template": "State is: {{ 42 }}"} + ) + + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert msg["success"] diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index abde126e675..d34c6983528 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -68,12 +68,13 @@ class FakeEndpoint: def patch_cluster(cluster): """Patch a cluster for testing.""" cluster.bind = CoroutineMock(return_value=[0]) + cluster.configure_reporting = CoroutineMock(return_value=[0]) cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() cluster.handle_cluster_general_request = Mock() + cluster.read_attributes = CoroutineMock() cluster.read_attributes_raw = Mock() - cluster.read_attributes = Mock() - cluster.unbind = Mock() + cluster.unbind = CoroutineMock(return_value=[0]) class FakeDevice: @@ -139,7 +140,10 @@ async def async_init_zigpy_device( device = make_device( in_cluster_ids, out_cluster_ids, device_type, ieee, manufacturer, model ) - await gateway.async_device_initialized(device, is_new_join) + if is_new_join: + await gateway.async_device_initialized(device) + else: + await gateway.async_device_restored(device) await hass.async_block_till_done() return device diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index c159d2b9486..b836c55df17 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -6,9 +6,6 @@ from homeassistant.components.zha.core.const import DOMAIN, DATA_ZHA, COMPONENTS from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.registries import establish_device_mappings -from homeassistant.components.zha.core.channels.registry import ( - populate_channel_registry, -) from .common import async_setup_entry from homeassistant.components.zha.core.store import async_get_registry @@ -17,7 +14,13 @@ from homeassistant.components.zha.core.store import async_get_registry def config_entry_fixture(hass): """Fixture representing a config entry.""" config_entry = config_entries.ConfigEntry( - 1, DOMAIN, "Mock Title", {}, "test", config_entries.CONN_CLASS_LOCAL_PUSH + 1, + DOMAIN, + "Mock Title", + {}, + "test", + config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) return config_entry @@ -29,7 +32,6 @@ async def zha_gateway_fixture(hass, config_entry): Create a ZHAGateway object that can be used to interact with as if we had a real zigbee network running. """ - populate_channel_registry() establish_device_mappings() for component in COMPONENTS: hass.data[DATA_ZHA][component] = hass.data[DATA_ZHA].get(component, {}) diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 0eb5bb4f230..5bf891b132e 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,15 +1,15 @@ """Test ZHA API.""" import pytest from homeassistant.components.switch import DOMAIN -from homeassistant.components.zha.api import async_load_api, ATTR_IEEE, TYPE, ID +from homeassistant.components.zha.api import async_load_api, TYPE, ID from homeassistant.components.zha.core.const import ( ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, - IN, - IEEE, - MODEL, - NAME, - QUIRK_APPLIED, + CLUSTER_TYPE_IN, + ATTR_IEEE, + ATTR_MODEL, + ATTR_NAME, + ATTR_QUIRK_APPLIED, ATTR_MANUFACTURER, ATTR_ENDPOINT_ID, ) @@ -49,14 +49,14 @@ async def test_device_clusters(hass, config_entry, zha_gateway, zha_client): cluster_infos = sorted(msg["result"], key=lambda k: k[ID]) cluster_info = cluster_infos[0] - assert cluster_info[TYPE] == IN + assert cluster_info[TYPE] == CLUSTER_TYPE_IN assert cluster_info[ID] == 0 - assert cluster_info[NAME] == "Basic" + assert cluster_info[ATTR_NAME] == "Basic" cluster_info = cluster_infos[1] - assert cluster_info[TYPE] == IN + assert cluster_info[TYPE] == CLUSTER_TYPE_IN assert cluster_info[ID] == 6 - assert cluster_info[NAME] == "OnOff" + assert cluster_info[ATTR_NAME] == "OnOff" async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_client): @@ -68,7 +68,7 @@ async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_cl ATTR_ENDPOINT_ID: 1, ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7", ATTR_CLUSTER_ID: 6, - ATTR_CLUSTER_TYPE: IN, + ATTR_CLUSTER_TYPE: CLUSTER_TYPE_IN, } ) @@ -79,7 +79,7 @@ async def test_device_cluster_attributes(hass, config_entry, zha_gateway, zha_cl for attribute in attributes: assert attribute[ID] is not None - assert attribute[NAME] is not None + assert attribute[ATTR_NAME] is not None async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_client): @@ -91,7 +91,7 @@ async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_clie ATTR_ENDPOINT_ID: 1, ATTR_IEEE: "00:0d:6f:00:0a:90:69:e7", ATTR_CLUSTER_ID: 6, - ATTR_CLUSTER_TYPE: IN, + ATTR_CLUSTER_TYPE: CLUSTER_TYPE_IN, } ) @@ -102,7 +102,7 @@ async def test_device_cluster_commands(hass, config_entry, zha_gateway, zha_clie for command in commands: assert command[ID] is not None - assert command[NAME] is not None + assert command[ATTR_NAME] is not None assert command[TYPE] is not None @@ -116,13 +116,13 @@ async def test_list_devices(hass, config_entry, zha_gateway, zha_client): assert len(devices) == 1 for device in devices: - assert device[IEEE] is not None + assert device[ATTR_IEEE] is not None assert device[ATTR_MANUFACTURER] is not None - assert device[MODEL] is not None - assert device[NAME] is not None - assert device[QUIRK_APPLIED] is not None + assert device[ATTR_MODEL] is not None + assert device[ATTR_NAME] is not None + assert device[ATTR_QUIRK_APPLIED] is not None assert device["entities"] is not None for entity_reference in device["entities"]: - assert entity_reference[NAME] is not None + assert entity_reference[ATTR_NAME] is not None assert entity_reference["entity_id"] is not None diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py new file mode 100644 index 00000000000..3be3aaf0930 --- /dev/null +++ b/tests/components/zha/test_channels.py @@ -0,0 +1,155 @@ +"""Test ZHA Core channels.""" +import pytest +import zigpy.types as t + +import homeassistant.components.zha.core.channels as channels +import homeassistant.components.zha.core.device as zha_device +import homeassistant.components.zha.core.registries as registries + +from .common import make_device + + +@pytest.fixture +def ieee(): + """IEEE fixture.""" + return t.EUI64.deserialize(b"ieeeaddr")[0] + + +@pytest.fixture +def nwk(): + """NWK fixture.""" + return t.NWK(0xBEEF) + + +@pytest.mark.parametrize( + "cluster_id, bind_count, attrs", + [ + (0x0000, 1, {}), + (0x0001, 1, {"battery_voltage", "battery_percentage_remaining"}), + (0x0003, 1, {}), + (0x0004, 1, {}), + (0x0005, 1, {}), + (0x0006, 1, {"on_off"}), + (0x0007, 1, {}), + (0x0008, 1, {"current_level"}), + (0x0009, 1, {}), + (0x000C, 1, {"present_value"}), + (0x000D, 1, {"present_value"}), + (0x000E, 1, {"present_value"}), + (0x000D, 1, {"present_value"}), + (0x0010, 1, {"present_value"}), + (0x0011, 1, {"present_value"}), + (0x0012, 1, {"present_value"}), + (0x0013, 1, {"present_value"}), + (0x0014, 1, {"present_value"}), + (0x0015, 1, {}), + (0x0016, 1, {}), + (0x0019, 1, {}), + (0x001A, 1, {}), + (0x001B, 1, {}), + (0x0020, 1, {}), + (0x0021, 1, {}), + (0x0101, 1, {"lock_state"}), + (0x0202, 1, {"fan_mode"}), + (0x0300, 1, {"current_x", "current_y", "color_temperature"}), + (0x0400, 1, {"measured_value"}), + (0x0401, 1, {"level_status"}), + (0x0402, 1, {"measured_value"}), + (0x0403, 1, {"measured_value"}), + (0x0404, 1, {"measured_value"}), + (0x0405, 1, {"measured_value"}), + (0x0406, 1, {"occupancy"}), + (0x0702, 1, {"instantaneous_demand"}), + (0x0B04, 1, {"active_power"}), + (0x1000, 1, {}), + ], +) +async def test_in_channel_config(cluster_id, bind_count, attrs, zha_gateway, hass): + """Test ZHA core channel configuration for input clusters.""" + zigpy_dev = make_device( + [cluster_id], + [], + 0x1234, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + zha_dev = zha_device.ZHADevice(hass, zigpy_dev, zha_gateway) + + cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id] + channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get( + cluster_id, channels.AttributeListeningChannel + ) + channel = channel_class(cluster, zha_dev) + + await channel.async_configure() + + assert cluster.bind.call_count == bind_count + assert cluster.configure_reporting.call_count == len(attrs) + reported_attrs = {attr[0][0] for attr in cluster.configure_reporting.call_args_list} + assert set(attrs) == reported_attrs + + +@pytest.mark.parametrize( + "cluster_id, bind_count", + [ + (0x0000, 1), + (0x0001, 1), + (0x0003, 1), + (0x0004, 1), + (0x0005, 1), + (0x0006, 1), + (0x0007, 1), + (0x0008, 1), + (0x0009, 1), + (0x0015, 1), + (0x0016, 1), + (0x0019, 1), + (0x001A, 1), + (0x001B, 1), + (0x0020, 1), + (0x0021, 1), + (0x0101, 1), + (0x0202, 1), + (0x0300, 1), + (0x0400, 1), + (0x0402, 1), + (0x0403, 1), + (0x0405, 1), + (0x0406, 1), + (0x0702, 1), + (0x0B04, 1), + (0x1000, 1), + ], +) +async def test_out_channel_config(cluster_id, bind_count, zha_gateway, hass): + """Test ZHA core channel configuration for output clusters.""" + zigpy_dev = make_device( + [], + [cluster_id], + 0x1234, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + zha_dev = zha_device.ZHADevice(hass, zigpy_dev, zha_gateway) + + cluster = zigpy_dev.endpoints[1].out_clusters[cluster_id] + cluster.bind_only = True + channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get( + cluster_id, channels.AttributeListeningChannel + ) + channel = channel_class(cluster, zha_dev) + + await channel.async_configure() + + assert cluster.bind.call_count == bind_count + assert cluster.configure_reporting.call_count == 0 + + +def test_channel_registry(): + """Test ZIGBEE Channel Registry.""" + for (cluster_id, channel) in registries.ZIGBEE_CHANNEL_REGISTRY.items(): + assert isinstance(cluster_id, int) + assert 0 <= cluster_id <= 0xFFFF + assert issubclass(channel, channels.ZigbeeChannel) diff --git a/tests/components/zwave/test_climate.py b/tests/components/zwave/test_climate.py index 60a9dcd0dab..2f13d95fb9f 100644 --- a/tests/components/zwave/test_climate.py +++ b/tests/components/zwave/test_climate.py @@ -43,7 +43,7 @@ def device(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -70,7 +70,7 @@ def device_zxt_120(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data=CURRENT_HVAC_HEAT, node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), zxt_120_swing_mode=MockValue(data="test3", data_items=[6, 7, 8], node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -92,7 +92,7 @@ def device_mapping(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="heating", node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -113,7 +113,7 @@ def device_unknown(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -140,7 +140,7 @@ def device_heat_cool(hass, mock_openzwave): ), fan_mode=MockValue(data="test2", data_items=[3, 4, 5], node=node), operating_state=MockValue(data="test4", node=node), - fan_state=MockValue(data=7, node=node), + fan_action=MockValue(data=7, node=node), ) device = climate.get_device(hass, node=node, values=values, node_config={}) @@ -442,3 +442,11 @@ def test_hvac_action_value_changed_unknown(device_unknown): device.values.operating_state.data = "another_hvac_action" value_changed(device.values.operating_state) assert device.hvac_action == "another_hvac_action" + + +def test_fan_action_value_changed(device): + """Test values changed for climate device.""" + assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 7 + device.values.fan_action.data = 9 + value_changed(device.values.fan_action) + assert device.device_state_attributes[climate.ATTR_FAN_ACTION] == 9 diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index 9c6ab7835cd..4a32c3fb07c 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -287,6 +287,7 @@ async def setup_ozw(hass, mock_openzwave): {"usb_path": "mock-path", "network_key": "mock-key"}, "test", config_entries.CONN_CLASS_LOCAL_PUSH, + system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "lock") await hass.async_block_till_done() diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py new file mode 100644 index 00000000000..b55024eb3f0 --- /dev/null +++ b/tests/components/zwave/test_websocket_api.py @@ -0,0 +1,38 @@ +"""Test Z-Wave Websocket API.""" +from homeassistant.bootstrap import async_setup_component + +from homeassistant.components.zwave.const import ( + CONF_USB_STICK_PATH, + CONF_AUTOHEAL, + CONF_POLLING_INTERVAL, +) +from homeassistant.components.zwave.websocket_api import ID, TYPE + + +async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): + """Test Z-Wave websocket API.""" + + await async_setup_component( + hass, + "zwave", + { + "zwave": { + CONF_AUTOHEAL: False, + CONF_USB_STICK_PATH: "/dev/zwave", + CONF_POLLING_INTERVAL: 6000, + } + }, + ) + + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + + await client.send_json({ID: 5, TYPE: "zwave/get_config"}) + + msg = await client.receive_json() + result = msg["result"] + + assert result[CONF_USB_STICK_PATH] == "/dev/zwave" + assert not result[CONF_AUTOHEAL] + assert result[CONF_POLLING_INTERVAL] == 6000 diff --git a/tests/fixtures/ring_devices.json b/tests/fixtures/ring_devices.json index 3e9171789d9..557aef3535c 100644 --- a/tests/fixtures/ring_devices.json +++ b/tests/fixtures/ring_devices.json @@ -214,5 +214,142 @@ "subscribed": true, "subscribed_motions": true, "time_zone": "America/New_York" - }] + }, + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 80, + "description": "Internal", + "device_id": "aacdef124", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "night_vision_enabled": false, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.9.3", + "id": 987652, + "kind": "hp_cam_v1", + "latitude": 12.000000, + "led_status": "on", + "location_id": null, + "longitude": -70.12345, + "motion_snooze": {"scheduled": true}, + "night_mode_status": "false", + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"}, + "ring_cam_light_installed": "false", + "ring_id": null, + "settings": { + "chime_settings": { + "duration": 10, + "enable": true, + "type": 0}, + "doorbell_volume": 11, + "enable_vod": true, + "floodlight_settings": { + "duration": 30, + "priority": 0}, + "light_schedule_settings": { + "end_hour": 0, + "end_minute": 0, + "start_hour": 0, + "start_minute": 0}, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "none", + "low", + "medium", + "high"], + "motion_zones": { + "active_motion_filter": 1, + "advanced_object_settings": { + "human_detection_confidence": { + "day": 0.7, + "night": 0.7}, + "motion_zone_overlap": { + "day": 0.1, + "night": 0.2}, + "object_size_maximum": { + "day": 0.8, + "night": 0.8}, + "object_size_minimum": { + "day": 0.03, + "night": 0.05}, + "object_time_overlap": { + "day": 0.1, + "night": 0.6} + }, + "enable_audio": false, + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "sensitivity": 5, + "zone1": { + "name": "Zone 1", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone2": { + "name": "Zone 2", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone3": { + "name": "Zone 3", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}}, + "pir_motion_zones": [0, 1, 1], + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "stream_setting": 0, + "video_settings": { + "ae_level": 0, + "birton": null, + "brightness": 0, + "contrast": 64, + "saturation": 80}}, + "siren_status": {"seconds_remaining": 30}, + "stolen": false, + "subscribed": true, + "subscribed_motions": true, + "time_zone": "America/New_York"}] } diff --git a/tests/fixtures/ring_devices_updated.json b/tests/fixtures/ring_devices_updated.json new file mode 100644 index 00000000000..fa3c0586101 --- /dev/null +++ b/tests/fixtures/ring_devices_updated.json @@ -0,0 +1,355 @@ +{ + "authorized_doorbots": [], + "chimes": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "description": "Downstairs", + "device_id": "abcdef123", + "do_not_disturb": {"seconds_left": 0}, + "features": {"ringtones_enabled": true}, + "firmware_version": "1.2.3", + "id": 999999, + "kind": "chime", + "latitude": 12.000000, + "longitude": -70.12345, + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Marcelo", + "id": 999999, + "last_name": "Assistant"}, + "settings": { + "ding_audio_id": null, + "ding_audio_user_id": null, + "motion_audio_id": null, + "motion_audio_user_id": null, + "volume": 2}, + "time_zone": "America/New_York"}], + "doorbots": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 4081, + "description": "Front Door", + "device_id": "aacdef123", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.4.26", + "id": 987652, + "kind": "lpd_v1", + "latitude": 12.000000, + "longitude": -70.12345, + "motion_snooze": null, + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Home", + "id": 999999, + "last_name": "Assistant"}, + "settings": { + "chime_settings": { + "duration": 3, + "enable": true, + "type": 0}, + "doorbell_volume": 1, + "enable_vod": true, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "null", + "low", + "medium", + "high"]}, + "subscribed": true, + "subscribed_motions": true, + "time_zone": "America/New_York"}], + "stickup_cams": [ + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 80, + "description": "Front", + "device_id": "aacdef123", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "night_vision_enabled": false, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.9.3", + "id": 987652, + "kind": "hp_cam_v1", + "latitude": 12.000000, + "led_status": "on", + "location_id": null, + "longitude": -70.12345, + "motion_snooze": {"scheduled": true}, + "night_mode_status": "false", + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"}, + "ring_cam_light_installed": "false", + "ring_id": null, + "settings": { + "chime_settings": { + "duration": 10, + "enable": true, + "type": 0}, + "doorbell_volume": 11, + "enable_vod": true, + "floodlight_settings": { + "duration": 30, + "priority": 0}, + "light_schedule_settings": { + "end_hour": 0, + "end_minute": 0, + "start_hour": 0, + "start_minute": 0}, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "none", + "low", + "medium", + "high"], + "motion_zones": { + "active_motion_filter": 1, + "advanced_object_settings": { + "human_detection_confidence": { + "day": 0.7, + "night": 0.7}, + "motion_zone_overlap": { + "day": 0.1, + "night": 0.2}, + "object_size_maximum": { + "day": 0.8, + "night": 0.8}, + "object_size_minimum": { + "day": 0.03, + "night": 0.05}, + "object_time_overlap": { + "day": 0.1, + "night": 0.6} + }, + "enable_audio": false, + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "sensitivity": 5, + "zone1": { + "name": "Zone 1", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone2": { + "name": "Zone 2", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone3": { + "name": "Zone 3", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}}, + "pir_motion_zones": [0, 1, 1], + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "stream_setting": 0, + "video_settings": { + "ae_level": 0, + "birton": null, + "brightness": 0, + "contrast": 64, + "saturation": 80}}, + "siren_status": {"seconds_remaining": 30}, + "stolen": false, + "subscribed": true, + "subscribed_motions": true, + "time_zone": "America/New_York" + }, + { + "address": "123 Main St", + "alerts": {"connection": "online"}, + "battery_life": 80, + "description": "Internal", + "device_id": "aacdef124", + "external_connection": false, + "features": { + "advanced_motion_enabled": false, + "motion_message_enabled": false, + "motions_enabled": true, + "night_vision_enabled": false, + "people_only_enabled": false, + "shadow_correction_enabled": false, + "show_recordings": true}, + "firmware_version": "1.9.3", + "id": 987652, + "kind": "hp_cam_v1", + "latitude": 12.000000, + "led_status": "off", + "location_id": null, + "longitude": -70.12345, + "motion_snooze": {"scheduled": true}, + "night_mode_status": "false", + "owned": true, + "owner": { + "email": "foo@bar.org", + "first_name": "Foo", + "id": 999999, + "last_name": "Bar"}, + "ring_cam_light_installed": "false", + "ring_id": null, + "settings": { + "chime_settings": { + "duration": 10, + "enable": true, + "type": 0}, + "doorbell_volume": 11, + "enable_vod": true, + "floodlight_settings": { + "duration": 30, + "priority": 0}, + "light_schedule_settings": { + "end_hour": 0, + "end_minute": 0, + "start_hour": 0, + "start_minute": 0}, + "live_view_preset_profile": "highest", + "live_view_presets": [ + "low", + "middle", + "high", + "highest"], + "motion_announcement": false, + "motion_snooze_preset_profile": "low", + "motion_snooze_presets": [ + "none", + "low", + "medium", + "high"], + "motion_zones": { + "active_motion_filter": 1, + "advanced_object_settings": { + "human_detection_confidence": { + "day": 0.7, + "night": 0.7}, + "motion_zone_overlap": { + "day": 0.1, + "night": 0.2}, + "object_size_maximum": { + "day": 0.8, + "night": 0.8}, + "object_size_minimum": { + "day": 0.03, + "night": 0.05}, + "object_time_overlap": { + "day": 0.1, + "night": 0.6} + }, + "enable_audio": false, + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "sensitivity": 5, + "zone1": { + "name": "Zone 1", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone2": { + "name": "Zone 2", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}, + "zone3": { + "name": "Zone 3", + "state": 2, + "vertex1": {"x": 0.0, "y": 0.0}, + "vertex2": {"x": 0.0, "y": 0.0}, + "vertex3": {"x": 0.0, "y": 0.0}, + "vertex4": {"x": 0.0, "y": 0.0}, + "vertex5": {"x": 0.0, "y": 0.0}, + "vertex6": {"x": 0.0, "y": 0.0}, + "vertex7": {"x": 0.0, "y": 0.0}, + "vertex8": {"x": 0.0, "y": 0.0}}}, + "pir_motion_zones": [0, 1, 1], + "pir_settings": { + "sensitivity1": 1, + "sensitivity2": 1, + "sensitivity3": 1, + "zone_mask": 6}, + "stream_setting": 0, + "video_settings": { + "ae_level": 0, + "birton": null, + "brightness": 0, + "contrast": 64, + "saturation": 80}}, + "siren_status": {"seconds_remaining": 30}, + "stolen": false, + "subscribed": true, + "subscribed_motions": true, + "time_zone": "America/New_York"}] +} diff --git a/tests/fixtures/ring_doorbot_siren_on_response.json b/tests/fixtures/ring_doorbot_siren_on_response.json new file mode 100644 index 00000000000..6bf91f88299 --- /dev/null +++ b/tests/fixtures/ring_doorbot_siren_on_response.json @@ -0,0 +1 @@ +{"started_at":"2019-07-28T16:58:27.593+00:00","duration":30,"ends_at":"2019-07-28T16:58:57.593+00:00","seconds_remaining":30} \ No newline at end of file diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 2a60b9ee2a4..9e5ea15293a 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -75,7 +75,7 @@ async def test_component_platform_not_found(hass, loop): assert res.keys() == {"homeassistant"} assert res.errors[0] == CheckConfigError( - "Integration not found: beer", None, None + "Component error: beer - Integration beer not found.", None, None ) # Only 1 error expected @@ -95,9 +95,7 @@ async def test_component_platform_not_found_2(hass, loop): assert res["light"] == [] assert res.errors[0] == CheckConfigError( - "Integration beer not found when trying to verify its " "light platform.", - None, - None, + "Platform error light.beer - Integration beer not found.", None, None ) # Only 1 error expected diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 7bce0d69c51..3c3d1224e12 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -80,6 +80,7 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): """Test we not allow duplicates.""" flow = config_entries.HANDLERS["test"]() flow.hass = hass + flow.context = {} MockConfigEntry(domain="test").add_to_hass(hass) result = await getattr(flow, "async_step_{}".format(source))({}) @@ -93,6 +94,7 @@ async def test_discovery_confirmation(hass, discovery_flow_conf, source): """Test we ask for confirmation via discovery.""" flow = config_entries.HANDLERS["test"]() flow.hass = hass + flow.context = {} result = await getattr(flow, "async_step_{}".format(source))({}) diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 58f76d396c1..3c89a5c6537 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -7,13 +7,13 @@ from unittest.mock import MagicMock, patch, PropertyMock import pytest -import homeassistant.helpers.entity as entity +from homeassistant.helpers import entity, entity_registry from homeassistant.core import Context from homeassistant.const import ATTR_HIDDEN, ATTR_DEVICE_CLASS from homeassistant.config import DATA_CUSTOMIZE from homeassistant.helpers.entity_values import EntityValues -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_registry def test_generate_entity_id_requires_hass_or_ids(): @@ -499,3 +499,61 @@ async def test_set_context_expired(hass): assert hass.states.get("hello.world").context != context assert ent._context is None assert ent._context_set is None + + +async def test_warn_disabled(hass, caplog): + """Test we warn once if we write to a disabled entity.""" + entry = entity_registry.RegistryEntry( + entity_id="hello.world", + unique_id="test-unique-id", + platform="test-platform", + disabled_by="user", + ) + mock_registry(hass, {"hello.world": entry}) + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.registry_entry = entry + ent.platform = MagicMock(platform_name="test-platform") + + caplog.clear() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + assert "Entity hello.world is incorrectly being triggered" in caplog.text + + caplog.clear() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + assert caplog.text == "" + + +async def test_disabled_in_entity_registry(hass): + """Test entity is removed if we disable entity registry entry.""" + entry = entity_registry.RegistryEntry( + entity_id="hello.world", + unique_id="test-unique-id", + platform="test-platform", + disabled_by="user", + ) + registry = mock_registry(hass, {"hello.world": entry}) + + ent = entity.Entity() + ent.hass = hass + ent.entity_id = "hello.world" + ent.registry_entry = entry + ent.platform = MagicMock(platform_name="test-platform") + + await ent.async_internal_added_to_hass() + ent.async_write_ha_state() + assert hass.states.get("hello.world") is None + + entry2 = registry.async_update_entity("hello.world", disabled_by=None) + await hass.async_block_till_done() + assert entry2 != entry + assert ent.registry_entry == entry2 + + entry3 = registry.async_update_entity("hello.world", disabled_by="user") + await hass.async_block_till_done() + assert entry3 != entry2 + assert ent.registry_entry == entry3 diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 0f43c6ab4aa..caf8bb702af 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -491,7 +491,7 @@ async def test_registry_respect_entity_disabled(hass): platform = MockEntityPlatform(hass) entity = MockEntity(unique_id="1234") await platform.async_add_entities([entity]) - assert entity.entity_id is None + assert entity.entity_id == "test_domain.world" assert hass.states.async_entity_ids() == [] @@ -775,3 +775,22 @@ async def test_device_info_not_overrides(hass): assert device.id == device2.id assert device2.manufacturer == "test-manufacturer" assert device2.model == "test-model" + + +async def test_entity_disabled_by_integration(hass): + """Test entity disabled by integration.""" + component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) + + entity_default = MockEntity(unique_id="default") + entity_disabled = MockEntity( + unique_id="disabled", entity_registry_enabled_default=False + ) + + await component.async_add_entities([entity_default, entity_disabled]) + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") + assert entry_default.disabled_by is None + entry_disabled = registry.async_get_or_create(DOMAIN, DOMAIN, "disabled") + assert entry_disabled.disabled_by == "integration" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index a3ffcb4d1ff..9debbdbcba7 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -8,7 +8,7 @@ import pytest from homeassistant.core import valid_entity_id, callback from homeassistant.helpers import entity_registry -from tests.common import mock_registry, flush_store +from tests.common import MockConfigEntry, mock_registry, flush_store YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" @@ -88,9 +88,11 @@ def test_create_triggers_save(hass, registry): async def test_loading_saving_data(hass, registry): """Test that we load/save data correctly.""" + mock_config = MockConfigEntry(domain="light") + orig_entry1 = registry.async_get_or_create("light", "hue", "1234") orig_entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id" + "light", "hue", "5678", config_entry=mock_config ) assert len(registry.entities) == 2 @@ -104,7 +106,7 @@ async def test_loading_saving_data(hass, registry): assert list(registry.entities) == list(registry2.entities) new_entry1 = registry.async_get_or_create("light", "hue", "1234") new_entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id" + "light", "hue", "5678", config_entry=mock_config ) assert orig_entry1 == new_entry1 @@ -198,11 +200,14 @@ def test_async_get_entity_id(registry): async def test_updating_config_entry_id(hass, registry, update_events): """Test that we update config entry id in registry.""" + mock_config_1 = MockConfigEntry(domain="light", entry_id="mock-id-1") entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config_1 ) + + mock_config_2 = MockConfigEntry(domain="light", entry_id="mock-id-2") entry2 = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-2" + "light", "hue", "5678", config_entry=mock_config_2 ) assert entry.entity_id == entry2.entity_id assert entry2.config_entry_id == "mock-id-2" @@ -214,12 +219,15 @@ async def test_updating_config_entry_id(hass, registry, update_events): assert update_events[0]["entity_id"] == entry.entity_id assert update_events[1]["action"] == "update" assert update_events[1]["entity_id"] == entry.entity_id + assert update_events[1]["changes"] == ["config_entry_id"] async def test_removing_config_entry_id(hass, registry, update_events): """Test that we update config entry id in registry.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config ) assert entry.config_entry_id == "mock-id-1" registry.async_clear_config_entry("mock-id-1") @@ -237,6 +245,8 @@ async def test_removing_config_entry_id(hass, registry, update_events): async def test_migration(hass): """Test migration from old data to new.""" + mock_config = MockConfigEntry(domain="test-platform", entry_id="test-config-id") + old_conf = { "light.kitchen": { "config_entry_id": "test-config-id", @@ -256,7 +266,7 @@ async def test_migration(hass): domain="light", platform="test-platform", unique_id="test-unique", - config_entry_id="test-config-id", + config_entry=mock_config, ) assert entry.name == "Test Name" assert entry.disabled_by == "hass" @@ -326,8 +336,10 @@ async def test_loading_race_condition(hass): async def test_update_entity_unique_id(registry): """Test entity's unique_id is updated.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config ) new_unique_id = "1234" with patch.object(registry, "async_schedule_save") as mock_schedule_save: @@ -341,14 +353,68 @@ async def test_update_entity_unique_id(registry): async def test_update_entity_unique_id_conflict(registry): """Test migration raises when unique_id already in use.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") entry = registry.async_get_or_create( - "light", "hue", "5678", config_entry_id="mock-id-1" + "light", "hue", "5678", config_entry=mock_config ) entry2 = registry.async_get_or_create( - "light", "hue", "1234", config_entry_id="mock-id-1" + "light", "hue", "1234", config_entry=mock_config ) with patch.object( registry, "async_schedule_save" ) as mock_schedule_save, pytest.raises(ValueError): registry.async_update_entity(entry.entity_id, new_unique_id=entry2.unique_id) assert mock_schedule_save.call_count == 0 + + +async def test_update_entity(registry): + """Test updating entity.""" + mock_config = MockConfigEntry(domain="light", entry_id="mock-id-1") + entry = registry.async_get_or_create( + "light", "hue", "5678", config_entry=mock_config + ) + + for attr_name, new_value in ( + ("name", "new name"), + ("disabled_by", entity_registry.DISABLED_USER), + ): + changes = {attr_name: new_value} + updated_entry = registry.async_update_entity(entry.entity_id, **changes) + + assert updated_entry != entry + assert getattr(updated_entry, attr_name) == new_value + assert getattr(updated_entry, attr_name) != getattr(entry, attr_name) + + entry = updated_entry + + +async def test_disabled_by(registry): + """Test that we can disable an entry when we create it.""" + entry = registry.async_get_or_create("light", "hue", "5678", disabled_by="hass") + assert entry.disabled_by == "hass" + + entry = registry.async_get_or_create( + "light", "hue", "5678", disabled_by="integration" + ) + assert entry.disabled_by == "hass" + + entry2 = registry.async_get_or_create("light", "hue", "1234") + assert entry2.disabled_by is None + + +async def test_disabled_by_system_options(registry): + """Test system options setting disabled_by.""" + mock_config = MockConfigEntry( + domain="light", + entry_id="mock-id-1", + system_options={"disable_new_entities": True}, + ) + entry = registry.async_get_or_create( + "light", "hue", "AAAA", config_entry=mock_config + ) + assert entry.disabled_by == "integration" + + entry2 = registry.async_get_or_create( + "light", "hue", "BBBB", config_entry=mock_config, disabled_by="user" + ) + assert entry2.disabled_by == "user" diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index a7e4df068fa..7f428c0833d 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -53,13 +53,13 @@ def test_async_track_states(hass): def test_call_to_component(hass): """Test calls to components state reproduction functions.""" with patch( - ("homeassistant.components.media_player." "async_reproduce_states") + ("homeassistant.components.media_player.reproduce_state.async_reproduce_states") ) as media_player_fun: media_player_fun.return_value = asyncio.Future() media_player_fun.return_value.set_result(None) with patch( - ("homeassistant.components.climate." "async_reproduce_states") + ("homeassistant.components.climate.reproduce_state.async_reproduce_states") ) as climate_fun: climate_fun.return_value = asyncio.Future() climate_fun.return_value.set_result(None) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index d1beb4eb47b..cc1f7707df6 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -349,6 +349,101 @@ def test_sqrt(hass): ) +def test_arc_sine(hass): + """Test arcus sine.""" + tests = [ + (-2.0, "-2.0"), # value error + (-1.0, "-1.571"), + (-0.5, "-0.524"), + (0.0, "0.0"), + (0.5, "0.524"), + (1.0, "1.571"), + (2.0, "2.0"), # value error + ('"error"', "error"), + ] + + for value, expected in tests: + assert ( + template.Template("{{ %s | asin | round(3) }}" % value, hass).async_render() + == expected + ) + + +def test_arc_cos(hass): + """Test arcus cosine.""" + tests = [ + (-2.0, "-2.0"), # value error + (-1.0, "3.142"), + (-0.5, "2.094"), + (0.0, "1.571"), + (0.5, "1.047"), + (1.0, "0.0"), + (2.0, "2.0"), # value error + ('"error"', "error"), + ] + + for value, expected in tests: + assert ( + template.Template("{{ %s | acos | round(3) }}" % value, hass).async_render() + == expected + ) + + +def test_arc_tan(hass): + """Test arcus tangent.""" + tests = [ + (-10.0, "-1.471"), + (-2.0, "-1.107"), + (-1.0, "-0.785"), + (-0.5, "-0.464"), + (0.0, "0.0"), + (0.5, "0.464"), + (1.0, "0.785"), + (2.0, "1.107"), + (10.0, "1.471"), + ('"error"', "error"), + ] + + for value, expected in tests: + assert ( + template.Template("{{ %s | atan | round(3) }}" % value, hass).async_render() + == expected + ) + + +def test_arc_tan2(hass): + """Test two parameter version of arcus tangent.""" + tests = [ + (-10.0, -10.0, "-2.356"), + (-10.0, 0.0, "-1.571"), + (-10.0, 10.0, "-0.785"), + (0.0, -10.0, "3.142"), + (0.0, 0.0, "0.0"), + (0.0, 10.0, "0.0"), + (10.0, -10.0, "2.356"), + (10.0, 0.0, "1.571"), + (10.0, 10.0, "0.785"), + (-4.0, 3.0, "-0.927"), + (-1.0, 2.0, "-0.464"), + (2.0, 1.0, "1.107"), + ('"duck"', '"goose"', "('duck', 'goose')"), + ] + + for y, x, expected in tests: + assert ( + template.Template( + "{{ (%s, %s) | atan2 | round(3) }}" % (y, x), hass + ).async_render() + == expected + ) + assert ( + template.Template( + "{{ atan2(%s, %s) | round(3) }}" % (y, x), hass + ).async_render() + == expected + ) + + def test_strptime(hass): """Test the parse timestamp method.""" tests = [ diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index a07b812bc96..bd4f37bd135 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -62,7 +62,9 @@ def test_component_platform_not_found(isfile_patch, loop): res = check_config.check(get_test_config_dir()) assert res["components"].keys() == {"homeassistant"} assert res["except"] == { - check_config.ERROR_STR: ["Integration not found: beer"] + check_config.ERROR_STR: [ + "Component error: beer - Integration beer not found." + ] } assert res["secret_cache"] == {} assert res["secrets"] == {} @@ -75,8 +77,7 @@ def test_component_platform_not_found(isfile_patch, loop): assert res["components"]["light"] == [] assert res["except"] == { check_config.ERROR_STR: [ - "Integration beer not found when trying to verify its " - "light platform." + "Platform error light.beer - Integration beer not found." ] } assert res["secret_cache"] == {} diff --git a/tests/test_config.py b/tests/test_config.py index 6d85712bc25..a67cd345797 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -735,7 +735,7 @@ async def test_merge_once_only_lists(hass): """Test if we have a merge for a comp that may occur only once. Lists.""" packages = { "pack_2": { - "api": {"list_1": ["item_2", "item_3"], "list_2": ["item_1"], "list_3": []} + "api": {"list_1": ["item_2", "item_3"], "list_2": ["item_4"], "list_3": []} } } config = { @@ -745,7 +745,8 @@ async def test_merge_once_only_lists(hass): await config_util.merge_packages_config(hass, config, packages) assert config["api"] == { "list_1": ["item_1", "item_2", "item_3"], - "list_2": ["item_1"], + "list_2": ["item_4"], + "list_3": [], } diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b529ae5b471..d9dd614c9a5 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -20,6 +20,7 @@ from tests.common import ( MockEntity, mock_integration, mock_entity_platform, + mock_registry, ) @@ -521,31 +522,32 @@ async def test_discovery_notification(hass): mock_entity_platform(hass, "config_flow.test", None) await async_setup_component(hass, "persistent_notification", {}) - class TestFlow(config_entries.ConfigFlow): - VERSION = 5 + with patch.dict(config_entries.HANDLERS): - async def async_step_discovery(self, user_input=None): - if user_input is not None: - return self.async_create_entry( - title="Test Title", data={"token": "abcd"} - ) - return self.async_show_form(step_id="discovery") + class TestFlow(config_entries.ConfigFlow, domain="test"): + VERSION = 5 + + async def async_step_discovery(self, user_input=None): + if user_input is not None: + return self.async_create_entry( + title="Test Title", data={"token": "abcd"} + ) + return self.async_show_form(step_id="discovery") - with patch.dict(config_entries.HANDLERS, {"test": TestFlow}): result = await hass.config_entries.flow.async_init( "test", context={"source": config_entries.SOURCE_DISCOVERY} ) - await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is not None + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_discovery") + assert state is not None - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - await hass.async_block_till_done() - state = hass.states.get("persistent_notification.config_entry_discovery") - assert state is None + await hass.async_block_till_done() + state = hass.states.get("persistent_notification.config_entry_discovery") + assert state is None async def test_discovery_notification_not_created(hass): @@ -596,6 +598,22 @@ async def test_updating_entry_data(manager): assert entry.data == {"second": True} +async def test_updating_entry_system_options(manager): + """Test that we can update an entry data.""" + entry = MockConfigEntry( + domain="test", + data={"first": True}, + state=config_entries.ENTRY_STATE_SETUP_ERROR, + system_options={"disable_new_entities": True}, + ) + entry.add_to_manager(manager) + + assert entry.system_options.disable_new_entities + + entry.system_options.update(disable_new_entities=False) + assert not entry.system_options.disable_new_entities + + async def test_update_entry_options_and_trigger_listener(hass, manager): """Test that we can update entry options and trigger listener.""" entry = MockConfigEntry(domain="test", options={"first": True}) @@ -666,12 +684,11 @@ async def test_entry_options(hass, manager): class TestFlow: @staticmethod @callback - def async_get_options_flow(config, options): + def async_get_options_flow(config_entry): class OptionsFlowHandler(data_entry_flow.FlowHandler): - def __init__(self, config, options): - pass + pass - return OptionsFlowHandler(config, options) + return OptionsFlowHandler() config_entries.HANDLERS["test"] = TestFlow() flow = await manager.options._async_create_flow( @@ -909,3 +926,78 @@ async def test_init_custom_integration(hass): return_value=mock_coro(integration), ): await hass.config_entries.flow.async_init("bla") + + +async def test_support_entry_unload(hass): + """Test unloading entry.""" + assert await config_entries.support_entry_unload(hass, "light") + assert not await config_entries.support_entry_unload(hass, "auth") + + +async def test_reload_entry_entity_registry_ignores_no_entry(hass): + """Test reloading entry in entity registry skips if no config entry linked.""" + handler = config_entries.EntityRegistryDisabledHandler(hass) + registry = mock_registry(hass) + + # Test we ignore entities without config entry + entry = registry.async_get_or_create("light", "hue", "123") + registry.async_update_entity(entry.entity_id, disabled_by="user") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + +async def test_reload_entry_entity_registry_works(hass): + """Test we schedule an entry to be reloaded if disabled_by is updated.""" + handler = config_entries.EntityRegistryDisabledHandler(hass) + handler.async_setup() + registry = mock_registry(hass) + + config_entry = MockConfigEntry( + domain="comp", state=config_entries.ENTRY_STATE_LOADED + ) + config_entry.add_to_hass(hass) + mock_setup_entry = MagicMock(return_value=mock_coro(True)) + mock_unload_entry = MagicMock(return_value=mock_coro(True)) + mock_integration( + hass, + MockModule( + "comp", + async_setup_entry=mock_setup_entry, + async_unload_entry=mock_unload_entry, + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + + # Only changing disabled_by should update trigger + entity_entry = registry.async_get_or_create( + "light", "hue", "123", config_entry=config_entry + ) + registry.async_update_entity(entity_entry.entity_id, name="yo") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + # Disable entity, we should not do anything, only act when enabled. + registry.async_update_entity(entity_entry.entity_id, disabled_by="user") + await hass.async_block_till_done() + assert not handler.changed + assert handler._remove_call_later is None + + # Enable entity, check we are reloading config entry. + registry.async_update_entity(entity_entry.entity_id, disabled_by=None) + await hass.async_block_till_done() + assert handler.changed == {config_entry.entry_id} + assert handler._remove_call_later is not None + + async_fire_time_changed( + hass, + dt.utcnow() + + timedelta( + seconds=config_entries.EntityRegistryDisabledHandler.RELOAD_AFTER_UPDATE_DELAY + + 1 + ), + ) + await hass.async_block_till_done() + + assert len(mock_unload_entry.mock_calls) == 1 diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 0c73e9f38f0..b5574fe96fd 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -2,21 +2,19 @@ import os from pathlib import Path from unittest.mock import patch, call +from pytest import raises from homeassistant import setup from homeassistant.requirements import ( CONSTRAINT_FILE, + async_get_integration_with_requirements, async_process_requirements, PROGRESS_FILE, _install, + RequirementsNotFound, ) -from tests.common import ( - get_test_home_assistant, - MockModule, - mock_coro, - mock_integration, -) +from tests.common import get_test_home_assistant, MockModule, mock_integration class TestRequirements: @@ -77,24 +75,52 @@ class TestRequirements: async def test_install_existing_package(hass): """Test an install attempt on an existing package.""" with patch( - "homeassistant.util.package.install_package", return_value=mock_coro(True) + "homeassistant.util.package.install_package", return_value=True ) as mock_inst: - assert await async_process_requirements( - hass, "test_component", ["hello==1.0.0"] - ) + await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) assert len(mock_inst.mock_calls) == 1 with patch("homeassistant.util.package.is_installed", return_value=True), patch( "homeassistant.util.package.install_package" ) as mock_inst: - assert await async_process_requirements( - hass, "test_component", ["hello==1.0.0"] - ) + await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) assert len(mock_inst.mock_calls) == 0 +async def test_install_missing_package(hass): + """Test an install attempt on an existing package.""" + with patch( + "homeassistant.util.package.install_package", return_value=False + ) as mock_inst: + with raises(RequirementsNotFound): + await async_process_requirements(hass, "test_component", ["hello==1.0.0"]) + + assert len(mock_inst.mock_calls) == 1 + + +async def test_get_integration_with_requirements(hass): + """Check getting an integration with loaded requirements.""" + hass.config.skip_pip = False + mock_integration(hass, MockModule("test_component", requirements=["hello==1.0.0"])) + + with patch( + "homeassistant.util.package.is_installed", return_value=False + ) as mock_is_installed, patch( + "homeassistant.util.package.install_package", return_value=True + ) as mock_inst: + + integration = await async_get_integration_with_requirements( + hass, "test_component" + ) + assert integration + assert integration.domain == "test_component" + + assert len(mock_is_installed.mock_calls) == 1 + assert len(mock_inst.mock_calls) == 1 + + async def test_install_with_wheels_index(hass): """Test an install attempt with wheels index URL.""" hass.config.skip_pip = False diff --git a/tox.ini b/tox.ini index dd7206f403a..2d4cf7c54ba 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,6 @@ commands = python -m script.gen_requirements_all validate python -m script.hassfest validate flake8 {posargs: homeassistant tests script} - pydocstyle {posargs:homeassistant tests} [testenv:typing] whitelist_externals=/bin/bash diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev deleted file mode 100644 index 260a29cb3d0..00000000000 --- a/virtualization/Docker/Dockerfile.dev +++ /dev/null @@ -1,64 +0,0 @@ -# Dockerfile for development -# Based on the production Dockerfile, but with development additions. -# Keep this file as close as possible to the production Dockerfile, so the environments match. - -FROM python:3.7-buster -LABEL maintainer="Paulus Schoutsen " - -# Uncomment any of the following lines to disable the installation. -#ENV INSTALL_TELLSTICK no -#ENV INSTALL_OPENALPR no -#ENV INSTALL_FFMPEG no -#ENV INSTALL_LIBCEC no -#ENV INSTALL_COAP no -#ENV INSTALL_SSOCR no -#ENV INSTALL_DLIB no -#ENV INSTALL_IPERF3 no -#ENV INSTALL_LOCALES no - -VOLUME /config - -WORKDIR /usr/src/app - -# Copy build scripts -COPY virtualization/Docker/ virtualization/Docker/ -RUN virtualization/Docker/setup_docker_prereqs - -# Install hass component dependencies -COPY requirements_all.txt requirements_all.txt - -RUN pip3 install --no-cache-dir -r requirements_all.txt && \ - pip3 install --no-cache-dir mysqlclient psycopg2 uvloop==0.12.2 cchardet cython - -# BEGIN: Development additions - -# Install git -RUN apt-get update \ - && apt-get install -y --no-install-recommends git \ - && rm -rf /var/lib/apt/lists/* - -# Install nodejs -RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && \ - apt-get install -y nodejs - -# Install tox -RUN pip3 install --no-cache-dir tox - -# Copy over everything required to run tox -COPY requirements_test_all.txt setup.cfg setup.py tox.ini ./ -COPY homeassistant/const.py homeassistant/const.py - -# Prefetch dependencies for tox -COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt -RUN tox -e py37 --notest - -# END: Development additions - -# Copy source -COPY . . - -EXPOSE 8123 -EXPOSE 8300 -EXPOSE 51827 - -CMD [ "python", "-m", "homeassistant", "--config", "/config" ] diff --git a/virtualization/Docker/scripts/libcec b/virtualization/Docker/scripts/libcec deleted file mode 100755 index 481b3e700ac..00000000000 --- a/virtualization/Docker/scripts/libcec +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/sh -# Sets up libcec. -# Dependencies that need to be installed: -# apt-get install cmake libudev-dev libxrandr-dev swig - -# Stop on errors -set -e - -# Load required information about the current python environment -PYTHON_LIBDIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_config_var("LIBDIR"))') -PYTHON_LDLIBRARY=$(python -c 'from distutils import sysconfig; print(sysconfig.get_config_var("LDLIBRARY"))') -PYTHON_LIBRARY="${PYTHON_LIBDIR}/${PYTHON_LDLIBRARY}" -PYTHON_INCLUDE_DIR=$(python -c 'from distutils import sysconfig; print(sysconfig.get_python_inc())') - -cd /usr/src/app/ -mkdir -p build && cd build - -if [ ! -d libcec ]; then - git clone --branch release --depth 1 https://github.com/Pulse-Eight/libcec.git -fi - -cd libcec -git checkout release -git pull -git submodule update --init src/platform - -# Build libcec platform libs -( - mkdir -p src/platform/build - cd src/platform/build - cmake .. - make - make install -) - -# Build libcec -( - mkdir -p build && cd build - - cmake \ - -DPYTHON_LIBRARY="${PYTHON_LIBRARY}" \ - -DPYTHON_INCLUDE_DIR="${PYTHON_INCLUDE_DIR}" \ - .. - make -j$(nproc) - make install - ldconfig -) diff --git a/virtualization/Docker/scripts/locales b/virtualization/Docker/scripts/locales deleted file mode 100755 index cbbe0341575..00000000000 --- a/virtualization/Docker/scripts/locales +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# Sets up locales. - -# Stop on errors -set -e - -apt-get update -apt-get install -y --no-install-recommends locales - -# Set the locale -sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen -locale-gen diff --git a/virtualization/Docker/scripts/openalpr b/virtualization/Docker/scripts/openalpr deleted file mode 100755 index 38669f8175b..00000000000 --- a/virtualization/Docker/scripts/openalpr +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# Sets up openalpr. - -# Stop on errors -set -e - -PACKAGES=( - # homeassistant.components.image_processing.openalpr_local - libopencv-dev libtesseract-dev libleptonica-dev liblog4cplus-dev -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} - -cd /usr/src/app/ -mkdir -p build && cd build - -# Clone the latest code from GitHub -git clone --depth 1 https://github.com/openalpr/openalpr.git openalpr - -# Setup the build directory -cd openalpr/src/ -mkdir -p build -cd build - -# Setup the compile environment -cmake -DWITH_TESTS=FALSE -DWITH_BINDING_JAVA=FALSE --DWITH_BINDING_PYTHON=FALSE --DWITH_BINDING_GO=FALSE -DWITH_DAEMON=FALSE -DCMAKE_INSTALL_PREFIX:PATH=/usr/local .. - -# compile the library -make -j$(nproc) - -# Install the binaries/libraries to your local system (prefix is /usr/local) -make install diff --git a/virtualization/Docker/scripts/ssocr b/virtualization/Docker/scripts/ssocr deleted file mode 100755 index 6778bcab90d..00000000000 --- a/virtualization/Docker/scripts/ssocr +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Sets up ssocr to support Seven Segments Display. - -# Stop on errors -set -e - -PACKAGES=( - libimlib2 libimlib2-dev -) - -apt-get install -y --no-install-recommends ${PACKAGES[@]} - -cd /usr/src/app/ -mkdir -p build && cd build - -# Clone the latest code from GitHub -git clone --depth 1 https://github.com/auerswal/ssocr.git ssocr -cd ssocr/ - -# Compile the library -make -j$(nproc) - -# Install the binaries/libraries to your local system (prefix is /usr/local) -make install diff --git a/virtualization/Docker/scripts/tellstick b/virtualization/Docker/scripts/tellstick deleted file mode 100755 index d35e1cac2db..00000000000 --- a/virtualization/Docker/scripts/tellstick +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -# Sets up tellstick. - -# Stop on errors -set -e - -PACKAGES=( - # homeassistant.components.tellstick - libtelldus-core2 socat -) - -# Add Tellstick repository -echo "deb http://download.telldus.com/debian/ stable main" >> /etc/apt/sources.list.d/telldus.list -wget -qO - http://download.telldus.com/debian/telldus-public.key | apt-key add - - -apt-get update -apt-get install -y --no-install-recommends ${PACKAGES[@]} diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs deleted file mode 100755 index 62ac73d366e..00000000000 --- a/virtualization/Docker/setup_docker_prereqs +++ /dev/null @@ -1,84 +0,0 @@ -#!/bin/bash -# Install requirements and build dependencies for Home Assistant in Docker. - -# Stop on errors -set -e - -INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" -INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" -INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" -INSTALL_SSOCR="${INSTALL_SSOCR:-yes}" -INSTALL_DLIB="${INSTALL_DLIB:-yes}" -INSTALL_LOCALES="${INSTALL_LOCALES:-yes}" - -# Required debian packages for running hass or components -PACKAGES=( - # build-essential is required for python pillow module on non-x86_64 arch - build-essential - # homeassistant.components.image_processing.openalpr_local - libxrandr-dev - # homeassistant.components.device_tracker.nmap_tracker - nmap net-tools libcurl3-dev - # homeassistant.components.device_tracker.bluetooth_tracker - bluetooth libglib2.0-dev libbluetooth-dev - # homeassistant.components.device_tracker.owntracks - libsodium23 - # homeassistant.components.zwave - libudev-dev - # homeassistant.components.homekit_controller - libmpc-dev libmpfr-dev libgmp-dev - # homeassistant.components.ffmpeg - ffmpeg - # homeassistant.components.stream - libavformat-dev libavcodec-dev libavdevice-dev - libavutil-dev libswscale-dev libswresample-dev libavfilter-dev - # homeassistant.components.sensor.iperf3 - iperf3 -) - -# Required debian packages for building dependencies -PACKAGES_DEV=( - cmake - git - swig -) - -# Install packages -apt-get update -apt-get install -y --no-install-recommends ${PACKAGES[@]} ${PACKAGES_DEV[@]} - -# This is a list of scripts that install additional dependencies. If you only -# need to install a package from the official debian repository, just add it -# to the list above. Only create a script if you need compiling, manually -# downloading or a 3rd party repository. -if [ "$INSTALL_TELLSTICK" == "yes" ]; then - virtualization/Docker/scripts/tellstick -fi - -if [ "$INSTALL_OPENALPR" == "yes" ]; then - virtualization/Docker/scripts/openalpr -fi - -if [ "$INSTALL_LIBCEC" == "yes" ]; then - virtualization/Docker/scripts/libcec -fi - -if [ "$INSTALL_SSOCR" == "yes" ]; then - virtualization/Docker/scripts/ssocr -fi - -if [ "$INSTALL_DLIB" == "yes" ]; then - pip3 install --no-cache-dir "dlib>=19.5" -fi - -if [ "$INSTALL_LOCALES" == "yes" ]; then - virtualization/Docker/scripts/locales -fi - -# Remove packages -apt-get remove -y --purge ${PACKAGES_DEV[@]} -apt-get -y --purge autoremove - -# Cleanup -apt-get clean -rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/src/app/build/ diff --git a/virtualization/vagrant/Vagrantfile b/virtualization/vagrant/Vagrantfile deleted file mode 100644 index d3974d51a7a..00000000000 --- a/virtualization/vagrant/Vagrantfile +++ /dev/null @@ -1,24 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant.configure(2) do |config| - config.vm.box = "debian/contrib-stretch64" - config.vm.synced_folder "../../", "/home-assistant" - config.vm.synced_folder "./config", "/root/.homeassistant" - config.vm.network "forwarded_port", guest: 8123, host: 8123 - config.vm.provision "fix-no-tty", type: "shell" do |shell| - shell.path = "provision.sh" - end - config.vm.provider :virtualbox do |vb| - vb.cpus = 2 - vb.customize ['modifyvm', :id, '--memory', '1024'] - end - config.vm.provider :hyperv do |h, override| - override.vm.box = "generic/debian9" - override.vm.hostname = "contrib-stretch" - h.vmname = "home-assistant" - h.cpus = 2 - h.memory = 1024 - h.maxmemory = 1024 - end -end diff --git a/virtualization/vagrant/config/.placeholder b/virtualization/vagrant/config/.placeholder deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/virtualization/vagrant/home-assistant@.service b/virtualization/vagrant/home-assistant@.service deleted file mode 100644 index 91b7307f30f..00000000000 --- a/virtualization/vagrant/home-assistant@.service +++ /dev/null @@ -1,23 +0,0 @@ -# This is a simple service file for systems with systemd to tun HA as user. -# -# For details please check https://home-assistant.io/getting-started/autostart/ -# -[Unit] -Description=Home Assistant for %i -After=network.target - -[Service] -Type=simple -User=%i -# Enable the following line if you get network-related HA errors during boot -#ExecStartPre=/usr/bin/sleep 60 -# Use `whereis hass` to determine the path of hass -ExecStart=/usr/bin/hass --runner -SendSIGKILL=no -RestartForceExitStatus=100 - -# on vagrant (vboxfs), disable sendfile https://www.virtualbox.org/ticket/9069 -Environment=AIOHTTP_NOSENDFILE=1 - -[Install] -WantedBy=multi-user.target diff --git a/virtualization/vagrant/provision.bat b/virtualization/vagrant/provision.bat deleted file mode 100644 index c8174e939a1..00000000000 --- a/virtualization/vagrant/provision.bat +++ /dev/null @@ -1,50 +0,0 @@ -@echo off -call:main %* -goto:eof - -:usage -echo.############################################################ -echo. -echo.Use `./provision.bat` to interact with HASS. E.g: -echo. -echo.- setup the environment: `./provision.bat start` -echo.- restart HASS process: `./provision.bat restart` -echo.- run test suit: `./provision.bat tests` -echo.- destroy the host and start anew: `./provision.bat recreate` -echo. -echo.Official documentation at https://home-assistant.io/docs/installation/vagrant/ -echo. -echo.############################################################' -goto:eof - -:main -if "%*"=="setup" ( - if exist setup_done del setup_done - vagrant up --provision - copy /y nul setup_done -) else ( -if "%*"=="tests" ( - copy /y nul run_tests - vagrant provision -) else ( -if "%*"=="restart" ( - copy /y nul restart - vagrant provision -) else ( -if "%*"=="start" ( - vagrant up --provision -) else ( -if "%*"=="stop" ( - vagrant halt -) else ( -if "%*"=="destroy" ( - vagrant destroy -f -) else ( -if "%*"=="recreate" ( - if exist setup_done del setup_done - if exist restart del restart - vagrant destroy -f - vagrant up --provision -) else ( - call:usage -))))))) diff --git a/virtualization/vagrant/provision.sh b/virtualization/vagrant/provision.sh deleted file mode 100755 index 1d2eecddc73..00000000000 --- a/virtualization/vagrant/provision.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -set -e - -readonly SETUP_DONE='/home-assistant/virtualization/vagrant/setup_done' -readonly RUN_TESTS='/home-assistant/virtualization/vagrant/run_tests' -readonly RESTART='/home-assistant/virtualization/vagrant/restart' - -usage() { - echo '############################################################ - -Use `./provision.sh` to interact with HASS. E.g: - -- setup the environment: `./provision.sh start` -- restart HASS process: `./provision.sh restart` -- run test suit: `./provision.sh tests` -- destroy the host and start anew: `./provision.sh recreate` - -Official documentation at https://home-assistant.io/docs/installation/vagrant/ - -############################################################' -} - -print_done() { - echo '############################################################ - - -HASS running => http://localhost:8123/ - -' -} - -setup_error() { - echo '############################################################ -Something is off... maybe setup did not complete properly? -Please ensure setup did run correctly at least once. - -To run setup again: `./provision.sh setup` - -############################################################' - exit 1 -} - -setup() { - local hass_path='/root/venv/bin/hass' - local systemd_bin_path='/usr/bin/hass' - # Setup systemd - cp /home-assistant/virtualization/vagrant/home-assistant@.service \ - /etc/systemd/system/home-assistant.service - systemctl --system daemon-reload - systemctl enable home-assistant - systemctl stop home-assistant - # Install packages - apt-get update - apt-get install -y git rsync python3-dev python3-pip libssl-dev libffi-dev - pip3 install --upgrade virtualenv - virtualenv ~/venv - source ~/venv/bin/activate - pip3 install --upgrade tox - /home-assistant/script/setup - if ! [ -f $systemd_bin_path ]; then - ln -s $hass_path $systemd_bin_path - fi - touch $SETUP_DONE - print_done - usage -} - -run_tests() { - rm -f $RUN_TESTS - echo '############################################################' - echo; echo "Running test suite, hang on..."; echo; echo - if ! systemctl stop home-assistant; then - setup_error - fi - source ~/venv/bin/activate - rsync -a --delete \ - --exclude='*.tox' \ - --exclude='*.git' \ - --exclude='.vagrant' \ - --exclude='lib64' \ - --exclude='bin/python' \ - --exclude='bin/python3' \ - /home-assistant/ /home-assistant-tests/ - cd /home-assistant-tests && tox || true - echo '############################################################' -} - -restart() { - echo "Restarting Home Assistant..." - if ! systemctl restart home-assistant; then - setup_error - else - echo "done" - fi - rm $RESTART -} - -main() { - # If a parameter is provided, we assume it's the user interacting - # with the provider script... - case $1 in - "setup") rm -f setup_done; vagrant up --provision && touch setup_done; exit ;; - "tests") touch run_tests; vagrant provision ; exit ;; - "restart") touch restart; vagrant provision ; exit ;; - "start") vagrant up --provision ; exit ;; - "stop") vagrant halt ; exit ;; - "destroy") vagrant destroy -f ; exit ;; - "recreate") rm -f setup_done restart; vagrant destroy -f; \ - vagrant up --provision; exit ;; - esac - # ...otherwise we assume it's the Vagrant provisioner - if [ $(hostname) != "contrib-jessie" ] && [ $(hostname) != "contrib-stretch" ]; then usage; exit; fi - if ! [ -f $SETUP_DONE ]; then setup; fi - if [ -f $RESTART ]; then restart; fi - if [ -f $RUN_TESTS ]; then run_tests; fi - if ! systemctl start home-assistant; then - setup_error - fi -} - -main $*