diff --git a/.coveragerc b/.coveragerc
index 9f6bb0d1b95bed..e329e2f31b9dbd 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -13,6 +13,10 @@ omit =
homeassistant/components/abode/*
homeassistant/components/acer_projector/switch.py
homeassistant/components/actiontec/device_tracker.py
+ homeassistant/components/adguard/__init__.py
+ homeassistant/components/adguard/const.py
+ homeassistant/components/adguard/sensor.py
+ homeassistant/components/adguard/switch.py
homeassistant/components/ads/*
homeassistant/components/aftership/sensor.py
homeassistant/components/airvisual/sensor.py
@@ -172,6 +176,7 @@ omit =
homeassistant/components/esphome/camera.py
homeassistant/components/esphome/climate.py
homeassistant/components/esphome/cover.py
+ homeassistant/components/esphome/entry_data.py
homeassistant/components/esphome/fan.py
homeassistant/components/esphome/light.py
homeassistant/components/esphome/sensor.py
@@ -488,6 +493,8 @@ omit =
homeassistant/components/reddit/*
homeassistant/components/rejseplanen/sensor.py
homeassistant/components/remember_the_milk/__init__.py
+ homeassistant/components/repetier/__init__.py
+ homeassistant/components/repetier/sensor.py
homeassistant/components/remote_rpi_gpio/*
homeassistant/components/rest/binary_sensor.py
homeassistant/components/rest/notify.py
@@ -548,6 +555,7 @@ omit =
homeassistant/components/sochain/sensor.py
homeassistant/components/socialblade/sensor.py
homeassistant/components/solaredge/sensor.py
+ homeassistant/components/solaredge_local/sensor.py
homeassistant/components/solax/sensor.py
homeassistant/components/somfy_mylink/*
homeassistant/components/sonarr/sensor.py
diff --git a/CODEOWNERS b/CODEOWNERS
index 1e7c3c87a0700b..2dc7d5f3701ed1 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -17,6 +17,7 @@ virtualization/Docker/* @home-assistant/docker
homeassistant/scripts/check_config.py @kellerza
# Integrations
+homeassistant/components/adguard/* @frenck
homeassistant/components/airvisual/* @bachya
homeassistant/components/alarm_control_panel/* @colinodell
homeassistant/components/alpha_vantage/* @fabaff
@@ -30,6 +31,7 @@ homeassistant/components/asuswrt/* @kennedyshead
homeassistant/components/auth/* @home-assistant/core
homeassistant/components/automatic/* @armills
homeassistant/components/automation/* @home-assistant/core
+homeassistant/components/awair/* @danielsjf
homeassistant/components/aws/* @awarecan @robbiet480
homeassistant/components/axis/* @kane610
homeassistant/components/azure_event_hub/* @eavanvalkenburg
@@ -149,6 +151,7 @@ homeassistant/components/mcp23017/* @jardiamj
homeassistant/components/mediaroom/* @dgomes
homeassistant/components/melissa/* @kennedyshead
homeassistant/components/met/* @danielhiversen
+homeassistant/components/meteo_france/* @victorcerutti
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/mill/* @danielhiversen
@@ -193,6 +196,7 @@ homeassistant/components/qwikswitch/* @kellerza
homeassistant/components/raincloud/* @vanstinator
homeassistant/components/rainmachine/* @bachya
homeassistant/components/random/* @fabaff
+homeassistant/components/repetier/* @MTrab
homeassistant/components/rfxtrx/* @danielhiversen
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roomba/* @pschmitt
@@ -200,6 +204,7 @@ homeassistant/components/ruter/* @ludeeus
homeassistant/components/scene/* @home-assistant/core
homeassistant/components/scrape/* @fabaff
homeassistant/components/script/* @home-assistant/core
+homeassistant/components/sense/* @kbickar
homeassistant/components/sensibo/* @andrey-git
homeassistant/components/serial/* @fabaff
homeassistant/components/seventeentrack/* @bachya
@@ -211,6 +216,7 @@ homeassistant/components/sma/* @kellerza
homeassistant/components/smarthab/* @outadoc
homeassistant/components/smartthings/* @andrewsayre
homeassistant/components/smtp/* @fabaff
+homeassistant/components/solaredge_local/* @drobtravels
homeassistant/components/solax/* @squishykid
homeassistant/components/sonos/* @amelchio
homeassistant/components/spaceapi/* @fabaff
@@ -274,6 +280,7 @@ homeassistant/components/yeelight/* @rytilahti @zewelor
homeassistant/components/yeelightsunflower/* @lindsaymarkward
homeassistant/components/yessssms/* @flowolf
homeassistant/components/yi/* @bachya
+homeassistant/components/yr/* @danielhiversen
homeassistant/components/zeroconf/* @robbiet480 @Kane610
homeassistant/components/zha/* @dmulcahey @adminiuga
homeassistant/components/zone/* @home-assistant/core
diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml
new file mode 100644
index 00000000000000..8f250f16ce3456
--- /dev/null
+++ b/azure-pipelines-release.yml
@@ -0,0 +1,168 @@
+# https://dev.azure.com/home-assistant
+
+trigger:
+ batch: true
+ tags:
+ include:
+ - '*'
+pr: none
+variables:
+ - name: versionBuilder
+ value: '3.2'
+ - group: docker
+ - group: github
+ - group: twine
+
+
+jobs:
+
+
+- job: 'VersionValidate'
+ condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
+ 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
+
+ release="$(Build.SourceBranchName)"
+ created_by="$(curl -s https://api.github.com/repos/home-assistant/home-assistant/releases/tags/${release} | jq --raw-output '.author.login')"
+
+ if [[ "${created_by}" =~ ^(balloob|pvizeli|fabaff|robbiet480)$ ]]; then
+ exit 0
+ fi
+
+ echo "${created_by} is not allowed to create an release!"
+ exit 1
+ displayName: 'Check rights'
+
+
+- job: 'ReleasePython'
+ condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
+ dependsOn:
+ - 'VersionValidate'
+ pool:
+ vmImage: 'ubuntu-latest'
+ steps:
+ - task: UsePythonVersion@0
+ displayName: 'Use Python 3.7'
+ inputs:
+ versionSpec: '3.7'
+ - script: pip install twine wheel
+ displayName: 'Install tools'
+ - script: python setup.py sdist bdist_wheel
+ displayName: 'Build package'
+ - script: |
+ export TWINE_USERNAME="$(twineUser)"
+ export TWINE_PASSWORD="$(twinePassword)"
+
+ twine upload dist/* --skip-existing
+ displayName: 'Upload pypi'
+
+
+- job: 'ReleaseDocker'
+ condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate'))
+ dependsOn:
+ - 'VersionValidate'
+ timeoutInMinutes: 240
+ pool:
+ vmImage: 'ubuntu-latest'
+ strategy:
+ maxParallel: 5
+ matrix:
+ amd64:
+ buildArch: 'amd64'
+ buildMachine: 'qemux86-64,intel-nuc'
+ i386:
+ buildArch: 'i386'
+ buildMachine: 'qemux86'
+ armhf:
+ buildArch: 'armhf'
+ buildMachine: 'qemuarm,raspberrypi'
+ armv7:
+ buildArch: 'armv7'
+ buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker'
+ aarch64:
+ buildArch: 'aarch64'
+ buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime'
+ steps:
+ - script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
+ displayName: 'Docker hub login'
+ - script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
+ displayName: 'Install Builder'
+ - script: |
+ set -e
+
+ sudo docker run --rm --privileged \
+ -v ~/.docker:/root/.docker \
+ -v /run/docker.sock:/run/docker.sock:rw \
+ homeassistant/amd64-builder:$(versionBuilder) \
+ --homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
+ -r https://github.com/home-assistant/hassio-homeassistant \
+ -t generic --docker-hub homeassistant
+
+ sudo 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)" \
+ -r https://github.com/home-assistant/hassio-homeassistant \
+ -t machine --docker-hub homeassistant
+ displayName: 'Build Release'
+
+
+- job: 'ReleaseHassio'
+ condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('ReleaseDocker'))
+ dependsOn:
+ - 'ReleaseDocker'
+ pool:
+ vmImage: 'ubuntu-latest'
+ steps:
+ - script: |
+ sudo apt-get install -y --no-install-recommends \
+ git jq curl
+
+ git config --global user.name "Pascal Vizeli"
+ git config --global user.email "pvizeli@syshack.ch"
+ git config --global credential.helper store
+
+ echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME/.git-credentials
+ displayName: 'Install requirements'
+ - script: |
+ set -e
+
+ version="$(Build.SourceBranchName)"
+
+ git clone https://github.com/home-assistant/hassio-version
+ cd hassio-version
+
+ dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
+ beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
+ stable_version="$(jq --raw-output '.homeassistant.default' stable.json)"
+
+ if [[ "$version" =~ b ]]; then
+ sed -i "s|$dev_version|$version|g" dev.json
+ 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
+
+ git commit -am "Bump Home Assistant $version"
+ git push
+ displayName: 'Update version files'
diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml
new file mode 100644
index 00000000000000..c49c7ee0358d44
--- /dev/null
+++ b/azure-pipelines-wheels.yml
@@ -0,0 +1,100 @@
+# https://dev.azure.com/home-assistant
+
+trigger:
+ batch: true
+ branches:
+ include:
+ - dev
+ paths:
+ include:
+ - requirements_all.txt
+pr: none
+variables:
+ - name: versionWheels
+ value: '0.7'
+ - group: wheels
+
+
+jobs:
+
+- job: 'Wheels'
+ condition: or(eq(variables['Build.SourceBranchName'], 'dev'), eq(variables['Build.SourceBranchName'], 'master'))
+ timeoutInMinutes: 360
+ pool:
+ vmImage: 'ubuntu-latest'
+ strategy:
+ maxParallel: 3
+ 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'
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
deleted file mode 100644
index 7a1e6e550d79c3..00000000000000
--- a/azure-pipelines.yml
+++ /dev/null
@@ -1,188 +0,0 @@
-# https://dev.azure.com/home-assistant
-
-trigger:
- batch: true
- branches:
- include:
- - dev
- tags:
- include:
- - '*'
-
-variables:
- - name: versionBuilder
- value: '3.2'
- - name: versionWheels
- value: '0.3'
- - group: docker
- - group: wheels
- - group: github
-
-jobs:
-
-- job: 'Wheels'
- condition: eq(variables['Build.SourceBranchName'], 'dev')
- timeoutInMinutes: 360
- pool:
- vmImage: 'ubuntu-16.04'
- strategy:
- maxParallel: 3
- 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
-
- 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_hassio.txt
-
- # Enable because we can build it
- sed -i "s|# pytradfri|pytradfri|g" requirements_hassio.txt
- sed -i "s|# pybluez|pybluez|g" requirements_hassio.txt
- sed -i "s|# bluepy|bluepy|g" requirements_hassio.txt
- sed -i "s|# beacontools|beacontools|g" requirements_hassio.txt
- sed -i "s|# RPi.GPIO|RPi.GPIO|g" requirements_hassio.txt
- sed -i "s|# raspihats|raspihats|g" requirements_hassio.txt
- sed -i "s|# rpi-rf|rpi-rf|g" requirements_hassio.txt
- sed -i "s|# blinkt|blinkt|g" requirements_hassio.txt
- sed -i "s|# fritzconnection|fritzconnection|g" requirements_hassio.txt
- sed -i "s|# pyuserinput|pyuserinput|g" requirements_hassio.txt
- sed -i "s|# evdev|evdev|g" requirements_hassio.txt
- sed -i "s|# smbus-cffi|smbus-cffi|g" requirements_hassio.txt
- sed -i "s|# i2csense|i2csense|g" requirements_hassio.txt
- sed -i "s|# python-eq3bt|python-eq3bt|g" requirements_hassio.txt
- sed -i "s|# pycups|pycups|g" requirements_hassio.txt
- sed -i "s|# homekit|homekit|g" requirements_hassio.txt
- sed -i "s|# decora_wifi|decora_wifi|g" requirements_hassio.txt
- sed -i "s|# decora|decora|g" requirements_hassio.txt
- sed -i "s|# PySwitchbot|PySwitchbot|g" requirements_hassio.txt
- sed -i "s|# pySwitchmate|pySwitchmate|g" requirements_hassio.txt
-
- # Disable because of error
- sed -i "s|insteonplm|# insteonplm|g" requirements_hassio.txt
- 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 https://wheels.hass.io \
- --requirement requirements_hassio.txt \
- --upload rsync \
- --remote wheels@$(wheelsHost):/opt/wheels
- displayName: 'Run wheels build'
-
-
-- job: 'Release'
- condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
- timeoutInMinutes: 120
- pool:
- vmImage: 'ubuntu-16.04'
- strategy:
- maxParallel: 5
- matrix:
- amd64:
- buildArch: 'amd64'
- buildMachine: 'qemux86-64,intel-nuc'
- i386:
- buildArch: 'i386'
- buildMachine: 'qemux86'
- armhf:
- buildArch: 'armhf'
- buildMachine: 'qemuarm,raspberrypi'
- armv7:
- buildArch: 'armv7'
- buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker'
- aarch64:
- buildArch: 'aarch64'
- buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime'
- steps:
- - script: sudo docker login -u $(dockerUser) -p $(dockerPassword)
- displayName: 'Docker hub login'
- - script: sudo docker pull homeassistant/amd64-builder:$(versionBuilder)
- displayName: 'Install Builder'
- - script: |
- set -e
-
- sudo docker run --rm --privileged \
- -v ~/.docker:/root/.docker \
- -v /run/docker.sock:/run/docker.sock:rw \
- homeassistant/amd64-builder:$(versionBuilder) \
- --homeassistant $(Build.SourceBranchName) "--$(buildArch)" \
- -r https://github.com/home-assistant/hassio-homeassistant \
- -t generic --docker-hub homeassistant
-
- sudo 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)" \
- -r https://github.com/home-assistant/hassio-homeassistant \
- -t machine --docker-hub homeassistant
- displayName: 'Build Release'
-
-
-- job: 'ReleasePublish'
- condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('Release'))
- dependsOn:
- - 'Release'
- pool:
- vmImage: 'ubuntu-16.04'
- steps:
- - script: |
- sudo apt-get install -y --no-install-recommends \
- git jq
-
- git config --global user.name "Pascal Vizeli"
- git config --global user.email "pvizeli@syshack.ch"
- git config --global credential.helper store
-
- echo "https://$(githubToken):x-oauth-basic@github.com" > $HOME/.git-credentials
- displayName: 'Install requirements'
- - script: |
- set -e
-
- version="$(Build.SourceBranchName)"
-
- git clone https://github.com/home-assistant/hassio-version
- cd hassio-version
-
- dev_version="$(jq --raw-output '.homeassistant.default' dev.json)"
- beta_version="$(jq --raw-output '.homeassistant.default' beta.json)"
- stable_version="$(jq --raw-output '.homeassistant.default' stable.json)"
-
- if [[ "$version" =~ b ]]; then
- sed -i "s|$dev_version|$version|g" dev.json
- 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
-
- git commit -am "Bump Home Assistant $version"
- git push
- displayName: 'Update version files'
diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py
index d63caf9e76f405..79e5ec248ae128 100644
--- a/homeassistant/bootstrap.py
+++ b/homeassistant/bootstrap.py
@@ -17,7 +17,6 @@
from homeassistant.util.package import async_get_user_site, is_virtual_env
from homeassistant.util.yaml import clear_secret_cache
from homeassistant.exceptions import HomeAssistantError
-from homeassistant.helpers import config_validation as cv
_LOGGER = logging.getLogger(__name__)
@@ -94,49 +93,11 @@ async def async_from_config_dict(config: Dict[str, Any],
stop = time()
_LOGGER.info("Home Assistant initialized in %.2fs", stop-start)
- # TEMP: warn users for invalid slugs
- # Remove after 0.94 or 1.0
- if cv.INVALID_SLUGS_FOUND or cv.INVALID_ENTITY_IDS_FOUND:
- msg = []
-
- if cv.INVALID_ENTITY_IDS_FOUND:
- msg.append(
- "Your configuration contains invalid entity ID references. "
- "Please find and update the following. "
- "This will become a breaking change."
- )
- msg.append('\n'.join('- {} -> {}'.format(*item)
- for item
- in cv.INVALID_ENTITY_IDS_FOUND.items()))
-
- if cv.INVALID_SLUGS_FOUND:
- msg.append(
- "Your configuration contains invalid slugs. "
- "Please find and update the following. "
- "This will become a breaking change."
- )
- msg.append('\n'.join('- {} -> {}'.format(*item)
- for item in cv.INVALID_SLUGS_FOUND.items()))
-
- hass.components.persistent_notification.async_create(
- '\n\n'.join(msg), "Config Warning", "config_warning"
- )
-
- # TEMP: warn users of invalid extra keys
- # Remove after 0.92
- if cv.INVALID_EXTRA_KEYS_FOUND:
- msg = []
- msg.append(
- "Your configuration contains extra keys "
- "that the platform does not support (but were silently "
- "accepted before 0.88). Please find and remove the following."
- "This will become a breaking change."
- )
- msg.append('\n'.join('- {}'.format(it)
- for it in cv.INVALID_EXTRA_KEYS_FOUND))
-
+ if sys.version_info[:3] < (3, 6, 0):
hass.components.persistent_notification.async_create(
- '\n\n'.join(msg), "Config Warning", "config_warning"
+ "Python 3.5 support is deprecated and will "
+ "be removed in the first release after August 1. Please "
+ "upgrade Python.", "Python version", "python_version"
)
return hass
diff --git a/homeassistant/components/adguard/.translations/ca.json b/homeassistant/components/adguard/.translations/ca.json
new file mode 100644
index 00000000000000..1966002ea136f5
--- /dev/null
+++ b/homeassistant/components/adguard/.translations/ca.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home."
+ },
+ "error": {
+ "connection_error": "No s'ha pogut connectar."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb l'AdGuard Home proporcionat pel complement de Hass.io: {addon}?",
+ "title": "AdGuard Home (complement de Hass.io)"
+ },
+ "user": {
+ "data": {
+ "host": "Amfitri\u00f3",
+ "password": "Contrasenya",
+ "port": "Port",
+ "ssl": "AdGuard Home utilitza un certificat SSL",
+ "username": "Nom d'usuari",
+ "verify_ssl": "AdGuard Home utilitza un certificat adequat"
+ },
+ "description": "Configuraci\u00f3 de la inst\u00e0ncia d'AdGuard Home, permet el control i la monitoritzaci\u00f3.",
+ "title": "Enlla\u00e7ar AdGuard Home."
+ }
+ },
+ "title": "AdGuard Home"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/en.json b/homeassistant/components/adguard/.translations/en.json
new file mode 100644
index 00000000000000..d5f5e9ff78c6cf
--- /dev/null
+++ b/homeassistant/components/adguard/.translations/en.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
+ },
+ "error": {
+ "connection_error": "Failed to connect."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?",
+ "title": "AdGuard Home via Hass.io add-on"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "port": "Port",
+ "ssl": "AdGuard Home uses a SSL certificate",
+ "username": "Username",
+ "verify_ssl": "AdGuard Home uses a proper certificate"
+ },
+ "description": "Set up your AdGuard Home instance to allow monitoring and control.",
+ "title": "Link your AdGuard Home."
+ }
+ },
+ "title": "AdGuard Home"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/no.json b/homeassistant/components/adguard/.translations/no.json
new file mode 100644
index 00000000000000..0e18537dcf8b2d
--- /dev/null
+++ b/homeassistant/components/adguard/.translations/no.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Kun \u00e9n enkelt konfigurasjon av AdGuard Hjemer tillatt."
+ },
+ "error": {
+ "connection_error": "Tilkobling mislyktes."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Hjem gitt av hass.io tillegget {addon}?",
+ "title": "AdGuard Hjem via Hass.io tillegg"
+ },
+ "user": {
+ "data": {
+ "host": "Vert",
+ "password": "Passord",
+ "port": "Port",
+ "ssl": "AdGuard Hjem bruker et SSL-sertifikat",
+ "username": "Brukernavn",
+ "verify_ssl": "AdGuard Home bruker et riktig sertifikat"
+ },
+ "description": "Sett opp din AdGuard Hjem instans for \u00e5 tillate overv\u00e5king og kontroll.",
+ "title": "Koble til ditt AdGuard Hjem."
+ }
+ },
+ "title": "AdGuard Hjem"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/pt-BR.json b/homeassistant/components/adguard/.translations/pt-BR.json
new file mode 100644
index 00000000000000..a6115800787503
--- /dev/null
+++ b/homeassistant/components/adguard/.translations/pt-BR.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Apenas uma \u00fanica configura\u00e7\u00e3o do AdGuard Home \u00e9 permitida."
+ },
+ "error": {
+ "connection_error": "Falhou ao conectar."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "Deseja configurar o Home Assistant para se conectar ao AdGuard Home fornecido pelo complemento Hass.io: {addon} ?",
+ "title": "AdGuard Home via add-on Hass.io"
+ },
+ "user": {
+ "data": {
+ "host": "Host",
+ "password": "Senha",
+ "port": "Porta",
+ "ssl": "O AdGuard Home usa um certificado SSL",
+ "username": "Nome de usu\u00e1rio",
+ "verify_ssl": "O AdGuard Home usa um certificado apropriado"
+ },
+ "description": "Configure sua inst\u00e2ncia do AdGuard Home para permitir o monitoramento e o controle.",
+ "title": "Vincule o seu AdGuard Home."
+ }
+ },
+ "title": "AdGuard Home"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/ru.json b/homeassistant/components/adguard/.translations/ru.json
new file mode 100644
index 00000000000000..cddced8018de75
--- /dev/null
+++ b/homeassistant/components/adguard/.translations/ru.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "error": {
+ "connection_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?",
+ "title": "AdGuard Home (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)"
+ },
+ "user": {
+ "data": {
+ "host": "\u0425\u043e\u0441\u0442",
+ "password": "\u041f\u0430\u0440\u043e\u043b\u044c",
+ "port": "\u041f\u043e\u0440\u0442",
+ "ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL",
+ "username": "\u041b\u043e\u0433\u0438\u043d",
+ "verify_ssl": "AdGuard Home \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442"
+ },
+ "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f AdGuard Home.",
+ "title": "AdGuard Home"
+ }
+ },
+ "title": "AdGuard Home"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/sv.json b/homeassistant/components/adguard/.translations/sv.json
new file mode 100644
index 00000000000000..b4bd7f7481b6f4
--- /dev/null
+++ b/homeassistant/components/adguard/.translations/sv.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "Endast en enda konfiguration av AdGuard Home \u00e4r till\u00e5ten."
+ },
+ "error": {
+ "connection_error": "Det gick inte att ansluta."
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "Vill du konfigurera Home Assistant f\u00f6r att ansluta till AdGuard Home som tillhandah\u00e5lls av Hass.io Add-on: {addon}?",
+ "title": "AdGuard Home via Hass.io-till\u00e4gget"
+ },
+ "user": {
+ "data": {
+ "host": "V\u00e4rd",
+ "password": "L\u00f6senord",
+ "port": "Port",
+ "ssl": "AdGuard Home anv\u00e4nder ett SSL-certifikat",
+ "username": "Anv\u00e4ndarnamn",
+ "verify_ssl": "AdGuard Home anv\u00e4nder ett korrekt certifikat"
+ },
+ "description": "St\u00e4ll in din AdGuard Home-instans f\u00f6r att till\u00e5ta \u00f6vervakning och kontroll.",
+ "title": "L\u00e4nka din AdGuard Home."
+ }
+ },
+ "title": "AdGuard Home"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/.translations/zh-Hant.json b/homeassistant/components/adguard/.translations/zh-Hant.json
new file mode 100644
index 00000000000000..b97d50aa0b6ec6
--- /dev/null
+++ b/homeassistant/components/adguard/.translations/zh-Hant.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "abort": {
+ "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002"
+ },
+ "error": {
+ "connection_error": "\u9023\u7dda\u5931\u6557\u3002"
+ },
+ "step": {
+ "hassio_confirm": {
+ "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6\uff1a{addon} \u9023\u7dda\u81f3 AdGuard Home\uff1f",
+ "title": "\u4f7f\u7528 Hass.io \u9644\u52a0\u7d44\u4ef6 AdGuard Home"
+ },
+ "user": {
+ "data": {
+ "host": "\u4e3b\u6a5f\u7aef",
+ "password": "\u5bc6\u78bc",
+ "port": "\u901a\u8a0a\u57e0",
+ "ssl": "AdGuard Home \u4f7f\u7528 SSL \u8a8d\u8b49",
+ "username": "\u4f7f\u7528\u8005\u540d\u7a31",
+ "verify_ssl": "AdGuard Home \u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49"
+ },
+ "description": "\u8a2d\u5b9a AdGuard Home \u4ee5\u9032\u884c\u76e3\u63a7\u3002",
+ "title": "\u9023\u7d50 AdGuard Home\u3002"
+ }
+ },
+ "title": "AdGuard Home"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py
new file mode 100644
index 00000000000000..15b8b9978f6db9
--- /dev/null
+++ b/homeassistant/components/adguard/__init__.py
@@ -0,0 +1,180 @@
+"""Support for AdGuard Home."""
+import logging
+from typing import Any, Dict
+
+from adguardhome import AdGuardHome, AdGuardHomeError
+import voluptuous as vol
+
+from homeassistant.components.adguard.const import (
+ CONF_FORCE, DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN,
+ SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, SERVICE_REFRESH,
+ SERVICE_REMOVE_URL)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.const import (
+ CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_URL,
+ CONF_USERNAME, CONF_VERIFY_SSL)
+from homeassistant.helpers import config_validation as cv
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers.entity import Entity
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+_LOGGER = logging.getLogger(__name__)
+
+SERVICE_URL_SCHEMA = vol.Schema({vol.Required(CONF_URL): cv.url})
+SERVICE_ADD_URL_SCHEMA = vol.Schema(
+ {vol.Required(CONF_NAME): cv.string, vol.Required(CONF_URL): cv.url}
+)
+SERVICE_REFRESH_SCHEMA = vol.Schema(
+ {vol.Optional(CONF_FORCE, default=False): cv.boolean}
+)
+
+
+async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
+ """Set up the AdGuard Home components."""
+ return True
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry
+) -> bool:
+ """Set up AdGuard Home from a config entry."""
+ session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
+ adguard = AdGuardHome(
+ entry.data[CONF_HOST],
+ port=entry.data[CONF_PORT],
+ username=entry.data[CONF_USERNAME],
+ password=entry.data[CONF_PASSWORD],
+ tls=entry.data[CONF_SSL],
+ verify_ssl=entry.data[CONF_VERIFY_SSL],
+ loop=hass.loop,
+ session=session,
+ )
+
+ hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard
+
+ for component in 'sensor', 'switch':
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, component)
+ )
+
+ async def add_url(call) -> None:
+ """Service call to add a new filter subscription to AdGuard Home."""
+ await adguard.filtering.add_url(
+ call.data.get(CONF_NAME), call.data.get(CONF_URL)
+ )
+
+ async def remove_url(call) -> None:
+ """Service call to remove a filter subscription from AdGuard Home."""
+ await adguard.filtering.remove_url(call.data.get(CONF_URL))
+
+ async def enable_url(call) -> None:
+ """Service call to enable a filter subscription in AdGuard Home."""
+ await adguard.filtering.enable_url(call.data.get(CONF_URL))
+
+ async def disable_url(call) -> None:
+ """Service call to disable a filter subscription in AdGuard Home."""
+ await adguard.filtering.disable_url(call.data.get(CONF_URL))
+
+ async def refresh(call) -> None:
+ """Service call to refresh the filter subscriptions in AdGuard Home."""
+ await adguard.filtering.refresh(call.data.get(CONF_FORCE))
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_ADD_URL, add_url, schema=SERVICE_ADD_URL_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, SERVICE_REMOVE_URL, remove_url, schema=SERVICE_URL_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, SERVICE_ENABLE_URL, enable_url, schema=SERVICE_URL_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, SERVICE_DISABLE_URL, disable_url, schema=SERVICE_URL_SCHEMA
+ )
+ hass.services.async_register(
+ DOMAIN, SERVICE_REFRESH, refresh, schema=SERVICE_REFRESH_SCHEMA
+ )
+
+ return True
+
+
+async def async_unload_entry(
+ hass: HomeAssistantType, entry: ConfigType
+) -> bool:
+ """Unload AdGuard Home config entry."""
+ hass.services.async_remove(DOMAIN, SERVICE_ADD_URL)
+ hass.services.async_remove(DOMAIN, SERVICE_REMOVE_URL)
+ hass.services.async_remove(DOMAIN, SERVICE_ENABLE_URL)
+ hass.services.async_remove(DOMAIN, SERVICE_DISABLE_URL)
+ hass.services.async_remove(DOMAIN, SERVICE_REFRESH)
+
+ for component in 'sensor', 'switch':
+ await hass.config_entries.async_forward_entry_unload(entry, component)
+
+ del hass.data[DOMAIN]
+
+ return True
+
+
+class AdGuardHomeEntity(Entity):
+ """Defines a base AdGuard Home entity."""
+
+ def __init__(self, adguard, name: str, icon: str) -> None:
+ """Initialize the AdGuard Home entity."""
+ self._name = name
+ self._icon = icon
+ self._available = True
+ self.adguard = adguard
+
+ @property
+ def name(self) -> str:
+ """Return the name of the entity."""
+ return self._name
+
+ @property
+ def icon(self) -> str:
+ """Return the mdi icon of the entity."""
+ return self._icon
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
+ async def async_update(self) -> None:
+ """Update AdGuard Home entity."""
+ try:
+ await self._adguard_update()
+ self._available = True
+ except AdGuardHomeError:
+ if self._available:
+ _LOGGER.debug(
+ "An error occurred while updating AdGuard Home sensor.",
+ exc_info=True,
+ )
+ self._available = False
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ raise NotImplementedError()
+
+
+class AdGuardHomeDeviceEntity(AdGuardHomeEntity):
+ """Defines a AdGuard Home device entity."""
+
+ @property
+ def device_info(self) -> Dict[str, Any]:
+ """Return device information about this AdGuard Home instance."""
+ return {
+ 'identifiers': {
+ (
+ DOMAIN,
+ self.adguard.host,
+ self.adguard.port,
+ self.adguard.base_path,
+ )
+ },
+ 'name': 'AdGuard Home',
+ 'manufacturer': 'AdGuard Team',
+ 'sw_version': self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION),
+ }
diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py
new file mode 100644
index 00000000000000..7e144a76e222e0
--- /dev/null
+++ b/homeassistant/components/adguard/config_flow.py
@@ -0,0 +1,147 @@
+"""Config flow to configure the AdGuard Home integration."""
+import logging
+
+from adguardhome import AdGuardHome, AdGuardHomeConnectionError
+import voluptuous as vol
+
+from homeassistant import config_entries
+from homeassistant.components.adguard.const import DOMAIN
+from homeassistant.config_entries import ConfigFlow
+from homeassistant.const import (
+ CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME,
+ CONF_VERIFY_SSL)
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+_LOGGER = logging.getLogger(__name__)
+
+
+@config_entries.HANDLERS.register(DOMAIN)
+class AdGuardHomeFlowHandler(ConfigFlow):
+ """Handle a AdGuard Home config flow."""
+
+ VERSION = 1
+ CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
+
+ _hassio_discovery = None
+
+ def __init__(self):
+ """Initialize AgGuard Home flow."""
+ pass
+
+ async def _show_setup_form(self, errors=None):
+ """Show the setup form to the user."""
+ return self.async_show_form(
+ step_id='user',
+ data_schema=vol.Schema(
+ {
+ vol.Required(CONF_HOST): str,
+ vol.Required(CONF_PORT, default=3000): vol.Coerce(int),
+ vol.Optional(CONF_USERNAME): str,
+ vol.Optional(CONF_PASSWORD): str,
+ vol.Required(CONF_SSL, default=True): bool,
+ vol.Required(CONF_VERIFY_SSL, default=True): bool,
+ }
+ ),
+ errors=errors or {},
+ )
+
+ async def _show_hassio_form(self, errors=None):
+ """Show the Hass.io confirmation form to the user."""
+ return self.async_show_form(
+ step_id='hassio_confirm',
+ description_placeholders={
+ 'addon': self._hassio_discovery['addon']
+ },
+ data_schema=vol.Schema({}),
+ errors=errors or {},
+ )
+
+ async def async_step_user(self, user_input=None):
+ """Handle a flow initiated by the user."""
+ if self._async_current_entries():
+ return self.async_abort(reason='single_instance_allowed')
+
+ if user_input is None:
+ return await self._show_setup_form(user_input)
+
+ errors = {}
+
+ session = async_get_clientsession(
+ self.hass, user_input[CONF_VERIFY_SSL]
+ )
+
+ adguard = AdGuardHome(
+ user_input[CONF_HOST],
+ port=user_input[CONF_PORT],
+ username=user_input.get(CONF_USERNAME),
+ password=user_input.get(CONF_PASSWORD),
+ tls=user_input[CONF_SSL],
+ verify_ssl=user_input[CONF_VERIFY_SSL],
+ loop=self.hass.loop,
+ session=session,
+ )
+
+ try:
+ await adguard.version()
+ except AdGuardHomeConnectionError:
+ errors['base'] = 'connection_error'
+ return await self._show_setup_form(errors)
+
+ return self.async_create_entry(
+ title=user_input[CONF_HOST],
+ data={
+ CONF_HOST: user_input[CONF_HOST],
+ CONF_PASSWORD: user_input.get(CONF_PASSWORD),
+ CONF_PORT: user_input[CONF_PORT],
+ CONF_SSL: user_input[CONF_SSL],
+ CONF_USERNAME: user_input.get(CONF_USERNAME),
+ CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
+ },
+ )
+
+ async def async_step_hassio(self, user_input=None):
+ """Prepare configuration for a Hass.io AdGuard Home add-on.
+
+ This flow is triggered by the discovery component.
+ """
+ if self._async_current_entries():
+ return self.async_abort(reason='single_instance_allowed')
+
+ self._hassio_discovery = user_input
+
+ return await self.async_step_hassio_confirm()
+
+ async def async_step_hassio_confirm(self, user_input=None):
+ """Confirm Hass.io discovery."""
+ if user_input is None:
+ return await self._show_hassio_form()
+
+ errors = {}
+
+ session = async_get_clientsession(self.hass, False)
+
+ adguard = AdGuardHome(
+ self._hassio_discovery[CONF_HOST],
+ port=self._hassio_discovery[CONF_PORT],
+ tls=False,
+ loop=self.hass.loop,
+ session=session,
+ )
+
+ try:
+ await adguard.version()
+ except AdGuardHomeConnectionError:
+ errors['base'] = 'connection_error'
+ return await self._show_hassio_form(errors)
+
+ return self.async_create_entry(
+ title=self._hassio_discovery['addon'],
+ data={
+ CONF_HOST: self._hassio_discovery[CONF_HOST],
+ CONF_PORT: self._hassio_discovery[CONF_PORT],
+ CONF_PASSWORD: None,
+ CONF_SSL: False,
+ CONF_USERNAME: None,
+ CONF_VERIFY_SSL: True,
+ },
+ )
diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py
new file mode 100644
index 00000000000000..6bbabdafaf17c5
--- /dev/null
+++ b/homeassistant/components/adguard/const.py
@@ -0,0 +1,14 @@
+"""Constants for the AdGuard Home integration."""
+
+DOMAIN = 'adguard'
+
+DATA_ADGUARD_CLIENT = 'adguard_client'
+DATA_ADGUARD_VERION = 'adguard_version'
+
+CONF_FORCE = 'force'
+
+SERVICE_ADD_URL = 'add_url'
+SERVICE_DISABLE_URL = 'disable_url'
+SERVICE_ENABLE_URL = 'enable_url'
+SERVICE_REFRESH = 'refresh'
+SERVICE_REMOVE_URL = 'remove_url'
diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json
new file mode 100644
index 00000000000000..281a384e21fe94
--- /dev/null
+++ b/homeassistant/components/adguard/manifest.json
@@ -0,0 +1,13 @@
+{
+ "domain": "adguard",
+ "name": "AdGuard Home",
+ "config_flow": true,
+ "documentation": "https://www.home-assistant.io/components/adguard",
+ "requirements": [
+ "adguardhome==0.2.0"
+ ],
+ "dependencies": [],
+ "codeowners": [
+ "@frenck"
+ ]
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/sensor.py b/homeassistant/components/adguard/sensor.py
new file mode 100644
index 00000000000000..abb5309b449b8d
--- /dev/null
+++ b/homeassistant/components/adguard/sensor.py
@@ -0,0 +1,232 @@
+"""Support for AdGuard Home sensors."""
+from datetime import timedelta
+import logging
+
+from adguardhome import AdGuardHomeConnectionError
+
+from homeassistant.components.adguard import AdGuardHomeDeviceEntity
+from homeassistant.components.adguard.const import (
+ DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.exceptions import PlatformNotReady
+from homeassistant.helpers.typing import HomeAssistantType
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(seconds=300)
+PARALLEL_UPDATES = 4
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up AdGuard Home sensor based on a config entry."""
+ adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
+
+ try:
+ version = await adguard.version()
+ except AdGuardHomeConnectionError as exception:
+ raise PlatformNotReady from exception
+
+ hass.data[DOMAIN][DATA_ADGUARD_VERION] = version
+
+ sensors = [
+ AdGuardHomeDNSQueriesSensor(adguard),
+ AdGuardHomeBlockedFilteringSensor(adguard),
+ AdGuardHomePercentageBlockedSensor(adguard),
+ AdGuardHomeReplacedParentalSensor(adguard),
+ AdGuardHomeReplacedSafeBrowsingSensor(adguard),
+ AdGuardHomeReplacedSafeSearchSensor(adguard),
+ AdGuardHomeAverageProcessingTimeSensor(adguard),
+ AdGuardHomeRulesCountSensor(adguard),
+ ]
+
+ async_add_entities(sensors, True)
+
+
+class AdGuardHomeSensor(AdGuardHomeDeviceEntity):
+ """Defines a AdGuard Home sensor."""
+
+ def __init__(
+ self,
+ adguard,
+ name: str,
+ icon: str,
+ measurement: str,
+ unit_of_measurement: str,
+ ) -> None:
+ """Initialize AdGuard Home sensor."""
+ self._state = None
+ self._unit_of_measurement = unit_of_measurement
+ self.measurement = measurement
+
+ super().__init__(adguard, name, icon)
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this sensor."""
+ return '_'.join(
+ [
+ DOMAIN,
+ self.adguard.host,
+ str(self.adguard.port),
+ 'sensor',
+ self.measurement,
+ ]
+ )
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ @property
+ def unit_of_measurement(self) -> str:
+ """Return the unit this state is expressed in."""
+ return self._unit_of_measurement
+
+
+class AdGuardHomeDNSQueriesSensor(AdGuardHomeSensor):
+ """Defines a AdGuard Home DNS Queries sensor."""
+
+ def __init__(self, adguard):
+ """Initialize AdGuard Home sensor."""
+ super().__init__(
+ adguard,
+ 'AdGuard DNS Queries',
+ 'mdi:magnify',
+ 'dns_queries',
+ 'queries',
+ )
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.stats.dns_queries()
+
+
+class AdGuardHomeBlockedFilteringSensor(AdGuardHomeSensor):
+ """Defines a AdGuard Home blocked by filtering sensor."""
+
+ def __init__(self, adguard):
+ """Initialize AdGuard Home sensor."""
+ super().__init__(
+ adguard,
+ 'AdGuard DNS Queries Blocked',
+ 'mdi:magnify-close',
+ 'blocked_filtering',
+ 'queries',
+ )
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.stats.blocked_filtering()
+
+
+class AdGuardHomePercentageBlockedSensor(AdGuardHomeSensor):
+ """Defines a AdGuard Home blocked percentage sensor."""
+
+ def __init__(self, adguard):
+ """Initialize AdGuard Home sensor."""
+ super().__init__(
+ adguard,
+ 'AdGuard DNS Queries Blocked Ratio',
+ 'mdi:magnify-close',
+ 'blocked_percentage',
+ '%',
+ )
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ percentage = await self.adguard.stats.blocked_percentage()
+ self._state = "{:.2f}".format(percentage)
+
+
+class AdGuardHomeReplacedParentalSensor(AdGuardHomeSensor):
+ """Defines a AdGuard Home replaced by parental control sensor."""
+
+ def __init__(self, adguard):
+ """Initialize AdGuard Home sensor."""
+ super().__init__(
+ adguard,
+ 'AdGuard Parental Control Blocked',
+ 'mdi:human-male-girl',
+ 'blocked_parental',
+ 'requests',
+ )
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.stats.replaced_parental()
+
+
+class AdGuardHomeReplacedSafeBrowsingSensor(AdGuardHomeSensor):
+ """Defines a AdGuard Home replaced by safe browsing sensor."""
+
+ def __init__(self, adguard):
+ """Initialize AdGuard Home sensor."""
+ super().__init__(
+ adguard,
+ 'AdGuard Safe Browsing Blocked',
+ 'mdi:shield-half-full',
+ 'blocked_safebrowsing',
+ 'requests',
+ )
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.stats.replaced_safebrowsing()
+
+
+class AdGuardHomeReplacedSafeSearchSensor(AdGuardHomeSensor):
+ """Defines a AdGuard Home replaced by safe search sensor."""
+
+ def __init__(self, adguard):
+ """Initialize AdGuard Home sensor."""
+ super().__init__(
+ adguard,
+ 'Searches Safe Search Enforced',
+ 'mdi:shield-search',
+ 'enforced_safesearch',
+ 'requests',
+ )
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.stats.replaced_safesearch()
+
+
+class AdGuardHomeAverageProcessingTimeSensor(AdGuardHomeSensor):
+ """Defines a AdGuard Home average processing time sensor."""
+
+ def __init__(self, adguard):
+ """Initialize AdGuard Home sensor."""
+ super().__init__(
+ adguard,
+ 'AdGuard Average Processing Speed',
+ 'mdi:speedometer',
+ 'average_speed',
+ 'ms',
+ )
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ average = await self.adguard.stats.avg_processing_time()
+ self._state = "{:.2f}".format(average)
+
+
+class AdGuardHomeRulesCountSensor(AdGuardHomeSensor):
+ """Defines a AdGuard Home rules count sensor."""
+
+ def __init__(self, adguard):
+ """Initialize AdGuard Home sensor."""
+ super().__init__(
+ adguard,
+ 'AdGuard Rules Count',
+ 'mdi:counter',
+ 'rules_count',
+ 'rules',
+ )
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.filtering.rules_count()
diff --git a/homeassistant/components/adguard/services.yaml b/homeassistant/components/adguard/services.yaml
new file mode 100644
index 00000000000000..736acdd923c853
--- /dev/null
+++ b/homeassistant/components/adguard/services.yaml
@@ -0,0 +1,37 @@
+add_url:
+ description: Add a new filter subscription to AdGuard Home.
+ fields:
+ name:
+ description: The name of the filter subscription.
+ example: Example
+ url:
+ description: The filter URL to subscribe to, containing the filter rules.
+ example: https://www.example.com/filter/1.txt
+
+remove_url:
+ description: Removes a filter subscription from AdGuard Home.
+ fields:
+ url:
+ description: The filter subscription URL to remove.
+ example: https://www.example.com/filter/1.txt
+
+enable_url:
+ description: Enables a filter subscription in AdGuard Home.
+ fields:
+ url:
+ description: The filter subscription URL to enable.
+ example: https://www.example.com/filter/1.txt
+
+disable_url:
+ description: Disables a filter subscription in AdGuard Home.
+ fields:
+ url:
+ description: The filter subscription URL to disable.
+ example: https://www.example.com/filter/1.txt
+
+refresh:
+ description: Refresh all filter subscriptions in AdGuard Home.
+ fields:
+ force:
+ description: Force update (by passes AdGuard Home throttling).
+ example: '"true" to force, "false" or omit for a regular refresh.'
diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json
new file mode 100644
index 00000000000000..c88f7085e341c0
--- /dev/null
+++ b/homeassistant/components/adguard/strings.json
@@ -0,0 +1,29 @@
+{
+ "config": {
+ "title": "AdGuard Home",
+ "step": {
+ "user": {
+ "title": "Link your AdGuard Home.",
+ "description": "Set up your AdGuard Home instance to allow monitoring and control.",
+ "data": {
+ "host": "Host",
+ "password": "Password",
+ "port": "Port",
+ "username": "Username",
+ "ssl": "AdGuard Home uses a SSL certificate",
+ "verify_ssl": "AdGuard Home uses a proper certificate"
+ }
+ },
+ "hassio_confirm": {
+ "title": "AdGuard Home via Hass.io add-on",
+ "description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the Hass.io add-on: {addon}?"
+ }
+ },
+ "error": {
+ "connection_error": "Failed to connect."
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py
new file mode 100644
index 00000000000000..601bf25b5b06e4
--- /dev/null
+++ b/homeassistant/components/adguard/switch.py
@@ -0,0 +1,233 @@
+"""Support for AdGuard Home switches."""
+from datetime import timedelta
+import logging
+
+from adguardhome import AdGuardHomeConnectionError, AdGuardHomeError
+
+from homeassistant.components.adguard import AdGuardHomeDeviceEntity
+from homeassistant.components.adguard.const import (
+ DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.exceptions import PlatformNotReady
+from homeassistant.helpers.entity import ToggleEntity
+from homeassistant.helpers.typing import HomeAssistantType
+
+_LOGGER = logging.getLogger(__name__)
+
+SCAN_INTERVAL = timedelta(seconds=10)
+PARALLEL_UPDATES = 1
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType, entry: ConfigEntry, async_add_entities
+) -> None:
+ """Set up AdGuard Home switch based on a config entry."""
+ adguard = hass.data[DOMAIN][DATA_ADGUARD_CLIENT]
+
+ try:
+ version = await adguard.version()
+ except AdGuardHomeConnectionError as exception:
+ raise PlatformNotReady from exception
+
+ hass.data[DOMAIN][DATA_ADGUARD_VERION] = version
+
+ switches = [
+ AdGuardHomeProtectionSwitch(adguard),
+ AdGuardHomeFilteringSwitch(adguard),
+ AdGuardHomeParentalSwitch(adguard),
+ AdGuardHomeSafeBrowsingSwitch(adguard),
+ AdGuardHomeSafeSearchSwitch(adguard),
+ AdGuardHomeQueryLogSwitch(adguard),
+ ]
+ async_add_entities(switches, True)
+
+
+class AdGuardHomeSwitch(ToggleEntity, AdGuardHomeDeviceEntity):
+ """Defines a AdGuard Home switch."""
+
+ def __init__(self, adguard, name: str, icon: str, key: str):
+ """Initialize AdGuard Home switch."""
+ self._state = False
+ self._key = key
+ super().__init__(adguard, name, icon)
+
+ @property
+ def unique_id(self) -> str:
+ """Return the unique ID for this sensor."""
+ return '_'.join(
+ [
+ DOMAIN,
+ self.adguard.host,
+ str(self.adguard.port),
+ 'switch',
+ self._key,
+ ]
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the switch."""
+ return self._state
+
+ async def async_turn_off(self, **kwargs) -> None:
+ """Turn off the switch."""
+ try:
+ await self._adguard_turn_off()
+ except AdGuardHomeError:
+ _LOGGER.error(
+ "An error occurred while turning off AdGuard Home switch."
+ )
+ self._available = False
+
+ async def _adguard_turn_off(self) -> None:
+ """Turn off the switch."""
+ raise NotImplementedError()
+
+ async def async_turn_on(self, **kwargs) -> None:
+ """Turn on the switch."""
+ try:
+ await self._adguard_turn_on()
+ except AdGuardHomeError:
+ _LOGGER.error(
+ "An error occurred while turning on AdGuard Home switch."
+ )
+ self._available = False
+
+ async def _adguard_turn_on(self) -> None:
+ """Turn on the switch."""
+ raise NotImplementedError()
+
+
+class AdGuardHomeProtectionSwitch(AdGuardHomeSwitch):
+ """Defines a AdGuard Home protection switch."""
+
+ def __init__(self, adguard) -> None:
+ """Initialize AdGuard Home switch."""
+ super().__init__(
+ adguard, "AdGuard Protection", 'mdi:shield-check', 'protection'
+ )
+
+ async def _adguard_turn_off(self) -> None:
+ """Turn off the switch."""
+ await self.adguard.disable_protection()
+
+ async def _adguard_turn_on(self) -> None:
+ """Turn on the switch."""
+ await self.adguard.enable_protection()
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.protection_enabled()
+
+
+class AdGuardHomeParentalSwitch(AdGuardHomeSwitch):
+ """Defines a AdGuard Home parental control switch."""
+
+ def __init__(self, adguard) -> None:
+ """Initialize AdGuard Home switch."""
+ super().__init__(
+ adguard, "AdGuard Parental Control", 'mdi:shield-check', 'parental'
+ )
+
+ async def _adguard_turn_off(self) -> None:
+ """Turn off the switch."""
+ await self.adguard.parental.disable()
+
+ async def _adguard_turn_on(self) -> None:
+ """Turn on the switch."""
+ await self.adguard.parental.enable()
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.parental.enabled()
+
+
+class AdGuardHomeSafeSearchSwitch(AdGuardHomeSwitch):
+ """Defines a AdGuard Home safe search switch."""
+
+ def __init__(self, adguard) -> None:
+ """Initialize AdGuard Home switch."""
+ super().__init__(
+ adguard, "AdGuard Safe Search", 'mdi:shield-check', 'safesearch'
+ )
+
+ async def _adguard_turn_off(self) -> None:
+ """Turn off the switch."""
+ await self.adguard.safesearch.disable()
+
+ async def _adguard_turn_on(self) -> None:
+ """Turn on the switch."""
+ await self.adguard.safesearch.enable()
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.safesearch.enabled()
+
+
+class AdGuardHomeSafeBrowsingSwitch(AdGuardHomeSwitch):
+ """Defines a AdGuard Home safe search switch."""
+
+ def __init__(self, adguard) -> None:
+ """Initialize AdGuard Home switch."""
+ super().__init__(
+ adguard,
+ "AdGuard Safe Browsing",
+ 'mdi:shield-check',
+ 'safebrowsing',
+ )
+
+ async def _adguard_turn_off(self) -> None:
+ """Turn off the switch."""
+ await self.adguard.safebrowsing.disable()
+
+ async def _adguard_turn_on(self) -> None:
+ """Turn on the switch."""
+ await self.adguard.safebrowsing.enable()
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.safebrowsing.enabled()
+
+
+class AdGuardHomeFilteringSwitch(AdGuardHomeSwitch):
+ """Defines a AdGuard Home filtering switch."""
+
+ def __init__(self, adguard) -> None:
+ """Initialize AdGuard Home switch."""
+ super().__init__(
+ adguard, "AdGuard Filtering", 'mdi:shield-check', 'filtering'
+ )
+
+ async def _adguard_turn_off(self) -> None:
+ """Turn off the switch."""
+ await self.adguard.filtering.disable()
+
+ async def _adguard_turn_on(self) -> None:
+ """Turn on the switch."""
+ await self.adguard.filtering.enable()
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.filtering.enabled()
+
+
+class AdGuardHomeQueryLogSwitch(AdGuardHomeSwitch):
+ """Defines a AdGuard Home query log switch."""
+
+ def __init__(self, adguard) -> None:
+ """Initialize AdGuard Home switch."""
+ super().__init__(
+ adguard, "AdGuard Query Log", 'mdi:shield-check', 'querylog'
+ )
+
+ async def _adguard_turn_off(self) -> None:
+ """Turn off the switch."""
+ await self.adguard.querylog.disable()
+
+ async def _adguard_turn_on(self) -> None:
+ """Turn on the switch."""
+ await self.adguard.querylog.enable()
+
+ async def _adguard_update(self) -> None:
+ """Update AdGuard Home entity."""
+ self._state = await self.adguard.querylog.enabled()
diff --git a/homeassistant/components/ambiclimate/.translations/fr.json b/homeassistant/components/ambiclimate/.translations/fr.json
new file mode 100644
index 00000000000000..6d09fd6ee05424
--- /dev/null
+++ b/homeassistant/components/ambiclimate/.translations/fr.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.",
+ "already_setup": "Le compte Ambiclimate est configur\u00e9.",
+ "no_config": "Vous devez configurer Ambiclimate avant de pouvoir vous authentifier aupr\u00e8s de celui-ci. [Veuillez lire les instructions] (https://www.home-assistant.io/components/ambiclimate/)."
+ },
+ "create_entry": {
+ "default": "Authentifi\u00e9 avec succ\u00e8s avec Ambiclimate"
+ },
+ "error": {
+ "follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre.",
+ "no_token": "Non authentifi\u00e9 avec Ambiclimate"
+ },
+ "step": {
+ "auth": {
+ "description": "Suivez ce [lien] ( {authorization_url} ) et Autorisez l'acc\u00e8s \u00e0 votre compte Ambiclimate, puis revenez et appuyez sur Envoyer ci-dessous. \n (Assurez-vous que l'URL de rappel sp\u00e9cifi\u00e9 est {cb_url} )",
+ "title": "Authentifier Ambiclimate"
+ }
+ },
+ "title": "Ambiclimate"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/.translations/pt-BR.json b/homeassistant/components/ambiclimate/.translations/pt-BR.json
new file mode 100644
index 00000000000000..4de4190d0558c2
--- /dev/null
+++ b/homeassistant/components/ambiclimate/.translations/pt-BR.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "access_token": "Erro desconhecido ao gerar um token de acesso.",
+ "already_setup": "A conta Ambiclimate est\u00e1 configurada.",
+ "no_config": "Voc\u00ea precisa configurar o Ambiclimate antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/ambiclimate/)."
+ },
+ "create_entry": {
+ "default": "Autenticado com sucesso no Ambiclimate"
+ },
+ "error": {
+ "follow_link": "Por favor, siga o link e autentique-se antes de pressionar Enviar",
+ "no_token": "N\u00e3o autenticado com o Ambiclimate"
+ },
+ "step": {
+ "auth": {
+ "description": "Por favor, siga este [link]({authorization_url}) e Permitir acesso \u00e0 sua conta Ambiclimate, em seguida, volte e pressione Enviar abaixo. \n (Verifique se a URL de retorno de chamada especificada \u00e9 {cb_url})",
+ "title": "Autenticar Ambiclimate"
+ }
+ },
+ "title": "Ambiclimate"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ambiclimate/.translations/sl.json b/homeassistant/components/ambiclimate/.translations/sl.json
new file mode 100644
index 00000000000000..cae2e940d561b6
--- /dev/null
+++ b/homeassistant/components/ambiclimate/.translations/sl.json
@@ -0,0 +1,23 @@
+{
+ "config": {
+ "abort": {
+ "access_token": "Neznana napaka pri ustvarjanju \u017eetona za dostop.",
+ "already_setup": "Ra\u010dun Ambiclimate je konfiguriran.",
+ "no_config": "Ambiclimat morate konfigurirati, preden lahko z njo preverjate pristnost. [Preberite navodila] (https://www.home-assistant.io/components/ambiclimate/)."
+ },
+ "create_entry": {
+ "default": "Uspe\u0161no overjeno z funkcijo Ambiclimate"
+ },
+ "error": {
+ "follow_link": "Preden pritisnete Po\u0161lji, sledite povezavi in preverite pristnost",
+ "no_token": "Ni overjeno z Ambiclimate"
+ },
+ "step": {
+ "auth": {
+ "description": "Sledite temu povezavi ( {authorization_url} in Dovoli dostopu do svojega ra\u010duna Ambiclimate, nato se vrnite in pritisnite Po\u0161lji spodaj. \n (Poskrbite, da je dolo\u010den url za povratni klic {cb_url} )",
+ "title": "Overi Ambiclimate"
+ }
+ },
+ "title": "Ambiclimate"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py
index 6de31caa90e325..58df1d8e504763 100644
--- a/homeassistant/components/amcrest/__init__.py
+++ b/homeassistant/components/amcrest/__init__.py
@@ -3,6 +3,7 @@
from datetime import timedelta
import aiohttp
+from amcrest import AmcrestCamera, AmcrestError
import voluptuous as vol
from homeassistant.auth.permissions.const import POLICY_CONTROL
@@ -32,6 +33,7 @@
CONF_RESOLUTION = 'resolution'
CONF_STREAM_SOURCE = 'stream_source'
CONF_FFMPEG_ARGUMENTS = 'ffmpeg_arguments'
+CONF_CONTROL_LIGHT = 'control_light'
DEFAULT_NAME = 'Amcrest Camera'
DEFAULT_PORT = 80
@@ -103,6 +105,7 @@ def _has_unique_names(devices):
_deprecated_sensor_values),
vol.Optional(CONF_SWITCHES):
vol.All(cv.ensure_list, [vol.In(SWITCHES)]),
+ vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean,
}),
_deprecated_switches
)
@@ -114,8 +117,6 @@ def _has_unique_names(devices):
def setup(hass, config):
"""Set up the Amcrest IP Camera component."""
- from amcrest import AmcrestCamera, AmcrestError
-
hass.data.setdefault(DATA_AMCREST, {'devices': {}, 'cameras': []})
devices = config[DOMAIN]
@@ -149,6 +150,7 @@ def setup(hass, config):
sensors = device.get(CONF_SENSORS)
switches = device.get(CONF_SWITCHES)
stream_source = device[CONF_STREAM_SOURCE]
+ control_light = device.get(CONF_CONTROL_LIGHT)
# currently aiohttp only works with basic authentication
# only valid for mjpeg streaming
@@ -159,7 +161,7 @@ def setup(hass, config):
hass.data[DATA_AMCREST]['devices'][name] = AmcrestDevice(
api, authentication, ffmpeg_arguments, stream_source,
- resolution)
+ resolution, control_light)
discovery.load_platform(
hass, CAMERA, DOMAIN, {
@@ -245,10 +247,11 @@ class AmcrestDevice:
"""Representation of a base Amcrest discovery device."""
def __init__(self, api, authentication, ffmpeg_arguments,
- stream_source, resolution):
+ stream_source, resolution, control_light):
"""Initialize the entity."""
self.api = api
self.authentication = authentication
self.ffmpeg_arguments = ffmpeg_arguments
self.stream_source = stream_source
self.resolution = resolution
+ self.control_light = control_light
diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py
index 0eb9e42e707dd7..fe4eb25b3db152 100644
--- a/homeassistant/components/amcrest/binary_sensor.py
+++ b/homeassistant/components/amcrest/binary_sensor.py
@@ -2,6 +2,8 @@
from datetime import timedelta
import logging
+from amcrest import AmcrestError
+
from homeassistant.components.binary_sensor import (
BinarySensorDevice, DEVICE_CLASS_MOTION)
from homeassistant.const import CONF_NAME, CONF_BINARY_SENSORS
@@ -58,8 +60,6 @@ def device_class(self):
def update(self):
"""Update entity."""
- from amcrest import AmcrestError
-
_LOGGER.debug('Pulling data from %s binary sensor', self._name)
try:
diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py
index d75475dbb26c6e..3b8c8f38f8bc57 100644
--- a/homeassistant/components/amcrest/camera.py
+++ b/homeassistant/components/amcrest/camera.py
@@ -2,6 +2,7 @@
import asyncio
import logging
+from amcrest import AmcrestError
import voluptuous as vol
from homeassistant.components.camera import (
@@ -94,19 +95,20 @@ def __init__(self, name, device, ffmpeg):
self._stream_source = device.stream_source
self._resolution = device.resolution
self._token = self._auth = device.authentication
+ self._control_light = device.control_light
self._is_recording = False
self._motion_detection_enabled = None
+ self._brand = None
self._model = None
self._audio_enabled = None
self._motion_recording_enabled = None
self._color_bw = None
+ self._rtsp_url = None
self._snapshot_lock = asyncio.Lock()
self._unsub_dispatcher = []
async def async_camera_image(self):
"""Return a still image response from the camera."""
- from amcrest import AmcrestError
-
if not self.is_on:
_LOGGER.error(
'Attempt to take snaphot when %s camera is off', self.name)
@@ -143,7 +145,7 @@ async def handle_async_mjpeg_stream(self, request):
# streaming via ffmpeg
from haffmpeg.camera import CameraMjpeg
- streaming_url = self._api.rtsp_url(typeno=self._resolution)
+ streaming_url = self._rtsp_url
stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop)
await stream.open_camera(
streaming_url, extra_cmd=self._ffmpeg_arguments)
@@ -191,7 +193,7 @@ def is_recording(self):
@property
def brand(self):
"""Return the camera brand."""
- return 'Amcrest'
+ return self._brand
@property
def motion_detection_enabled(self):
@@ -205,7 +207,7 @@ def model(self):
async def stream_source(self):
"""Return the source of the stream."""
- return self._api.rtsp_url(typeno=self._resolution)
+ return self._rtsp_url
@property
def is_on(self):
@@ -231,9 +233,19 @@ async def async_will_remove_from_hass(self):
def update(self):
"""Update entity status."""
- from amcrest import AmcrestError
-
_LOGGER.debug('Pulling data from %s camera', self.name)
+ if self._brand is None:
+ try:
+ resp = self._api.vendor_information.strip()
+ if resp.startswith('vendor='):
+ self._brand = resp.split('=')[-1]
+ else:
+ self._brand = 'unknown'
+ except AmcrestError as error:
+ _LOGGER.error(
+ 'Could not get %s camera brand due to error: %s',
+ self.name, error)
+ self._brand = 'unknwown'
if self._model is None:
try:
self._model = self._api.device_type.split('=')[-1].strip()
@@ -241,7 +253,7 @@ def update(self):
_LOGGER.error(
'Could not get %s camera model due to error: %s',
self.name, error)
- self._model = ''
+ self._model = 'unknown'
try:
self.is_streaming = self._api.video_enabled
self._is_recording = self._api.record_mode == 'Manual'
@@ -251,6 +263,7 @@ def update(self):
self._motion_recording_enabled = (
self._api.is_record_on_motion_detection())
self._color_bw = _CBW[self._api.day_night_color]
+ self._rtsp_url = self._api.rtsp_url(typeno=self._resolution)
except AmcrestError as error:
_LOGGER.error(
'Could not get %s camera attributes due to error: %s',
@@ -322,8 +335,6 @@ async def async_stop_tour(self):
def _enable_video_stream(self, enable):
"""Enable or disable camera video stream."""
- from amcrest import AmcrestError
-
# Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave
# recording on if video stream is being turned off.
@@ -338,11 +349,11 @@ def _enable_video_stream(self, enable):
else:
self.is_streaming = enable
self.schedule_update_ha_state()
+ if self._control_light:
+ self._enable_light(self._audio_enabled or self.is_streaming)
def _enable_recording(self, enable):
"""Turn recording on or off."""
- from amcrest import AmcrestError
-
# Given the way the camera's state is determined by
# is_streaming and is_recording, we can't leave
# video stream off if recording is being turned on.
@@ -362,8 +373,6 @@ def _enable_recording(self, enable):
def _enable_motion_detection(self, enable):
"""Enable or disable motion detection."""
- from amcrest import AmcrestError
-
try:
self._api.motion_detection = str(enable).lower()
except AmcrestError as error:
@@ -376,8 +385,6 @@ def _enable_motion_detection(self, enable):
def _enable_audio(self, enable):
"""Enable or disable audio stream."""
- from amcrest import AmcrestError
-
try:
self._api.audio_enabled = enable
except AmcrestError as error:
@@ -387,11 +394,22 @@ def _enable_audio(self, enable):
else:
self._audio_enabled = enable
self.schedule_update_ha_state()
+ if self._control_light:
+ self._enable_light(self._audio_enabled or self.is_streaming)
+
+ def _enable_light(self, enable):
+ """Enable or disable indicator light."""
+ try:
+ self._api.command(
+ 'configManager.cgi?action=setConfig&LightGlobal[0].Enable={}'
+ .format(str(enable).lower()))
+ except AmcrestError as error:
+ _LOGGER.error(
+ 'Could not %s %s camera indicator light due to error: %s',
+ 'enable' if enable else 'disable', self.name, error)
def _enable_motion_recording(self, enable):
"""Enable or disable motion recording."""
- from amcrest import AmcrestError
-
try:
self._api.motion_recording = str(enable).lower()
except AmcrestError as error:
@@ -404,8 +422,6 @@ def _enable_motion_recording(self, enable):
def _goto_preset(self, preset):
"""Move camera position and zoom to preset."""
- from amcrest import AmcrestError
-
try:
self._api.go_to_preset(
action='start', preset_point_number=preset)
@@ -416,8 +432,6 @@ def _goto_preset(self, preset):
def _set_color_bw(self, cbw):
"""Set camera color mode."""
- from amcrest import AmcrestError
-
try:
self._api.day_night_color = _CBW.index(cbw)
except AmcrestError as error:
@@ -430,8 +444,6 @@ def _set_color_bw(self, cbw):
def _start_tour(self, start):
"""Start camera tour."""
- from amcrest import AmcrestError
-
try:
self._api.tour(start=start)
except AmcrestError as error:
diff --git a/homeassistant/components/awair/manifest.json b/homeassistant/components/awair/manifest.json
index cba11e8be1ca00..dfa5bec3c00309 100644
--- a/homeassistant/components/awair/manifest.json
+++ b/homeassistant/components/awair/manifest.json
@@ -6,5 +6,7 @@
"python_awair==0.0.4"
],
"dependencies": [],
- "codeowners": []
+ "codeowners": [
+ "@danielsjf"
+ ]
}
diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py
index 85f18e87d13f86..71b74c7971e404 100644
--- a/homeassistant/components/awair/sensor.py
+++ b/homeassistant/components/awair/sensor.py
@@ -219,6 +219,6 @@ async def _async_update(self):
# The air_data_latest call only returns one item, so this should
# be safe to only process one entry.
for sensor in resp[0][ATTR_SENSORS]:
- self.data[sensor[ATTR_COMPONENT]] = sensor[ATTR_VALUE]
+ self.data[sensor[ATTR_COMPONENT]] = round(sensor[ATTR_VALUE], 1)
_LOGGER.debug("Got Awair Data for %s: %s", self._uuid, self.data)
diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json
index 5e98dbf34189d1..e55d23b2a910ab 100644
--- a/homeassistant/components/axis/.translations/ca.json
+++ b/homeassistant/components/axis/.translations/ca.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "El dispositiu ja est\u00e0 configurat",
+ "already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.",
"device_unavailable": "El dispositiu no est\u00e0 disponible",
"faulty_credentials": "Credencials d'usuari incorrectes"
},
diff --git a/homeassistant/components/axis/.translations/de.json b/homeassistant/components/axis/.translations/de.json
index c979068b922229..123b0621424ac3 100644
--- a/homeassistant/components/axis/.translations/de.json
+++ b/homeassistant/components/axis/.translations/de.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "Ger\u00e4t ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.",
"device_unavailable": "Ger\u00e4t ist nicht verf\u00fcgbar",
"faulty_credentials": "Ung\u00fcltige Anmeldeinformationen"
},
diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json
index 6c5933dfd97263..5bf0e31b0b22e3 100644
--- a/homeassistant/components/axis/.translations/en.json
+++ b/homeassistant/components/axis/.translations/en.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "Device is already configured",
+ "already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials"
},
diff --git a/homeassistant/components/axis/.translations/hu.json b/homeassistant/components/axis/.translations/hu.json
index cbf055e2fba477..b0c8051e69f9fa 100644
--- a/homeassistant/components/axis/.translations/hu.json
+++ b/homeassistant/components/axis/.translations/hu.json
@@ -1,9 +1,17 @@
{
"config": {
+ "error": {
+ "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk",
+ "device_unavailable": "Az eszk\u00f6z nem \u00e9rhet\u0151 el",
+ "faulty_credentials": "Rossz felhaszn\u00e1l\u00f3i hiteles\u00edt\u0151 adatok"
+ },
"step": {
"user": {
"data": {
- "host": "Hoszt"
+ "host": "Hoszt",
+ "password": "Jelsz\u00f3",
+ "port": "Port",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
}
}
}
diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json
index aafa4fc18962e1..d16bd0f6e5e6f6 100644
--- a/homeassistant/components/axis/.translations/ko.json
+++ b/homeassistant/components/axis/.translations/ko.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.",
"device_unavailable": "\uae30\uae30\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"faulty_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4"
},
diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json
index 94b5a1680b7156..24cf845f9f0b57 100644
--- a/homeassistant/components/axis/.translations/no.json
+++ b/homeassistant/components/axis/.translations/no.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "Enheten er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
"device_unavailable": "Enheten er ikke tilgjengelig",
"faulty_credentials": "Ugyldig brukerlegitimasjon"
},
diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json
index 7903dc63bf8bca..9d8de4c4a7b1ba 100644
--- a/homeassistant/components/axis/.translations/pl.json
+++ b/homeassistant/components/axis/.translations/pl.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane",
+ "already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.",
"device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne",
"faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce"
},
diff --git a/homeassistant/components/axis/.translations/pt-BR.json b/homeassistant/components/axis/.translations/pt-BR.json
new file mode 100644
index 00000000000000..53b8079a1ea29a
--- /dev/null
+++ b/homeassistant/components/axis/.translations/pt-BR.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo j\u00e1 est\u00e1 em andamento."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json
index f303aa947ea8ba..dee7876fffcbf0 100644
--- a/homeassistant/components/axis/.translations/ru.json
+++ b/homeassistant/components/axis/.translations/ru.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430",
+ "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.",
"device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e",
"faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435"
},
diff --git a/homeassistant/components/axis/.translations/sv.json b/homeassistant/components/axis/.translations/sv.json
index 2f75a9dcfffa6d..d7f014c7800ba9 100644
--- a/homeassistant/components/axis/.translations/sv.json
+++ b/homeassistant/components/axis/.translations/sv.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "Enheten \u00e4r redan konfigurerad",
+ "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r enheten p\u00e5g\u00e5r redan.",
"device_unavailable": "Enheten \u00e4r inte tillg\u00e4nglig",
"faulty_credentials": "Felaktiga anv\u00e4ndaruppgifter"
},
diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json
index ac9f3ceb2b696f..7b93d2f7243ec8 100644
--- a/homeassistant/components/axis/.translations/zh-Hant.json
+++ b/homeassistant/components/axis/.translations/zh-Hant.json
@@ -7,6 +7,7 @@
},
"error": {
"already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u88dd\u7f6e\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
"device_unavailable": "\u88dd\u7f6e\u7121\u6cd5\u4f7f\u7528",
"faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548"
},
diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py
index fc2051e49257fd..2aa5c4de16e147 100644
--- a/homeassistant/components/axis/config_flow.py
+++ b/homeassistant/components/axis/config_flow.py
@@ -155,6 +155,13 @@ async def async_step_zeroconf(self, discovery_info):
return self.async_abort(reason='link_local_address')
serialnumber = discovery_info['properties']['macaddress']
+ # pylint: disable=unsupported-assignment-operation
+ self.context['macaddress'] = serialnumber
+
+ if any(serialnumber == flow['context']['macaddress']
+ for flow in self._async_in_progress()):
+ return self.async_abort(reason='already_in_progress')
+
device_entries = configured_devices(self.hass)
if serialnumber in device_entries:
diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json
index 27c108b334cbb3..dc64e90ba9a51a 100644
--- a/homeassistant/components/axis/manifest.json
+++ b/homeassistant/components/axis/manifest.json
@@ -3,7 +3,7 @@
"name": "Axis",
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/axis",
- "requirements": ["axis==23"],
+ "requirements": ["axis==24"],
"dependencies": [],
"zeroconf": ["_axis-video._tcp.local."],
"codeowners": ["@kane610"]
diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json
index 3c528dfbb16112..ebefbecf311284 100644
--- a/homeassistant/components/axis/strings.json
+++ b/homeassistant/components/axis/strings.json
@@ -14,6 +14,7 @@
},
"error": {
"already_configured": "Device is already configured",
+ "already_in_progress": "Config flow for device is already in progress.",
"device_unavailable": "Device is not available",
"faulty_credentials": "Bad user credentials"
},
diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json
index 125a3a83d21b22..b730413d0ce050 100644
--- a/homeassistant/components/broadlink/manifest.json
+++ b/homeassistant/components/broadlink/manifest.json
@@ -3,7 +3,7 @@
"name": "Broadlink",
"documentation": "https://www.home-assistant.io/components/broadlink",
"requirements": [
- "broadlink==0.10.0"
+ "broadlink==0.11.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py
index 73a779816a3d1c..5a1ce79c18cce1 100644
--- a/homeassistant/components/calendar/__init__.py
+++ b/homeassistant/components/calendar/__init__.py
@@ -36,7 +36,7 @@ async def async_setup(hass, config):
hass.http.register_view(CalendarEventView(component))
# Doesn't work in prod builds of the frontend: home-assistant-polymer#1289
- # await hass.components.frontend.async_register_built_in_panel(
+ # hass.components.frontend.async_register_built_in_panel(
# 'calendar', 'calendar', 'hass:calendar')
await component.async_setup(config)
diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json
index 2d310cdda8f2e6..5699f8764cd1aa 100644
--- a/homeassistant/components/cast/manifest.json
+++ b/homeassistant/components/cast/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/cast",
"requirements": [
- "pychromecast==3.2.1"
+ "pychromecast==3.2.2"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py
index f47eae74986afe..eadb1731bd021a 100644
--- a/homeassistant/components/cloud/client.py
+++ b/homeassistant/components/cloud/client.py
@@ -17,7 +17,9 @@
from . import utils
from .const import (
- CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE)
+ CONF_ENTITY_CONFIG, CONF_FILTER, DOMAIN, DISPATCHER_REMOTE_UPDATE,
+ PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE,
+ PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
from .prefs import CloudPreferences
@@ -98,12 +100,26 @@ def should_expose(entity):
if entity.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
return False
- return google_conf['filter'](entity.entity_id)
+ if not google_conf['filter'].empty_filter:
+ return google_conf['filter'](entity.entity_id)
+
+ entity_configs = self.prefs.google_entity_configs
+ entity_config = entity_configs.get(entity.entity_id, {})
+ return entity_config.get(
+ PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
+
+ def should_2fa(entity):
+ """If an entity should be checked for 2FA."""
+ entity_configs = self.prefs.google_entity_configs
+ entity_config = entity_configs.get(entity.entity_id, {})
+ return not entity_config.get(
+ PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
username = self._hass.data[DOMAIN].claims["cognito:username"]
self._google_config = ga_h.Config(
should_expose=should_expose,
+ should_2fa=should_2fa,
secure_devices_pin=self._prefs.google_secure_devices_pin,
entity_config=google_conf.get(CONF_ENTITY_CONFIG),
agent_user_id=username,
diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py
index 5002286edb9376..e2f4b9c078513b 100644
--- a/homeassistant/components/cloud/const.py
+++ b/homeassistant/components/cloud/const.py
@@ -8,6 +8,13 @@
PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin'
PREF_CLOUDHOOKS = 'cloudhooks'
PREF_CLOUD_USER = 'cloud_user'
+PREF_GOOGLE_ENTITY_CONFIGS = 'google_entity_configs'
+PREF_OVERRIDE_NAME = 'override_name'
+PREF_DISABLE_2FA = 'disable_2fa'
+PREF_ALIASES = 'aliases'
+PREF_SHOULD_EXPOSE = 'should_expose'
+DEFAULT_SHOULD_EXPOSE = True
+DEFAULT_DISABLE_2FA = False
CONF_ALEXA = 'alexa'
CONF_ALIASES = 'aliases'
diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py
index 40d19c198becc2..e6151a917afa59 100644
--- a/homeassistant/components/cloud/http_api.py
+++ b/homeassistant/components/cloud/http_api.py
@@ -14,8 +14,7 @@
RequestDataValidator)
from homeassistant.components import websocket_api
from homeassistant.components.alexa import smart_home as alexa_sh
-from homeassistant.components.google_assistant import (
- const as google_const)
+from homeassistant.components.google_assistant import helpers as google_helpers
from .const import (
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
@@ -81,6 +80,12 @@ async def async_setup(hass):
websocket_remote_connect)
hass.components.websocket_api.async_register_command(
websocket_remote_disconnect)
+
+ hass.components.websocket_api.async_register_command(
+ google_assistant_list)
+ hass.components.websocket_api.async_register_command(
+ google_assistant_update)
+
hass.http.register_view(GoogleActionsSyncView)
hass.http.register_view(CloudLoginView)
hass.http.register_view(CloudLogoutView)
@@ -411,7 +416,6 @@ def _account_data(cloud):
'cloud': cloud.iot.state,
'prefs': client.prefs.as_dict(),
'google_entities': client.google_user_config['filter'].config,
- 'google_domains': list(google_const.DOMAIN_TO_GOOGLE_TYPES),
'alexa_entities': client.alexa_config.should_expose.config,
'alexa_domains': list(alexa_sh.ENTITY_ADAPTERS),
'remote_domain': remote.instance_domain,
@@ -448,3 +452,55 @@ async def websocket_remote_disconnect(hass, connection, msg):
await cloud.client.prefs.async_update(remote_enabled=False)
await cloud.remote.disconnect()
connection.send_result(msg['id'], _account_data(cloud))
+
+
+@websocket_api.require_admin
+@_require_cloud_login
+@websocket_api.async_response
+@_ws_handle_cloud_errors
+@websocket_api.websocket_command({
+ 'type': 'cloud/google_assistant/entities'
+})
+async def google_assistant_list(hass, connection, msg):
+ """List all google assistant entities."""
+ cloud = hass.data[DOMAIN]
+ entities = google_helpers.async_get_entities(
+ hass, cloud.client.google_config
+ )
+
+ result = []
+
+ for entity in entities:
+ result.append({
+ 'entity_id': entity.entity_id,
+ 'traits': [trait.name for trait in entity.traits()],
+ 'might_2fa': entity.might_2fa(),
+ })
+
+ connection.send_result(msg['id'], result)
+
+
+@websocket_api.require_admin
+@_require_cloud_login
+@websocket_api.async_response
+@_ws_handle_cloud_errors
+@websocket_api.websocket_command({
+ 'type': 'cloud/google_assistant/entities/update',
+ 'entity_id': str,
+ vol.Optional('should_expose'): bool,
+ vol.Optional('override_name'): str,
+ vol.Optional('aliases'): [str],
+ vol.Optional('disable_2fa'): bool,
+})
+async def google_assistant_update(hass, connection, msg):
+ """List all google assistant entities."""
+ cloud = hass.data[DOMAIN]
+ changes = dict(msg)
+ changes.pop('type')
+ changes.pop('id')
+
+ await cloud.client.prefs.async_update_google_entity_config(**changes)
+
+ connection.send_result(
+ msg['id'],
+ cloud.client.prefs.google_entity_configs.get(msg['entity_id']))
diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json
index 863e3e86da4130..982b51133a51cc 100644
--- a/homeassistant/components/cloud/manifest.json
+++ b/homeassistant/components/cloud/manifest.json
@@ -3,7 +3,7 @@
"name": "Cloud",
"documentation": "https://www.home-assistant.io/components/cloud",
"requirements": [
- "hass-nabucasa==0.12"
+ "hass-nabucasa==0.13"
],
"dependencies": [
"http",
diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py
index 0e2abae15b0b73..0f45f25c49bb9a 100644
--- a/homeassistant/components/cloud/prefs.py
+++ b/homeassistant/components/cloud/prefs.py
@@ -4,6 +4,8 @@
from .const import (
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
+ PREF_GOOGLE_ENTITY_CONFIGS, PREF_OVERRIDE_NAME, PREF_DISABLE_2FA,
+ PREF_ALIASES, PREF_SHOULD_EXPOSE,
InvalidTrustedNetworks)
STORAGE_KEY = DOMAIN
@@ -30,6 +32,7 @@ async def async_initialize(self):
PREF_ENABLE_GOOGLE: True,
PREF_ENABLE_REMOTE: False,
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
+ PREF_GOOGLE_ENTITY_CONFIGS: {},
PREF_CLOUDHOOKS: {},
PREF_CLOUD_USER: None,
}
@@ -39,7 +42,7 @@ async def async_initialize(self):
async def async_update(self, *, google_enabled=_UNDEF,
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
- cloud_user=_UNDEF):
+ cloud_user=_UNDEF, google_entity_configs=_UNDEF):
"""Update user preferences."""
for key, value in (
(PREF_ENABLE_GOOGLE, google_enabled),
@@ -48,6 +51,7 @@ async def async_update(self, *, google_enabled=_UNDEF,
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
(PREF_CLOUDHOOKS, cloudhooks),
(PREF_CLOUD_USER, cloud_user),
+ (PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
):
if value is not _UNDEF:
self._prefs[key] = value
@@ -57,9 +61,48 @@ async def async_update(self, *, google_enabled=_UNDEF,
await self._store.async_save(self._prefs)
+ async def async_update_google_entity_config(
+ self, *, entity_id, override_name=_UNDEF, disable_2fa=_UNDEF,
+ aliases=_UNDEF, should_expose=_UNDEF):
+ """Update config for a Google entity."""
+ entities = self.google_entity_configs
+ entity = entities.get(entity_id, {})
+
+ changes = {}
+ for key, value in (
+ (PREF_OVERRIDE_NAME, override_name),
+ (PREF_DISABLE_2FA, disable_2fa),
+ (PREF_ALIASES, aliases),
+ (PREF_SHOULD_EXPOSE, should_expose),
+ ):
+ if value is not _UNDEF:
+ changes[key] = value
+
+ if not changes:
+ return
+
+ updated_entity = {
+ **entity,
+ **changes,
+ }
+
+ updated_entities = {
+ **entities,
+ entity_id: updated_entity,
+ }
+ await self.async_update(google_entity_configs=updated_entities)
+
def as_dict(self):
"""Return dictionary version."""
- return self._prefs
+ return {
+ PREF_ENABLE_ALEXA: self.alexa_enabled,
+ PREF_ENABLE_GOOGLE: self.google_enabled,
+ PREF_ENABLE_REMOTE: self.remote_enabled,
+ PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin,
+ PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
+ PREF_CLOUDHOOKS: self.cloudhooks,
+ PREF_CLOUD_USER: self.cloud_user,
+ }
@property
def remote_enabled(self):
@@ -89,6 +132,11 @@ def google_secure_devices_pin(self):
"""Return if Google is allowed to unlock locks."""
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
+ @property
+ def google_entity_configs(self):
+ """Return Google Entity configurations."""
+ return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
+
@property
def cloudhooks(self):
"""Return the published cloud webhooks."""
diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py
index 8cd8856c1ec194..0cb76cc8c3baf6 100644
--- a/homeassistant/components/config/__init__.py
+++ b/homeassistant/components/config/__init__.py
@@ -30,7 +30,7 @@
async def async_setup(hass, config):
"""Set up the config component."""
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'config', 'config', 'hass:settings', require_admin=True)
async def setup_panel(panel_name):
diff --git a/homeassistant/components/config/core.py b/homeassistant/components/config/core.py
index 31abb832f23d2d..a83516bdc3757f 100644
--- a/homeassistant/components/config/core.py
+++ b/homeassistant/components/config/core.py
@@ -56,7 +56,7 @@ async def websocket_update_config(hass, connection, msg):
data.pop('type')
try:
- await hass.config.update(**data)
+ await hass.config.async_update(**data)
connection.send_result(msg['id'])
except ValueError as err:
connection.send_error(
diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py
index 8609d3c9cf6402..4b05dedbf5e125 100644
--- a/homeassistant/components/cover/__init__.py
+++ b/homeassistant/components/cover/__init__.py
@@ -159,7 +159,7 @@ async def async_unload_entry(hass, entry):
class CoverDevice(Entity):
- """Representation a cover."""
+ """Representation of a cover."""
@property
def current_cover_position(self):
diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json
index 5f1ae46b48e882..7b69b7477f59c2 100644
--- a/homeassistant/components/deconz/.translations/ca.json
+++ b/homeassistant/components/deconz/.translations/ca.json
@@ -2,7 +2,9 @@
"config": {
"abort": {
"already_configured": "L'enlla\u00e7 ja est\u00e0 configurat",
+ "already_in_progress": "El flux de dades de configuraci\u00f3 per l'enlla\u00e7 ja est\u00e0 en curs.",
"no_bridges": "No s'han descobert enlla\u00e7os amb deCONZ",
+ "not_deconz_bridge": "No \u00e9s un enlla\u00e7 deCONZ",
"one_instance_only": "El component nom\u00e9s admet una inst\u00e0ncia deCONZ",
"updated_instance": "S'ha actualitzat la inst\u00e0ncia de deCONZ amb una nova adre\u00e7a"
},
@@ -15,7 +17,7 @@
"allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals",
"allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ"
},
- "description": "Vols configurar Home Assistant per a que es connecti amb la passarel\u00b7la deCONZ proporcionada per l\u2019add-on {addon} de hass.io?",
+ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement de Hass.io: {addon}?",
"title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee (complement de Hass.io)"
},
"init": {
diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json
index 981f579f09f44c..dd8f1cc4026edb 100644
--- a/homeassistant/components/deconz/.translations/en.json
+++ b/homeassistant/components/deconz/.translations/en.json
@@ -2,7 +2,9 @@
"config": {
"abort": {
"already_configured": "Bridge is already configured",
+ "already_in_progress": "Config flow for bridge is already in progress.",
"no_bridges": "No deCONZ bridges discovered",
+ "not_deconz_bridge": "Not a deCONZ bridge",
"one_instance_only": "Component only supports one deCONZ instance",
"updated_instance": "Updated deCONZ instance with new host address"
},
diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json
index 23036c19aec7d8..3d658ca00b0049 100644
--- a/homeassistant/components/deconz/.translations/fr.json
+++ b/homeassistant/components/deconz/.translations/fr.json
@@ -3,7 +3,8 @@
"abort": {
"already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
"no_bridges": "Aucun pont deCONZ n'a \u00e9t\u00e9 d\u00e9couvert",
- "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ"
+ "one_instance_only": "Le composant prend uniquement en charge une instance deCONZ",
+ "updated_instance": "Instance deCONZ mise \u00e0 jour avec la nouvelle adresse d'h\u00f4te"
},
"error": {
"no_key": "Impossible d'obtenir une cl\u00e9 d'API"
diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json
index 7934d20ec53275..7c674c71022fc0 100644
--- a/homeassistant/components/deconz/.translations/no.json
+++ b/homeassistant/components/deconz/.translations/no.json
@@ -2,7 +2,9 @@
"config": {
"abort": {
"already_configured": "Broen er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyt for bro p\u00e5g\u00e5r allerede.",
"no_bridges": "Ingen deCONZ broer oppdaget",
+ "not_deconz_bridge": "Ikke en deCONZ bro",
"one_instance_only": "Komponenten st\u00f8tter bare \u00e9n deCONZ forekomst",
"updated_instance": "Oppdatert deCONZ forekomst med ny vertsadresse"
},
diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json
index be79e7e461ae0b..dc7e682cafbcef 100644
--- a/homeassistant/components/deconz/.translations/pt-BR.json
+++ b/homeassistant/components/deconz/.translations/pt-BR.json
@@ -2,8 +2,11 @@
"config": {
"abort": {
"already_configured": "A ponte j\u00e1 est\u00e1 configurada",
+ "already_in_progress": "Fluxo de configura\u00e7\u00e3o para ponte j\u00e1 est\u00e1 em andamento.",
"no_bridges": "N\u00e3o h\u00e1 pontes de deCONZ descobertas",
- "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia deCONZ"
+ "not_deconz_bridge": "N\u00e3o \u00e9 uma ponte deCONZ",
+ "one_instance_only": "Componente suporta apenas uma inst\u00e2ncia deCONZ",
+ "updated_instance": "Atualiza\u00e7\u00e3o da inst\u00e2ncia deCONZ com novo endere\u00e7o de host"
},
"error": {
"no_key": "N\u00e3o foi poss\u00edvel obter uma chave de API"
diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json
index c4f2b2c4fab99a..ea701b3f759434 100644
--- a/homeassistant/components/deconz/.translations/ru.json
+++ b/homeassistant/components/deconz/.translations/ru.json
@@ -2,7 +2,9 @@
"config": {
"abort": {
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430",
+ "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.",
"no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
+ "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ",
"one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ",
"updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d"
},
diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json
index 17367c49f5bcac..a7b5160e8a3c50 100644
--- a/homeassistant/components/deconz/.translations/sv.json
+++ b/homeassistant/components/deconz/.translations/sv.json
@@ -2,7 +2,9 @@
"config": {
"abort": {
"already_configured": "Bryggan \u00e4r redan konfigurerad",
+ "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan.",
"no_bridges": "Inga deCONZ-bryggor uppt\u00e4cktes",
+ "not_deconz_bridge": "Inte en deCONZ-brygga",
"one_instance_only": "Komponenten st\u00f6djer endast en deCONZ-instans",
"updated_instance": "Uppdaterad deCONZ-instans med ny v\u00e4rdadress"
},
diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py
index 153e654f3fb322..71e03da70b7e92 100644
--- a/homeassistant/components/deconz/__init__.py
+++ b/homeassistant/components/deconz/__init__.py
@@ -164,6 +164,7 @@ async def async_unload_entry(hass, config_entry):
if not hass.data[DOMAIN]:
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
+
elif gateway.master:
await async_populate_options(hass, config_entry)
new_master_gateway = next(iter(hass.data[DOMAIN].values()))
diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py
index fbb15abc744ade..6fe8b4324b3c1a 100644
--- a/homeassistant/components/deconz/binary_sensor.py
+++ b/homeassistant/components/deconz/binary_sensor.py
@@ -1,6 +1,8 @@
"""Support for deCONZ binary sensors."""
+from pydeconz.sensor import Presence, Vibration
+
from homeassistant.components.binary_sensor import BinarySensorDevice
-from homeassistant.const import ATTR_BATTERY_LEVEL
+from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -15,7 +17,7 @@
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ binary sensors."""
+ """Old way of setting up deCONZ platforms."""
pass
@@ -26,12 +28,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
- from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
for sensor in sensors:
- if sensor.type in DECONZ_BINARY_SENSOR and \
+ if sensor.BINARY and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
@@ -49,16 +50,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
"""Representation of a deCONZ binary sensor."""
@callback
- def async_update_callback(self, reason):
- """Update the sensor's state.
-
- If reason is that state is updated,
- or reachable has changed or battery has changed.
- """
- if reason['state'] or \
- 'reachable' in reason['attr'] or \
- 'battery' in reason['attr'] or \
- 'on' in reason['attr']:
+ def async_update_callback(self, force_update=False):
+ """Update the sensor's state."""
+ changed = set(self._device.changed_keys)
+ keys = {'battery', 'on', 'reachable', 'state'}
+ if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
@@ -69,26 +65,33 @@ def is_on(self):
@property
def device_class(self):
"""Return the class of the sensor."""
- return self._device.sensor_class
+ return self._device.SENSOR_CLASS
@property
def icon(self):
"""Return the icon to use in the frontend."""
- return self._device.sensor_icon
+ return self._device.SENSOR_ICON
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
- from pydeconz.sensor import PRESENCE, VIBRATION
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
+
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
- if self._device.type in PRESENCE and self._device.dark is not None:
+
+ if self._device.secondary_temperature is not None:
+ attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
+
+ if self._device.type in Presence.ZHATYPE and \
+ self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
- elif self._device.type in VIBRATION:
+
+ elif self._device.type in Vibration.ZHATYPE:
attr[ATTR_ORIENTATION] = self._device.orientation
attr[ATTR_TILTANGLE] = self._device.tiltangle
attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibrationstrength
+
return attr
diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py
index c4a021a80c223c..cde123f7f08b9f 100644
--- a/homeassistant/components/deconz/climate.py
+++ b/homeassistant/components/deconz/climate.py
@@ -1,4 +1,6 @@
"""Support for deCONZ climate devices."""
+from pydeconz.sensor import Thermostat
+
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE)
@@ -12,6 +14,12 @@
from .gateway import get_gateway_from_config_entry
+async def async_setup_platform(
+ hass, config, async_add_entities, discovery_info=None):
+ """Old way of setting up deCONZ platforms."""
+ pass
+
+
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ climate devices.
@@ -22,12 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_climate(sensors):
"""Add climate devices from deCONZ."""
- from pydeconz.sensor import THERMOSTAT
entities = []
for sensor in sensors:
- if sensor.type in THERMOSTAT and \
+ if sensor.type in Thermostat.ZHATYPE and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
@@ -59,7 +66,7 @@ def supported_features(self):
@property
def is_on(self):
"""Return true if on."""
- return self._device.on
+ return self._device.state_on
async def async_turn_on(self):
"""Turn on switch."""
diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py
index d9065ad2727fc4..cf172ad799133a 100644
--- a/homeassistant/components/deconz/config_flow.py
+++ b/homeassistant/components/deconz/config_flow.py
@@ -4,13 +4,19 @@
import async_timeout
import voluptuous as vol
+from pydeconz.errors import ResponseError, RequestError
+from pydeconz.utils import (
+ async_discovery, async_get_api_key, async_get_bridgeid)
+
from homeassistant import config_entries
+from homeassistant.components.ssdp import ATTR_MANUFACTURERURL, ATTR_SERIAL
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
+DECONZ_MANUFACTURERURL = 'http://www.dresden-elektronik.de'
CONF_SERIAL = 'serial'
@@ -54,8 +60,6 @@ async def async_step_user(self, user_input=None):
If more than one bridge is found let user choose bridge to link.
If no bridge is found allow user to manually input configuration.
"""
- from pydeconz.utils import async_discovery
-
if user_input is not None:
for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]:
@@ -101,8 +105,6 @@ async def async_step_user(self, user_input=None):
async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
- from pydeconz.errors import ResponseError, RequestError
- from pydeconz.utils import async_get_api_key
errors = {}
if user_input is not None:
@@ -127,8 +129,6 @@ async def async_step_link(self, user_input=None):
async def _create_entry(self):
"""Create entry for gateway."""
- from pydeconz.utils import async_get_bridgeid
-
if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass)
@@ -151,12 +151,12 @@ async def _update_entry(self, entry, host):
entry.data[CONF_HOST] = host
self.hass.config_entries.async_update_entry(entry)
- async def async_step_discovery(self, discovery_info):
- """Prepare configuration for a discovered deCONZ bridge.
+ async def async_step_ssdp(self, discovery_info):
+ """Handle a discovered deCONZ bridge."""
+ if discovery_info[ATTR_MANUFACTURERURL] != DECONZ_MANUFACTURERURL:
+ return self.async_abort(reason='not_deconz_bridge')
- This flow is triggered by the discovery component.
- """
- bridgeid = discovery_info[CONF_SERIAL]
+ bridgeid = discovery_info[ATTR_SERIAL]
gateway_entries = configured_gateways(self.hass)
if bridgeid in gateway_entries:
@@ -164,10 +164,17 @@ async def async_step_discovery(self, discovery_info):
await self._update_entry(entry, discovery_info[CONF_HOST])
return self.async_abort(reason='updated_instance')
+ # pylint: disable=unsupported-assignment-operation
+ self.context[ATTR_SERIAL] = bridgeid
+
+ if any(bridgeid == flow['context'][ATTR_SERIAL]
+ for flow in self._async_in_progress()):
+ return self.async_abort(reason='already_in_progress')
+
deconz_config = {
CONF_HOST: discovery_info[CONF_HOST],
CONF_PORT: discovery_info[CONF_PORT],
- CONF_BRIDGEID: discovery_info[CONF_SERIAL]
+ CONF_BRIDGEID: bridgeid
}
return await self.async_step_import(deconz_config)
diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py
index aa29e8c6b58b13..a89e7fdd59565e 100644
--- a/homeassistant/components/deconz/cover.py
+++ b/homeassistant/components/deconz/cover.py
@@ -14,7 +14,7 @@
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
- """Unsupported way of setting up deCONZ covers."""
+ """Old way of setting up deCONZ platforms."""
pass
diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py
index 73ac2499cd3ad2..90a5c8a3ddebef 100644
--- a/homeassistant/components/deconz/deconz_device.py
+++ b/homeassistant/components/deconz/deconz_device.py
@@ -31,7 +31,7 @@ async def async_will_remove_from_hass(self) -> None:
self.unsub_dispatcher()
@callback
- def async_update_callback(self, reason):
+ def async_update_callback(self, force_update=False):
"""Update the device's state."""
self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py
index 46078ea6648e5b..f5d398fcd2f99d 100644
--- a/homeassistant/components/deconz/gateway.py
+++ b/homeassistant/components/deconz/gateway.py
@@ -2,6 +2,9 @@
import asyncio
import async_timeout
+from pydeconz import DeconzSession, errors
+from pydeconz.sensor import Switch
+
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID
from homeassistant.core import EventOrigin, callback
@@ -126,8 +129,7 @@ def event_reachable(self):
def async_connection_status_callback(self, available):
"""Handle signals of gateway connection status."""
self.available = available
- async_dispatcher_send(self.hass, self.event_reachable,
- {'state': True, 'attr': 'reachable'})
+ async_dispatcher_send(self.hass, self.event_reachable, True)
@callback
def async_event_new_device(self, device_type):
@@ -145,9 +147,8 @@ def async_add_device_callback(self, device_type, device):
@callback
def async_add_remote(self, sensors):
"""Set up remote from deCONZ."""
- from pydeconz.sensor import SWITCH as DECONZ_REMOTE
for sensor in sensors:
- if sensor.type in DECONZ_REMOTE and \
+ if sensor.type in Switch.ZHATYPE and \
not (not self.allow_clip_sensor and
sensor.type.startswith('CLIP')):
self.events.append(DeconzEvent(self.hass, sensor))
@@ -187,8 +188,6 @@ async def async_reset(self):
async def get_gateway(hass, config, async_add_device_callback,
async_connection_status_callback):
"""Create a gateway object and verify configuration."""
- from pydeconz import DeconzSession, errors
-
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **config,
@@ -232,8 +231,8 @@ def async_will_remove_from_hass(self) -> None:
self._device = None
@callback
- def async_update_callback(self, reason):
+ def async_update_callback(self, force_update=False):
"""Fire the event if reason is that state is updated."""
- if reason['state']:
+ if 'state' in self._device.changed_keys:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)
diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py
index c195703c36ac88..a3328ca804205f 100644
--- a/homeassistant/components/deconz/light.py
+++ b/homeassistant/components/deconz/light.py
@@ -15,7 +15,7 @@
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ lights and group."""
+ """Old way of setting up deCONZ platforms."""
pass
diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json
index 0692bd444b8845..56ea52b7693d52 100644
--- a/homeassistant/components/deconz/manifest.json
+++ b/homeassistant/components/deconz/manifest.json
@@ -4,8 +4,13 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/deconz",
"requirements": [
- "pydeconz==58"
+ "pydeconz==59"
],
+ "ssdp": {
+ "manufacturer": [
+ "Royal Philips Electronics"
+ ]
+ },
"dependencies": [],
"codeowners": [
"@kane610"
diff --git a/homeassistant/components/deconz/scene.py b/homeassistant/components/deconz/scene.py
index d2e7f6719e915a..c8cfa9674c5f1b 100644
--- a/homeassistant/components/deconz/scene.py
+++ b/homeassistant/components/deconz/scene.py
@@ -9,7 +9,7 @@
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ scenes."""
+ """Old way of setting up deCONZ platforms."""
pass
diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py
index 9f1e87db4ba821..efdb8ad80919b2 100644
--- a/homeassistant/components/deconz/sensor.py
+++ b/homeassistant/components/deconz/sensor.py
@@ -1,6 +1,8 @@
"""Support for deCONZ sensors."""
+from pydeconz.sensor import LightLevel, Switch
+
from homeassistant.const import (
- ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
+ ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
@@ -16,7 +18,7 @@
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ sensors."""
+ """Old way of setting up deCONZ platforms."""
pass
@@ -27,17 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_sensor(sensors):
"""Add sensors from deCONZ."""
- from pydeconz.sensor import (
- DECONZ_SENSOR, SWITCH as DECONZ_REMOTE)
entities = []
for sensor in sensors:
- if sensor.type in DECONZ_SENSOR and \
+ if not sensor.BINARY and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
- if sensor.type in DECONZ_REMOTE:
+ if sensor.type in Switch.ZHATYPE:
if sensor.battery:
entities.append(DeconzBattery(sensor, gateway))
@@ -56,16 +56,11 @@ class DeconzSensor(DeconzDevice):
"""Representation of a deCONZ sensor."""
@callback
- def async_update_callback(self, reason):
- """Update the sensor's state.
-
- If reason is that state is updated,
- or reachable has changed or battery has changed.
- """
- if reason['state'] or \
- 'reachable' in reason['attr'] or \
- 'battery' in reason['attr'] or \
- 'on' in reason['attr']:
+ def async_update_callback(self, force_update=False):
+ """Update the sensor's state."""
+ changed = set(self._device.changed_keys)
+ keys = {'battery', 'on', 'reachable', 'state'}
+ if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
@@ -76,34 +71,42 @@ def state(self):
@property
def device_class(self):
"""Return the class of the sensor."""
- return self._device.sensor_class
+ return self._device.SENSOR_CLASS
@property
def icon(self):
"""Return the icon to use in the frontend."""
- return self._device.sensor_icon
+ return self._device.SENSOR_ICON
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
- return self._device.sensor_unit
+ return self._device.SENSOR_UNIT
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
- from pydeconz.sensor import LIGHTLEVEL
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
+
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
- if self._device.type in LIGHTLEVEL and self._device.dark is not None:
+
+ if self._device.secondary_temperature is not None:
+ attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
+
+ if self._device.type in LightLevel.ZHATYPE and \
+ self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
+
if self.unit_of_measurement == 'Watts':
attr[ATTR_CURRENT] = self._device.current
attr[ATTR_VOLTAGE] = self._device.voltage
- if self._device.sensor_class == 'daylight':
+
+ if self._device.SENSOR_CLASS == 'daylight':
attr[ATTR_DAYLIGHT] = self._device.daylight
+
return attr
@@ -118,9 +121,11 @@ def __init__(self, device, gateway):
self._unit_of_measurement = "%"
@callback
- def async_update_callback(self, reason):
+ def async_update_callback(self, force_update=False):
"""Update the battery's state, if needed."""
- if 'reachable' in reason['attr'] or 'battery' in reason['attr']:
+ changed = set(self._device.changed_keys)
+ keys = {'battery', 'reachable'}
+ if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json
index 16177dbd3cc1d3..d1c70793063ee6 100644
--- a/homeassistant/components/deconz/strings.json
+++ b/homeassistant/components/deconz/strings.json
@@ -34,9 +34,11 @@
},
"abort": {
"already_configured": "Bridge is already configured",
+ "already_in_progress": "Config flow for bridge is already in progress.",
"no_bridges": "No deCONZ bridges discovered",
- "updated_instance": "Updated deCONZ instance with new host address",
- "one_instance_only": "Component only supports one deCONZ instance"
+ "not_deconz_bridge": "Not a deCONZ bridge",
+ "one_instance_only": "Component only supports one deCONZ instance",
+ "updated_instance": "Updated deCONZ instance with new host address"
}
}
}
diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py
index c399f5da128d6f..dd06dba9583eaa 100644
--- a/homeassistant/components/deconz/switch.py
+++ b/homeassistant/components/deconz/switch.py
@@ -10,7 +10,7 @@
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
- """Old way of setting up deCONZ switches."""
+ """Old way of setting up deCONZ platforms."""
pass
diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json
index f52da35dc64e90..992cb71c07c57c 100644
--- a/homeassistant/components/default_config/manifest.json
+++ b/homeassistant/components/default_config/manifest.json
@@ -15,6 +15,7 @@
"mobile_app",
"person",
"script",
+ "ssdp",
"sun",
"system_health",
"updater",
diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json
index df7d58169e056a..5e40dbb89da102 100644
--- a/homeassistant/components/denonavr/manifest.json
+++ b/homeassistant/components/denonavr/manifest.json
@@ -3,7 +3,7 @@
"name": "Denonavr",
"documentation": "https://www.home-assistant.io/components/denonavr",
"requirements": [
- "denonavr==0.7.8"
+ "denonavr==0.7.9"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py
index 7f05c70653cf63..a7c306ad241147 100644
--- a/homeassistant/components/discovery/__init__.py
+++ b/homeassistant/components/discovery/__init__.py
@@ -25,17 +25,13 @@
SCAN_INTERVAL = timedelta(seconds=300)
SERVICE_APPLE_TV = 'apple_tv'
SERVICE_DAIKIN = 'daikin'
-SERVICE_DECONZ = 'deconz'
SERVICE_DLNA_DMR = 'dlna_dmr'
SERVICE_ENIGMA2 = 'enigma2'
SERVICE_FREEBOX = 'freebox'
SERVICE_HASS_IOS_APP = 'hass_ios'
SERVICE_HASSIO = 'hassio'
-SERVICE_HOMEKIT = 'homekit'
SERVICE_HEOS = 'heos'
-SERVICE_HUE = 'philips_hue'
SERVICE_IGD = 'igd'
-SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
SERVICE_KONNECTED = 'konnected'
SERVICE_MOBILE_APP = 'hass_mobile_app'
SERVICE_NETGEAR = 'netgear_router'
@@ -51,22 +47,17 @@
CONFIG_ENTRY_HANDLERS = {
SERVICE_DAIKIN: 'daikin',
- SERVICE_DECONZ: 'deconz',
'google_cast': 'cast',
SERVICE_HEOS: 'heos',
- SERVICE_HUE: 'hue',
SERVICE_TELLDUSLIVE: 'tellduslive',
- SERVICE_IKEA_TRADFRI: 'tradfri',
'sonos': 'sonos',
SERVICE_IGD: 'upnp',
- SERVICE_HOMEKIT: 'homekit_controller',
}
SERVICE_HANDLERS = {
SERVICE_MOBILE_APP: ('mobile_app', None),
SERVICE_HASS_IOS_APP: ('ios', None),
SERVICE_NETGEAR: ('device_tracker', None),
- SERVICE_WEMO: ('wemo', None),
SERVICE_HASSIO: ('hassio', None),
SERVICE_APPLE_TV: ('apple_tv', None),
SERVICE_ENIGMA2: ('media_player', 'enigma2'),
@@ -102,8 +93,20 @@
SERVICE_DLNA_DMR: ('media_player', 'dlna_dmr'),
}
-DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS)
-DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS)
+MIGRATED_SERVICE_HANDLERS = [
+ 'axis',
+ 'deconz',
+ 'esphome',
+ 'ikea_tradfri',
+ 'homekit',
+ 'philips_hue',
+ SERVICE_WEMO,
+]
+
+DEFAULT_ENABLED = list(CONFIG_ENTRY_HANDLERS) + list(SERVICE_HANDLERS) + \
+ MIGRATED_SERVICE_HANDLERS
+DEFAULT_DISABLED = list(OPTIONAL_SERVICE_HANDLERS) + \
+ MIGRATED_SERVICE_HANDLERS
CONF_IGNORE = 'ignore'
CONF_ENABLE = 'enable'
@@ -150,6 +153,9 @@ async def async_setup(hass, config):
async def new_service_found(service, info):
"""Handle a new service if one is found."""
+ if service in MIGRATED_SERVICE_HANDLERS:
+ return
+
if service in ignored_platforms:
logger.info("Ignoring service: %s %s", service, info)
return
diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py
index e19d910ad83409..15b2b7fd0de468 100644
--- a/homeassistant/components/dsmr/sensor.py
+++ b/homeassistant/components/dsmr/sensor.py
@@ -183,11 +183,12 @@ def update_entities_telegram(telegram):
if CONF_HOST in config:
reader_factory = partial(
create_tcp_dsmr_reader, config[CONF_HOST], config[CONF_PORT],
- config[CONF_DSMR_VERSION], update_entities_telegram)
+ config[CONF_DSMR_VERSION], update_entities_telegram,
+ loop=hass.loop)
else:
reader_factory = partial(
create_dsmr_reader, config[CONF_PORT], config[CONF_DSMR_VERSION],
- update_entities_telegram)
+ update_entities_telegram, loop=hass.loop)
async def connect_and_reconnect():
"""Connect to DSMR and keep reconnecting until Home Assistant stops."""
diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json
index 6fee88b39fca80..1a816bc91d91d1 100644
--- a/homeassistant/components/enphase_envoy/manifest.json
+++ b/homeassistant/components/enphase_envoy/manifest.json
@@ -3,7 +3,7 @@
"name": "Enphase envoy",
"documentation": "https://www.home-assistant.io/components/enphase_envoy",
"requirements": [
- "envoy_reader==0.3"
+ "envoy_reader==0.4"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/esphome/.translations/ca.json b/homeassistant/components/esphome/.translations/ca.json
index f9c60979c8d86c..2e6f8dc62ad9bb 100644
--- a/homeassistant/components/esphome/.translations/ca.json
+++ b/homeassistant/components/esphome/.translations/ca.json
@@ -8,6 +8,7 @@
"invalid_password": "Contrasenya inv\u00e0lida!",
"resolve_error": "No s'ha pogut trobar l'adre\u00e7a de l'ESP. Si l'error persisteix, configura una adre\u00e7a IP est\u00e0tica: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/de.json b/homeassistant/components/esphome/.translations/de.json
index 30cbf09525f884..80111f34984cbd 100644
--- a/homeassistant/components/esphome/.translations/de.json
+++ b/homeassistant/components/esphome/.translations/de.json
@@ -8,6 +8,7 @@
"invalid_password": "Ung\u00fcltiges Passwort!",
"resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, legen Sie eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/en.json b/homeassistant/components/esphome/.translations/en.json
index 3a73e54c34558a..f5236d1735dccb 100644
--- a/homeassistant/components/esphome/.translations/en.json
+++ b/homeassistant/components/esphome/.translations/en.json
@@ -8,6 +8,7 @@
"invalid_password": "Invalid password!",
"resolve_error": "Can't resolve address of the ESP. If this error persists, please set a static IP address: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/fr.json b/homeassistant/components/esphome/.translations/fr.json
index b230a73c354eda..26fa4ec0bd46d2 100644
--- a/homeassistant/components/esphome/.translations/fr.json
+++ b/homeassistant/components/esphome/.translations/fr.json
@@ -8,6 +8,7 @@
"invalid_password": "Mot de passe invalide !",
"resolve_error": "Impossible de r\u00e9soudre l'adresse de l'ESP. Si cette erreur persiste, veuillez d\u00e9finir une adresse IP statique: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/hu.json b/homeassistant/components/esphome/.translations/hu.json
index c665637ba05248..628983fec03704 100644
--- a/homeassistant/components/esphome/.translations/hu.json
+++ b/homeassistant/components/esphome/.translations/hu.json
@@ -8,6 +8,7 @@
"invalid_password": "\u00c9rv\u00e9nytelen jelsz\u00f3!",
"resolve_error": "Az ESP c\u00edme nem oldhat\u00f3 fel. Ha a hiba tov\u00e1bbra is fenn\u00e1ll, k\u00e9rlek, \u00e1ll\u00edts be egy statikus IP-c\u00edmet: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/ko.json b/homeassistant/components/esphome/.translations/ko.json
index f58d43f9df9ae6..b6bcf3cd1b3377 100644
--- a/homeassistant/components/esphome/.translations/ko.json
+++ b/homeassistant/components/esphome/.translations/ko.json
@@ -8,6 +8,7 @@
"invalid_password": "\uc798\ubabb\ub41c \ube44\ubc00\ubc88\ud638",
"resolve_error": "ESP \uc758 \uc8fc\uc18c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc774 \uc624\ub958\uac00 \uacc4\uc18d \ubc1c\uc0dd\ud558\uba74 \uace0\uc815 IP \uc8fc\uc18c\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/no.json b/homeassistant/components/esphome/.translations/no.json
index c71424b6f00e57..f7dac2a9d568dd 100644
--- a/homeassistant/components/esphome/.translations/no.json
+++ b/homeassistant/components/esphome/.translations/no.json
@@ -8,6 +8,7 @@
"invalid_password": "Ugyldig passord!",
"resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, m\u00e5 du [angi en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/pl.json b/homeassistant/components/esphome/.translations/pl.json
index 5693efde9a8d50..d2fceb93223f17 100644
--- a/homeassistant/components/esphome/.translations/pl.json
+++ b/homeassistant/components/esphome/.translations/pl.json
@@ -8,6 +8,7 @@
"invalid_password": "Nieprawid\u0142owe has\u0142o!",
"resolve_error": "Nie mo\u017cna rozpozna\u0107 adresu ESP. Je\u015bli ten b\u0142\u0105d b\u0119dzie si\u0119 powtarza\u0142, nale\u017cy ustawi\u0107 statyczny adres IP: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/pt-BR.json b/homeassistant/components/esphome/.translations/pt-BR.json
index 87adc69021c699..80a5c28598c8b9 100644
--- a/homeassistant/components/esphome/.translations/pt-BR.json
+++ b/homeassistant/components/esphome/.translations/pt-BR.json
@@ -8,6 +8,7 @@
"invalid_password": "Senha inv\u00e1lida!",
"resolve_error": "N\u00e3o \u00e9 poss\u00edvel resolver o endere\u00e7o do ESP. Se este erro persistir, por favor, defina um endere\u00e7o IP est\u00e1tico: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json
index 9777a920a944e3..1405112c07022f 100644
--- a/homeassistant/components/esphome/.translations/ru.json
+++ b/homeassistant/components/esphome/.translations/ru.json
@@ -8,6 +8,7 @@
"invalid_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!",
"resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/sl.json b/homeassistant/components/esphome/.translations/sl.json
index 93ca607aabec0b..5f4e9d3e4c43ad 100644
--- a/homeassistant/components/esphome/.translations/sl.json
+++ b/homeassistant/components/esphome/.translations/sl.json
@@ -8,6 +8,7 @@
"invalid_password": "Neveljavno geslo!",
"resolve_error": "Ne moremo razre\u0161iti naslova ESP. \u010ce se napaka ponovi, prosimo nastavite stati\u010dni IP naslov: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/sv.json b/homeassistant/components/esphome/.translations/sv.json
index da977af601ab37..37788522e4f699 100644
--- a/homeassistant/components/esphome/.translations/sv.json
+++ b/homeassistant/components/esphome/.translations/sv.json
@@ -8,6 +8,7 @@
"invalid_password": "Ogiltigt l\u00f6senord!",
"resolve_error": "Det g\u00e5r inte att hitta IP-adressen f\u00f6r ESP med DNS-namnet. Om det h\u00e4r felet kvarst\u00e5r anger du en statisk IP-adress: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome: {name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/.translations/zh-Hant.json b/homeassistant/components/esphome/.translations/zh-Hant.json
index 9a5821f0b8fe41..721f4362103df2 100644
--- a/homeassistant/components/esphome/.translations/zh-Hant.json
+++ b/homeassistant/components/esphome/.translations/zh-Hant.json
@@ -8,6 +8,7 @@
"invalid_password": "\u5bc6\u78bc\u7121\u6548\uff01",
"resolve_error": "\u7121\u6cd5\u89e3\u6790 ESP \u4f4d\u5740\uff0c\u5047\u5982\u6b64\u932f\u8aa4\u6301\u7e8c\u767c\u751f\uff0c\u8acb\u53c3\u8003\u8aaa\u660e\u8a2d\u5b9a\u70ba\u975c\u614b\u56fa\u5b9a IP \uff1a https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips"
},
+ "flow_title": "ESPHome\uff1a{name}",
"step": {
"authenticate": {
"data": {
diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py
index e5feedd84215a0..395c145e5df241 100644
--- a/homeassistant/components/esphome/__init__.py
+++ b/homeassistant/components/esphome/__init__.py
@@ -2,45 +2,40 @@
import asyncio
import logging
import math
-from typing import Any, Dict, List, Optional, TYPE_CHECKING, Callable, Tuple
+from typing import Any, Callable, Dict, List, Optional
-import attr
+from aioesphomeapi import (
+ APIClient, APIConnectionError, DeviceInfo, EntityInfo, EntityState,
+ ServiceCall, UserService, UserServiceArgType)
import voluptuous as vol
from homeassistant import const
from homeassistant.config_entries import ConfigEntry
-from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, \
- EVENT_HOMEASSISTANT_STOP
-from homeassistant.core import callback, Event, State
-import homeassistant.helpers.device_registry as dr
+from homeassistant.const import (
+ CONF_HOST, CONF_PASSWORD, CONF_PORT, EVENT_HOMEASSISTANT_STOP)
+from homeassistant.core import Event, State, callback
from homeassistant.exceptions import TemplateError
-import homeassistant.helpers.config_validation as cv
from homeassistant.helpers import template
-from homeassistant.helpers.dispatcher import async_dispatcher_connect, \
- async_dispatcher_send
+import homeassistant.helpers.config_validation as cv
+import homeassistant.helpers.device_registry as dr
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_state_change
-from homeassistant.helpers.template import Template
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.storage import Store
-from homeassistant.helpers.typing import HomeAssistantType, ConfigType
+from homeassistant.helpers.template import Template
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
# Import config flow so that it's added to the registry
from .config_flow import EsphomeFlowHandler # noqa
-
-if TYPE_CHECKING:
- from aioesphomeapi import APIClient, EntityInfo, EntityState, DeviceInfo, \
- ServiceCall, UserService
+from .entry_data import (
+ DATA_KEY, DISPATCHER_ON_DEVICE_UPDATE, DISPATCHER_ON_LIST,
+ DISPATCHER_ON_STATE, DISPATCHER_REMOVE_ENTITY, DISPATCHER_UPDATE_ENTITY,
+ RuntimeEntryData)
DOMAIN = 'esphome'
_LOGGER = logging.getLogger(__name__)
-DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}'
-DISPATCHER_REMOVE_ENTITY = 'esphome_{entry_id}_remove_{component_key}_{key}'
-DISPATCHER_ON_LIST = 'esphome_{entry_id}_on_list'
-DISPATCHER_ON_DEVICE_UPDATE = 'esphome_{entry_id}_on_device_update'
-DISPATCHER_ON_STATE = 'esphome_{entry_id}_on_state'
-
STORAGE_KEY = 'esphome.{}'
STORAGE_VERSION = 1
@@ -60,99 +55,6 @@
CONFIG_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA)
-@attr.s
-class RuntimeEntryData:
- """Store runtime data for esphome config entries."""
-
- entry_id = attr.ib(type=str)
- client = attr.ib(type='APIClient')
- store = attr.ib(type=Store)
- reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None)
- state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict)
- info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict)
- services = attr.ib(type=Dict[int, 'UserService'], factory=dict)
- available = attr.ib(type=bool, default=False)
- device_info = attr.ib(type='DeviceInfo', default=None)
- cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list)
- disconnect_callbacks = attr.ib(type=List[Callable[[], None]], factory=list)
-
- def async_update_entity(self, hass: HomeAssistantType, component_key: str,
- key: int) -> None:
- """Schedule the update of an entity."""
- signal = DISPATCHER_UPDATE_ENTITY.format(
- entry_id=self.entry_id, component_key=component_key, key=key)
- async_dispatcher_send(hass, signal)
-
- def async_remove_entity(self, hass: HomeAssistantType, component_key: str,
- key: int) -> None:
- """Schedule the removal of an entity."""
- signal = DISPATCHER_REMOVE_ENTITY.format(
- entry_id=self.entry_id, component_key=component_key, key=key)
- async_dispatcher_send(hass, signal)
-
- def async_update_static_infos(self, hass: HomeAssistantType,
- infos: 'List[EntityInfo]') -> None:
- """Distribute an update of static infos to all platforms."""
- signal = DISPATCHER_ON_LIST.format(entry_id=self.entry_id)
- async_dispatcher_send(hass, signal, infos)
-
- def async_update_state(self, hass: HomeAssistantType,
- state: 'EntityState') -> None:
- """Distribute an update of state information to all platforms."""
- signal = DISPATCHER_ON_STATE.format(entry_id=self.entry_id)
- async_dispatcher_send(hass, signal, state)
-
- def async_update_device_state(self, hass: HomeAssistantType) -> None:
- """Distribute an update of a core device state like availability."""
- signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id)
- async_dispatcher_send(hass, signal)
-
- async def async_load_from_store(self) -> Tuple[List['EntityInfo'],
- List['UserService']]:
- """Load the retained data from store and return de-serialized data."""
- # pylint: disable= redefined-outer-name
- from aioesphomeapi import COMPONENT_TYPE_TO_INFO, DeviceInfo, \
- UserService
-
- restored = await self.store.async_load()
- if restored is None:
- return [], []
-
- self.device_info = _attr_obj_from_dict(DeviceInfo,
- **restored.pop('device_info'))
- infos = []
- for comp_type, restored_infos in restored.items():
- if comp_type not in COMPONENT_TYPE_TO_INFO:
- continue
- for info in restored_infos:
- cls = COMPONENT_TYPE_TO_INFO[comp_type]
- infos.append(_attr_obj_from_dict(cls, **info))
- services = []
- for service in restored.get('services', []):
- services.append(UserService.from_dict(service))
- return infos, services
-
- async def async_save_to_store(self) -> None:
- """Generate dynamic data to store and save it to the filesystem."""
- store_data = {
- 'device_info': attr.asdict(self.device_info),
- 'services': []
- }
-
- for comp_type, infos in self.info.items():
- store_data[comp_type] = [attr.asdict(info)
- for info in infos.values()]
- for service in self.services.values():
- store_data['services'].append(service.to_dict())
-
- await self.store.async_save(store_data)
-
-
-def _attr_obj_from_dict(cls, **kwargs):
- return cls(**{key: kwargs[key] for key in attr.fields_dict(cls)
- if key in kwargs})
-
-
async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
"""Stub to allow setting up this component.
@@ -164,10 +66,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistantType,
entry: ConfigEntry) -> bool:
"""Set up the esphome component."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import APIClient, APIConnectionError
-
- hass.data.setdefault(DOMAIN, {})
+ hass.data.setdefault(DATA_KEY, {})
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
@@ -179,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistantType,
# Store client in per-config-entry hass.data
store = Store(hass, STORAGE_VERSION, STORAGE_KEY.format(entry.entry_id),
encoder=JSONEncoder)
- entry_data = hass.data[DOMAIN][entry.entry_id] = RuntimeEntryData(
+ entry_data = hass.data[DATA_KEY][entry.entry_id] = RuntimeEntryData(
client=cli,
entry_id=entry.entry_id,
store=store,
@@ -194,12 +93,12 @@ async def on_stop(event: Event) -> None:
)
@callback
- def async_on_state(state: 'EntityState') -> None:
+ def async_on_state(state: EntityState) -> None:
"""Send dispatcher updates when a new state is received."""
entry_data.async_update_state(hass, state)
@callback
- def async_on_service_call(service: 'ServiceCall') -> None:
+ def async_on_service_call(service: ServiceCall) -> None:
"""Call service when user automation in ESPHome config is triggered."""
domain, service_name = service.service.split('.', 1)
service_data = service.data
@@ -261,26 +160,6 @@ async def on_login() -> None:
try_connect = await _setup_auto_reconnect_logic(hass, cli, entry, host,
on_login)
- # This is a bit of a hack: We schedule complete_setup into the
- # event loop and return immediately (return True)
- #
- # Usually, we should avoid that so that HA can track which components
- # have been started successfully and which failed to be set up.
- # That doesn't work here for two reasons:
- # - We have our own re-connect logic
- # - Before we do the first try_connect() call, we need to make sure
- # all dispatcher event listeners have been connected, so
- # async_forward_entry_setup needs to be awaited. However, if we
- # would await async_forward_entry_setup() in async_setup_entry(),
- # we would end up with a deadlock.
- #
- # Solution is: complete the setup outside of the async_setup_entry()
- # function. HA will wait until the first connection attempt is made
- # before starting up (as it should), but if the first connection attempt
- # fails we will schedule all next re-connect attempts outside of the
- # tracked tasks (hass.loop.create_task). This way HA won't stall startup
- # forever until a connection is successful.
-
async def complete_setup() -> None:
"""Complete the config entry setup."""
tasks = []
@@ -293,21 +172,18 @@ async def complete_setup() -> None:
entry_data.async_update_static_infos(hass, infos)
await _setup_services(hass, entry_data, services)
- # If first connect fails, the next re-connect will be scheduled
- # outside of _pending_task, in order not to delay HA startup
- # indefinitely
- await try_connect(is_disconnect=False)
+ # Create connection attempt outside of HA's tracked task in order
+ # not to delay startup.
+ hass.loop.create_task(try_connect(is_disconnect=False))
hass.async_create_task(complete_setup())
return True
async def _setup_auto_reconnect_logic(hass: HomeAssistantType,
- cli: 'APIClient',
+ cli: APIClient,
entry: ConfigEntry, host: str, on_login):
"""Set up the re-connect logic for the API client."""
- from aioesphomeapi import APIConnectionError
-
async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None:
"""Try connecting to the API client. Will retry if not successful."""
if entry.entry_id not in hass.data[DOMAIN]:
@@ -361,7 +237,7 @@ async def try_connect(tries: int = 0, is_disconnect: bool = True) -> None:
async def _async_setup_device_registry(hass: HomeAssistantType,
entry: ConfigEntry,
- device_info: 'DeviceInfo'):
+ device_info: DeviceInfo):
"""Set up device registry feature for a particular config entry."""
sw_version = device_info.esphome_core_version
if device_info.compilation_time:
@@ -381,8 +257,7 @@ async def _async_setup_device_registry(hass: HomeAssistantType,
async def _register_service(hass: HomeAssistantType,
entry_data: RuntimeEntryData,
- service: 'UserService'):
- from aioesphomeapi import UserServiceArgType
+ service: UserService):
service_name = '{}_{}'.format(entry_data.device_info.name, service.name)
schema = {}
for arg in service.args:
@@ -402,7 +277,7 @@ async def execute_service(call):
async def _setup_services(hass: HomeAssistantType,
entry_data: RuntimeEntryData,
- services: List['UserService']):
+ services: List[UserService]):
old_services = entry_data.services.copy()
to_unregister = []
to_register = []
@@ -435,7 +310,7 @@ async def _setup_services(hass: HomeAssistantType,
async def _cleanup_instance(hass: HomeAssistantType,
entry: ConfigEntry) -> None:
"""Cleanup the esphome client if it exists."""
- data = hass.data[DOMAIN].pop(entry.entry_id) # type: RuntimeEntryData
+ data = hass.data[DATA_KEY].pop(entry.entry_id) # type: RuntimeEntryData
if data.reconnect_task is not None:
data.reconnect_task.cancel()
for disconnect_cb in data.disconnect_callbacks:
@@ -478,7 +353,7 @@ async def platform_async_setup_entry(hass: HomeAssistantType,
entry_data.state[component_key] = {}
@callback
- def async_list_entities(infos: List['EntityInfo']):
+ def async_list_entities(infos: List[EntityInfo]):
"""Update entities of this platform when entities are listed."""
old_infos = entry_data.info[component_key]
new_infos = {}
@@ -509,7 +384,7 @@ def async_list_entities(infos: List['EntityInfo']):
)
@callback
- def async_entity_state(state: 'EntityState'):
+ def async_entity_state(state: EntityState):
"""Notify the appropriate entity of an updated state."""
if not isinstance(state, state_type):
return
@@ -530,6 +405,7 @@ def esphome_state_property(func):
"""
@property
def _wrapper(self):
+ # pylint: disable=protected-access
if self._state is None:
return None
val = func(self)
@@ -614,22 +490,22 @@ async def async_will_remove_from_hass(self) -> None:
@property
def _entry_data(self) -> RuntimeEntryData:
- return self.hass.data[DOMAIN][self._entry_id]
+ return self.hass.data[DATA_KEY][self._entry_id]
@property
- def _static_info(self) -> 'EntityInfo':
+ def _static_info(self) -> EntityInfo:
return self._entry_data.info[self._component_key][self._key]
@property
- def _device_info(self) -> 'DeviceInfo':
+ def _device_info(self) -> DeviceInfo:
return self._entry_data.device_info
@property
- def _client(self) -> 'APIClient':
+ def _client(self) -> APIClient:
return self._entry_data.client
@property
- def _state(self) -> 'Optional[EntityState]':
+ def _state(self) -> Optional[EntityState]:
try:
return self._entry_data.state[self._component_key][self._key]
except KeyError:
diff --git a/homeassistant/components/esphome/binary_sensor.py b/homeassistant/components/esphome/binary_sensor.py
index 6a6f9bfac1c967..75a7235c58fe9f 100644
--- a/homeassistant/components/esphome/binary_sensor.py
+++ b/homeassistant/components/esphome/binary_sensor.py
@@ -1,23 +1,18 @@
"""Support for ESPHome binary sensors."""
import logging
-from typing import TYPE_CHECKING, Optional
+from typing import Optional
+
+from aioesphomeapi import BinarySensorInfo, BinarySensorState
from homeassistant.components.binary_sensor import BinarySensorDevice
from . import EsphomeEntity, platform_async_setup_entry
-if TYPE_CHECKING:
- # pylint: disable=unused-import
- from aioesphomeapi import BinarySensorInfo, BinarySensorState # noqa
-
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up ESPHome binary sensors based on a config entry."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import BinarySensorInfo, BinarySensorState # noqa
-
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='binary_sensor',
@@ -30,11 +25,11 @@ class EsphomeBinarySensor(EsphomeEntity, BinarySensorDevice):
"""A binary sensor implementation for ESPHome."""
@property
- def _static_info(self) -> 'BinarySensorInfo':
+ def _static_info(self) -> BinarySensorInfo:
return super()._static_info
@property
- def _state(self) -> Optional['BinarySensorState']:
+ def _state(self) -> Optional[BinarySensorState]:
return super()._state
@property
diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py
index 64e73dc8784f91..54f774bc42616c 100644
--- a/homeassistant/components/esphome/camera.py
+++ b/homeassistant/components/esphome/camera.py
@@ -1,17 +1,16 @@
"""Support for ESPHome cameras."""
import asyncio
import logging
-from typing import Optional, TYPE_CHECKING
+from typing import Optional
+
+from aioesphomeapi import CameraInfo, CameraState
from homeassistant.components import camera
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
-from . import EsphomeEntity, platform_async_setup_entry
-if TYPE_CHECKING:
- # pylint: disable=unused-import
- from aioesphomeapi import CameraInfo, CameraState # noqa
+from . import EsphomeEntity, platform_async_setup_entry
_LOGGER = logging.getLogger(__name__)
@@ -19,9 +18,6 @@
async def async_setup_entry(hass: HomeAssistantType,
entry: ConfigEntry, async_add_entities) -> None:
"""Set up esphome cameras based on a config entry."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import CameraInfo, CameraState # noqa
-
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='camera',
@@ -40,11 +36,11 @@ def __init__(self, entry_id: str, component_key: str, key: int):
self._image_cond = asyncio.Condition()
@property
- def _static_info(self) -> 'CameraInfo':
+ def _static_info(self) -> CameraInfo:
return super()._static_info
@property
- def _state(self) -> Optional['CameraState']:
+ def _state(self) -> Optional[CameraState]:
return super()._state
async def _on_update(self) -> None:
diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py
index 184eb4b6270deb..33ea55247876e8 100644
--- a/homeassistant/components/esphome/climate.py
+++ b/homeassistant/components/esphome/climate.py
@@ -1,6 +1,8 @@
"""Support for ESPHome climate devices."""
import logging
-from typing import TYPE_CHECKING, List, Optional
+from typing import List, Optional
+
+from aioesphomeapi import ClimateInfo, ClimateMode, ClimateState
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
@@ -12,21 +14,15 @@
ATTR_TEMPERATURE, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE,
STATE_OFF, TEMP_CELSIUS)
-from . import EsphomeEntity, platform_async_setup_entry, \
- esphome_state_property, esphome_map_enum
-
-if TYPE_CHECKING:
- # pylint: disable=unused-import
- from aioesphomeapi import ClimateInfo, ClimateState, ClimateMode # noqa
+from . import (
+ EsphomeEntity, esphome_map_enum, esphome_state_property,
+ platform_async_setup_entry)
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up ESPHome climate devices based on a config entry."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import ClimateInfo, ClimateState # noqa
-
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='climate',
@@ -37,8 +33,6 @@ async def async_setup_entry(hass, entry, async_add_entities):
@esphome_map_enum
def _climate_modes():
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import ClimateMode # noqa
return {
ClimateMode.OFF: STATE_OFF,
ClimateMode.AUTO: STATE_AUTO,
@@ -51,11 +45,11 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice):
"""A climate implementation for ESPHome."""
@property
- def _static_info(self) -> 'ClimateInfo':
+ def _static_info(self) -> ClimateInfo:
return super()._static_info
@property
- def _state(self) -> Optional['ClimateState']:
+ def _state(self) -> Optional[ClimateState]:
return super()._state
@property
diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py
index f2344e40b2a8f4..ad18e681021d56 100644
--- a/homeassistant/components/esphome/config_flow.py
+++ b/homeassistant/components/esphome/config_flow.py
@@ -7,6 +7,8 @@
from homeassistant import config_entries
from homeassistant.helpers import ConfigType
+from .entry_data import DATA_KEY, RuntimeEntryData
+
@config_entries.HANDLERS.register('esphome')
class EsphomeFlowHandler(config_entries.ConfigFlow):
@@ -76,10 +78,26 @@ async def async_step_discovery_confirm(self, user_input=None):
async def async_step_zeroconf(self, user_input: ConfigType):
"""Handle zeroconf discovery."""
- address = user_input['properties'].get(
- 'address', user_input['hostname'][:-1])
+ # Hostname is format: livingroom.local.
+ local_name = user_input['hostname'][:-1]
+ node_name = local_name[:-len('.local')]
+ address = user_input['properties'].get('address', local_name)
+
+ # Check if already configured
for entry in self._async_current_entries():
+ already_configured = False
if entry.data['host'] == address:
+ # Is this address already configured?
+ already_configured = True
+ elif entry.entry_id in self.hass.data.get(DATA_KEY, {}):
+ # Does a config entry with this name already exist?
+ data = self.hass.data[DATA_KEY][
+ entry.entry_id] # type: RuntimeEntryData
+ # Node names are unique in the network
+ if data.device_info is not None:
+ already_configured = data.device_info.name == node_name
+
+ if already_configured:
return self.async_abort(
reason='already_configured'
)
diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py
index a3ef15fa4c72f3..b69b62075dbf8c 100644
--- a/homeassistant/components/esphome/cover.py
+++ b/homeassistant/components/esphome/cover.py
@@ -1,6 +1,8 @@
"""Support for ESPHome covers."""
import logging
-from typing import TYPE_CHECKING, Optional
+from typing import Optional
+
+from aioesphomeapi import CoverInfo, CoverState
from homeassistant.components.cover import (
ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, SUPPORT_CLOSE_TILT,
@@ -9,11 +11,7 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
-from . import EsphomeEntity, platform_async_setup_entry, esphome_state_property
-
-if TYPE_CHECKING:
- # pylint: disable=unused-import
- from aioesphomeapi import CoverInfo, CoverState # noqa
+from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry
_LOGGER = logging.getLogger(__name__)
@@ -21,9 +19,6 @@
async def async_setup_entry(hass: HomeAssistantType,
entry: ConfigEntry, async_add_entities) -> None:
"""Set up ESPHome covers based on a config entry."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import CoverInfo, CoverState # noqa
-
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='cover',
@@ -36,7 +31,7 @@ class EsphomeCover(EsphomeEntity, CoverDevice):
"""A cover implementation for ESPHome."""
@property
- def _static_info(self) -> 'CoverInfo':
+ def _static_info(self) -> CoverInfo:
return super()._static_info
@property
@@ -61,7 +56,7 @@ def assumed_state(self) -> bool:
return self._static_info.assumed_state
@property
- def _state(self) -> Optional['CoverState']:
+ def _state(self) -> Optional[CoverState]:
return super()._state
@esphome_state_property
diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py
new file mode 100644
index 00000000000000..47cadc00653103
--- /dev/null
+++ b/homeassistant/components/esphome/entry_data.py
@@ -0,0 +1,107 @@
+"""Runtime entry data for ESPHome stored in hass.data."""
+import asyncio
+from typing import Any, Callable, Dict, List, Optional, Tuple
+
+from aioesphomeapi import (
+ COMPONENT_TYPE_TO_INFO, DeviceInfo, EntityInfo, EntityState, UserService)
+import attr
+
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.storage import Store
+from homeassistant.helpers.typing import HomeAssistantType
+
+DATA_KEY = 'esphome'
+DISPATCHER_UPDATE_ENTITY = 'esphome_{entry_id}_update_{component_key}_{key}'
+DISPATCHER_REMOVE_ENTITY = 'esphome_{entry_id}_remove_{component_key}_{key}'
+DISPATCHER_ON_LIST = 'esphome_{entry_id}_on_list'
+DISPATCHER_ON_DEVICE_UPDATE = 'esphome_{entry_id}_on_device_update'
+DISPATCHER_ON_STATE = 'esphome_{entry_id}_on_state'
+
+
+@attr.s
+class RuntimeEntryData:
+ """Store runtime data for esphome config entries."""
+
+ entry_id = attr.ib(type=str)
+ client = attr.ib(type='APIClient')
+ store = attr.ib(type=Store)
+ reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None)
+ state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict)
+ info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict)
+ services = attr.ib(type=Dict[int, 'UserService'], factory=dict)
+ available = attr.ib(type=bool, default=False)
+ device_info = attr.ib(type=DeviceInfo, default=None)
+ cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list)
+ disconnect_callbacks = attr.ib(type=List[Callable[[], None]], factory=list)
+
+ def async_update_entity(self, hass: HomeAssistantType, component_key: str,
+ key: int) -> None:
+ """Schedule the update of an entity."""
+ signal = DISPATCHER_UPDATE_ENTITY.format(
+ entry_id=self.entry_id, component_key=component_key, key=key)
+ async_dispatcher_send(hass, signal)
+
+ def async_remove_entity(self, hass: HomeAssistantType, component_key: str,
+ key: int) -> None:
+ """Schedule the removal of an entity."""
+ signal = DISPATCHER_REMOVE_ENTITY.format(
+ entry_id=self.entry_id, component_key=component_key, key=key)
+ async_dispatcher_send(hass, signal)
+
+ def async_update_static_infos(self, hass: HomeAssistantType,
+ infos: List[EntityInfo]) -> None:
+ """Distribute an update of static infos to all platforms."""
+ signal = DISPATCHER_ON_LIST.format(entry_id=self.entry_id)
+ async_dispatcher_send(hass, signal, infos)
+
+ def async_update_state(self, hass: HomeAssistantType,
+ state: EntityState) -> None:
+ """Distribute an update of state information to all platforms."""
+ signal = DISPATCHER_ON_STATE.format(entry_id=self.entry_id)
+ async_dispatcher_send(hass, signal, state)
+
+ def async_update_device_state(self, hass: HomeAssistantType) -> None:
+ """Distribute an update of a core device state like availability."""
+ signal = DISPATCHER_ON_DEVICE_UPDATE.format(entry_id=self.entry_id)
+ async_dispatcher_send(hass, signal)
+
+ async def async_load_from_store(self) -> Tuple[List[EntityInfo],
+ List[UserService]]:
+ """Load the retained data from store and return de-serialized data."""
+ restored = await self.store.async_load()
+ if restored is None:
+ return [], []
+
+ self.device_info = _attr_obj_from_dict(DeviceInfo,
+ **restored.pop('device_info'))
+ infos = []
+ for comp_type, restored_infos in restored.items():
+ if comp_type not in COMPONENT_TYPE_TO_INFO:
+ continue
+ for info in restored_infos:
+ cls = COMPONENT_TYPE_TO_INFO[comp_type]
+ infos.append(_attr_obj_from_dict(cls, **info))
+ services = []
+ for service in restored.get('services', []):
+ services.append(UserService.from_dict(service))
+ return infos, services
+
+ async def async_save_to_store(self) -> None:
+ """Generate dynamic data to store and save it to the filesystem."""
+ store_data = {
+ 'device_info': attr.asdict(self.device_info),
+ 'services': []
+ }
+
+ for comp_type, infos in self.info.items():
+ store_data[comp_type] = [attr.asdict(info)
+ for info in infos.values()]
+ for service in self.services.values():
+ store_data['services'].append(service.to_dict())
+
+ await self.store.async_save(store_data)
+
+
+def _attr_obj_from_dict(cls, **kwargs):
+ return cls(**{key: kwargs[key] for key in attr.fields_dict(cls)
+ if key in kwargs})
diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py
index 50cf04203f3570..255bdaa8cb1a30 100644
--- a/homeassistant/components/esphome/fan.py
+++ b/homeassistant/components/esphome/fan.py
@@ -1,6 +1,8 @@
"""Support for ESPHome fans."""
import logging
-from typing import TYPE_CHECKING, List, Optional
+from typing import List, Optional
+
+from aioesphomeapi import FanInfo, FanSpeed, FanState
from homeassistant.components.fan import (
SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, SUPPORT_OSCILLATE,
@@ -8,12 +10,9 @@
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
-from . import EsphomeEntity, platform_async_setup_entry, \
- esphome_state_property, esphome_map_enum
-
-if TYPE_CHECKING:
- # pylint: disable=unused-import
- from aioesphomeapi import FanInfo, FanState, FanSpeed # noqa
+from . import (
+ EsphomeEntity, esphome_map_enum, esphome_state_property,
+ platform_async_setup_entry)
_LOGGER = logging.getLogger(__name__)
@@ -21,9 +20,6 @@
async def async_setup_entry(hass: HomeAssistantType,
entry: ConfigEntry, async_add_entities) -> None:
"""Set up ESPHome fans based on a config entry."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import FanInfo, FanState # noqa
-
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='fan',
@@ -34,8 +30,6 @@ async def async_setup_entry(hass: HomeAssistantType,
@esphome_map_enum
def _fan_speeds():
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import FanSpeed # noqa
return {
FanSpeed.LOW: SPEED_LOW,
FanSpeed.MEDIUM: SPEED_MEDIUM,
@@ -47,11 +41,11 @@ class EsphomeFan(EsphomeEntity, FanEntity):
"""A fan implementation for ESPHome."""
@property
- def _static_info(self) -> 'FanInfo':
+ def _static_info(self) -> FanInfo:
return super()._static_info
@property
- def _state(self) -> Optional['FanState']:
+ def _state(self) -> Optional[FanState]:
return super()._state
async def async_set_speed(self, speed: str) -> None:
diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py
index 6b4abafe62b95e..f94229d61cc6d8 100644
--- a/homeassistant/components/esphome/light.py
+++ b/homeassistant/components/esphome/light.py
@@ -1,6 +1,8 @@
"""Support for ESPHome lights."""
import logging
-from typing import TYPE_CHECKING, List, Optional, Tuple
+from typing import List, Optional, Tuple
+
+from aioesphomeapi import LightInfo, LightState
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
@@ -11,11 +13,7 @@
from homeassistant.helpers.typing import HomeAssistantType
import homeassistant.util.color as color_util
-from . import EsphomeEntity, platform_async_setup_entry, esphome_state_property
-
-if TYPE_CHECKING:
- # pylint: disable=unused-import
- from aioesphomeapi import LightInfo, LightState # noqa
+from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry
_LOGGER = logging.getLogger(__name__)
@@ -29,9 +27,6 @@
async def async_setup_entry(hass: HomeAssistantType,
entry: ConfigEntry, async_add_entities) -> None:
"""Set up ESPHome lights based on a config entry."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import LightInfo, LightState # noqa
-
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='light',
@@ -44,11 +39,11 @@ class EsphomeLight(EsphomeEntity, Light):
"""A switch implementation for ESPHome."""
@property
- def _static_info(self) -> 'LightInfo':
+ def _static_info(self) -> LightInfo:
return super()._static_info
@property
- def _state(self) -> Optional['LightState']:
+ def _state(self) -> Optional[LightState]:
return super()._state
@esphome_state_property
diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json
index 71d233fee2ed7b..a986a8641897b6 100644
--- a/homeassistant/components/esphome/manifest.json
+++ b/homeassistant/components/esphome/manifest.json
@@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/esphome",
"requirements": [
- "aioesphomeapi==2.0.1"
+ "aioesphomeapi==2.1.0"
],
"dependencies": [],
"zeroconf": ["_esphomelib._tcp.local."],
diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py
index 8d8fb938c6867b..a5a530b49f1586 100644
--- a/homeassistant/components/esphome/sensor.py
+++ b/homeassistant/components/esphome/sensor.py
@@ -1,17 +1,15 @@
"""Support for esphome sensors."""
import logging
import math
-from typing import TYPE_CHECKING, Optional
+from typing import Optional
+
+from aioesphomeapi import (
+ SensorInfo, SensorState, TextSensorInfo, TextSensorState)
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
-from . import EsphomeEntity, platform_async_setup_entry, esphome_state_property
-
-if TYPE_CHECKING:
- # pylint: disable=unused-import
- from aioesphomeapi import ( # noqa
- SensorInfo, SensorState, TextSensorInfo, TextSensorState)
+from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry
_LOGGER = logging.getLogger(__name__)
@@ -19,10 +17,6 @@
async def async_setup_entry(hass: HomeAssistantType,
entry: ConfigEntry, async_add_entities) -> None:
"""Set up esphome sensors based on a config entry."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import ( # noqa
- SensorInfo, SensorState, TextSensorInfo, TextSensorState)
-
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='sensor',
@@ -41,11 +35,11 @@ class EsphomeSensor(EsphomeEntity):
"""A sensor implementation for esphome."""
@property
- def _static_info(self) -> 'SensorInfo':
+ def _static_info(self) -> SensorInfo:
return super()._static_info
@property
- def _state(self) -> Optional['SensorState']:
+ def _state(self) -> Optional[SensorState]:
return super()._state
@property
diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py
index 77994d0be58f03..d209df8cd834b6 100644
--- a/homeassistant/components/esphome/switch.py
+++ b/homeassistant/components/esphome/switch.py
@@ -1,16 +1,14 @@
"""Support for ESPHome switches."""
import logging
-from typing import TYPE_CHECKING, Optional
+from typing import Optional
+
+from aioesphomeapi import SwitchInfo, SwitchState
from homeassistant.components.switch import SwitchDevice
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.typing import HomeAssistantType
-from . import EsphomeEntity, platform_async_setup_entry, esphome_state_property
-
-if TYPE_CHECKING:
- # pylint: disable=unused-import
- from aioesphomeapi import SwitchInfo, SwitchState # noqa
+from . import EsphomeEntity, esphome_state_property, platform_async_setup_entry
_LOGGER = logging.getLogger(__name__)
@@ -18,9 +16,6 @@
async def async_setup_entry(hass: HomeAssistantType,
entry: ConfigEntry, async_add_entities) -> None:
"""Set up ESPHome switches based on a config entry."""
- # pylint: disable=redefined-outer-name
- from aioesphomeapi import SwitchInfo, SwitchState # noqa
-
await platform_async_setup_entry(
hass, entry, async_add_entities,
component_key='switch',
@@ -33,11 +28,11 @@ class EsphomeSwitch(EsphomeEntity, SwitchDevice):
"""A switch implementation for ESPHome."""
@property
- def _static_info(self) -> 'SwitchInfo':
+ def _static_info(self) -> SwitchInfo:
return super()._static_info
@property
- def _state(self) -> Optional['SwitchState']:
+ def _state(self) -> Optional[SwitchState]:
return super()._state
@property
diff --git a/homeassistant/components/essent/manifest.json b/homeassistant/components/essent/manifest.json
index 49189f6bacb15b..41313cb44a918a 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.10"],
+ "requirements": ["PyEssent==0.12"],
"dependencies": [],
"codeowners": ["@TheLastProject"]
}
diff --git a/homeassistant/components/essent/sensor.py b/homeassistant/components/essent/sensor.py
index 545ed3d5baf5f5..e77b256abb73e0 100644
--- a/homeassistant/components/essent/sensor.py
+++ b/homeassistant/components/essent/sensor.py
@@ -36,6 +36,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
tariff,
data['values']['LVR'][tariff]['unit']))
+ if not meters:
+ hass.components.persistent_notification.create(
+ 'Couldn\'t find any meter readings. '
+ 'Please ensure Verbruiks Manager is enabled in Mijn Essent '
+ 'and at least one reading has been logged to Meterstanden.',
+ title='Essent', notification_id='essent_notification')
+ return
+
add_devices(meters, True)
@@ -46,14 +54,13 @@ def __init__(self, username, password):
"""Initialize the Essent API."""
self._username = username
self._password = password
- self._meters = []
self._meter_data = {}
self.update()
def retrieve_meters(self):
"""Retrieve the list of meters."""
- return self._meters
+ return self._meter_data.keys()
def retrieve_meter_data(self, meter):
"""Retrieve the data for this meter."""
@@ -63,10 +70,12 @@ def retrieve_meter_data(self, meter):
def update(self):
"""Retrieve the latest meter data from Essent."""
essent = PyEssent(self._username, self._password)
- self._meters = essent.get_EANs()
- for meter in self._meters:
- self._meter_data[meter] = essent.read_meter(
- meter, only_last_meter_reading=True)
+ eans = essent.get_EANs()
+ for possible_meter in eans:
+ meter_data = essent.read_meter(
+ possible_meter, only_last_meter_reading=True)
+ if meter_data:
+ self._meter_data[possible_meter] = meter_data
class EssentMeter(Entity):
diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json
index baf0d8aaed1db4..6a6316d80a3f6a 100644
--- a/homeassistant/components/fitbit/manifest.json
+++ b/homeassistant/components/fitbit/manifest.json
@@ -3,7 +3,7 @@
"name": "Fitbit",
"documentation": "https://www.home-assistant.io/components/fitbit",
"requirements": [
- "fitbit==0.3.0"
+ "fitbit==0.3.1"
],
"dependencies": [
"configurator",
diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py
index 8d7f7213787a13..a18ed6eb3d1d2e 100644
--- a/homeassistant/components/frontend/__init__.py
+++ b/homeassistant/components/frontend/__init__.py
@@ -1,13 +1,13 @@
"""Handle the frontend for Home Assistant."""
-import asyncio
import json
import logging
import os
import pathlib
-from aiohttp import web
+from aiohttp import web, web_urldispatcher, hdrs
import voluptuous as vol
import jinja2
+from yarl import URL
import homeassistant.helpers.config_validation as cv
from homeassistant.components.http.view import HomeAssistantView
@@ -26,6 +26,7 @@
CONF_EXTRA_HTML_URL_ES5 = 'extra_html_url_es5'
CONF_FRONTEND_REPO = 'development_repo'
CONF_JS_VERSION = 'javascript_version'
+EVENT_PANELS_UPDATED = 'panels_updated'
DEFAULT_THEME_COLOR = '#03A9F4'
@@ -50,7 +51,6 @@
'type': 'image/png'
})
-DATA_FINALIZE_PANEL = 'frontend_finalize_panel'
DATA_PANELS = 'frontend_panels'
DATA_JS_VERSION = 'frontend_js_version'
DATA_EXTRA_HTML_URL = 'frontend_extra_html_url'
@@ -128,15 +128,6 @@ def __init__(self, component_name, sidebar_title, sidebar_icon,
self.config = config
self.require_admin = require_admin
- @callback
- def async_register_index_routes(self, router, index_view):
- """Register routes for panel to be served by index view."""
- router.add_route(
- 'get', '/{}'.format(self.frontend_url_path), index_view.get)
- router.add_route(
- 'get', '/{}/{{extra:.+}}'.format(self.frontend_url_path),
- index_view.get)
-
@callback
def to_response(self):
"""Panel as dictionary."""
@@ -151,26 +142,36 @@ def to_response(self):
@bind_hass
-async def async_register_built_in_panel(hass, component_name,
- sidebar_title=None, sidebar_icon=None,
- frontend_url_path=None, config=None,
- require_admin=False):
+@callback
+def async_register_built_in_panel(hass, component_name,
+ sidebar_title=None, sidebar_icon=None,
+ frontend_url_path=None, config=None,
+ require_admin=False):
"""Register a built-in panel."""
panel = Panel(component_name, sidebar_title, sidebar_icon,
frontend_url_path, config, require_admin)
- panels = hass.data.get(DATA_PANELS)
- if panels is None:
- panels = hass.data[DATA_PANELS] = {}
+ panels = hass.data.setdefault(DATA_PANELS, {})
if panel.frontend_url_path in panels:
_LOGGER.warning("Overwriting component %s", panel.frontend_url_path)
- if DATA_FINALIZE_PANEL in hass.data:
- hass.data[DATA_FINALIZE_PANEL](panel)
-
panels[panel.frontend_url_path] = panel
+ hass.bus.async_fire(EVENT_PANELS_UPDATED)
+
+
+@bind_hass
+@callback
+def async_remove_panel(hass, frontend_url_path):
+ """Remove a built-in panel."""
+ panel = hass.data.get(DATA_PANELS, {}).pop(frontend_url_path, None)
+
+ if panel is None:
+ _LOGGER.warning("Removing unknown panel %s", frontend_url_path)
+
+ hass.bus.async_fire(EVENT_PANELS_UPDATED)
+
@bind_hass
@callback
@@ -233,28 +234,14 @@ async def async_setup(hass, config):
if os.path.isdir(local):
hass.http.register_static_path("/local", local, not is_dev)
- index_view = IndexView(repo_path)
- hass.http.register_view(index_view)
-
- @callback
- def async_finalize_panel(panel):
- """Finalize setup of a panel."""
- panel.async_register_index_routes(hass.http.app.router, index_view)
-
- await asyncio.wait(
- [async_register_built_in_panel(hass, panel) for panel in (
- 'kiosk', 'states', 'profile')])
- await asyncio.wait(
- [async_register_built_in_panel(hass, panel, require_admin=True)
- for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
- 'dev-template', 'dev-mqtt')])
+ hass.http.app.router.register_resource(IndexView(repo_path, hass))
- hass.data[DATA_FINALIZE_PANEL] = async_finalize_panel
+ for panel in ('kiosk', 'states', 'profile'):
+ async_register_built_in_panel(hass, panel)
- # Finalize registration of panels that registered before frontend was setup
- # This includes the built-in panels from line above.
- for panel in hass.data[DATA_PANELS].values():
- async_finalize_panel(panel)
+ for panel in ('dev-event', 'dev-info', 'dev-service', 'dev-state',
+ 'dev-template', 'dev-mqtt'):
+ async_register_built_in_panel(hass, panel, require_admin=True)
if DATA_EXTRA_HTML_URL not in hass.data:
hass.data[DATA_EXTRA_HTML_URL] = set()
@@ -318,18 +305,64 @@ def reload_themes(_):
hass.services.async_register(DOMAIN, SERVICE_RELOAD_THEMES, reload_themes)
-class IndexView(HomeAssistantView):
+class IndexView(web_urldispatcher.AbstractResource):
"""Serve the frontend."""
- url = '/'
- name = 'frontend:index'
- requires_auth = False
-
- def __init__(self, repo_path):
+ def __init__(self, repo_path, hass):
"""Initialize the frontend view."""
+ super().__init__(name="frontend:index")
self.repo_path = repo_path
+ self.hass = hass
self._template_cache = None
+ @property
+ def canonical(self) -> str:
+ """Return resource's canonical path."""
+ return '/'
+
+ @property
+ def _route(self):
+ """Return the index route."""
+ return web_urldispatcher.ResourceRoute('GET', self.get, self)
+
+ def url_for(self, **kwargs: str) -> URL:
+ """Construct url for resource with additional params."""
+ return URL("/")
+
+ async def resolve(self, request: web.Request):
+ """Resolve resource.
+
+ Return (UrlMappingMatchInfo, allowed_methods) pair.
+ """
+ if (request.path != '/' and
+ request.url.parts[1] not in self.hass.data[DATA_PANELS]):
+ return None, set()
+
+ if request.method != hdrs.METH_GET:
+ return None, {'GET'}
+
+ return web_urldispatcher.UrlMappingMatchInfo({}, self._route), {'GET'}
+
+ def add_prefix(self, prefix: str) -> None:
+ """Add a prefix to processed URLs.
+
+ Required for subapplications support.
+ """
+
+ def get_info(self):
+ """Return a dict with additional info useful for introspection."""
+ return {
+ 'panels': list(self.hass.data[DATA_PANELS])
+ }
+
+ def freeze(self) -> None:
+ """Freeze the resource."""
+ pass
+
+ def raw_match(self, path: str) -> bool:
+ """Perform a raw match against path."""
+ pass
+
def get_template(self):
"""Get template."""
tpl = self._template_cache
@@ -345,8 +378,8 @@ def get_template(self):
return tpl
- async def get(self, request, extra=None):
- """Serve the index view."""
+ async def get(self, request: web.Request):
+ """Serve the index page for panel pages."""
hass = request.app['hass']
if not hass.components.onboarding.async_is_onboarded():
@@ -367,6 +400,14 @@ async def get(self, request, extra=None):
content_type='text/html'
)
+ def __len__(self) -> int:
+ """Return length of resource."""
+ return 1
+
+ def __iter__(self):
+ """Iterate over routes."""
+ return iter([self._route])
+
class ManifestJSONView(HomeAssistantView):
"""View to return a manifest.json."""
diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json
index 1150c70d0d8d8c..0d517aa6560523 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==20190523.0"
+ "home-assistant-frontend==20190604.0"
],
"dependencies": [
"api",
diff --git a/homeassistant/components/geofency/device_tracker.py b/homeassistant/components/geofency/device_tracker.py
index e340272c966024..f9a7df638eb80f 100644
--- a/homeassistant/components/geofency/device_tracker.py
+++ b/homeassistant/components/geofency/device_tracker.py
@@ -1,12 +1,18 @@
"""Support for the Geofency device tracker platform."""
import logging
+from homeassistant.const import (
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+)
from homeassistant.core import callback
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import (
DeviceTrackerEntity
)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers import device_registry
from . import DOMAIN as GF_DOMAIN, TRACKER_UPDATE
@@ -30,19 +36,33 @@ def _receive_data(device, gps, location_name, attributes):
hass.data[GF_DOMAIN]['unsub_device_tracker'][config_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] == GF_DOMAIN
+ }
+
+ if dev_ids:
+ hass.data[GF_DOMAIN]['devices'].update(dev_ids)
+ async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids)
+
return True
-class GeofencyEntity(DeviceTrackerEntity):
+class GeofencyEntity(DeviceTrackerEntity, RestoreEntity):
"""Represent a tracked device."""
- def __init__(self, device, gps, location_name, attributes):
+ def __init__(self, device, gps=None, location_name=None, attributes=None):
"""Set up Geofency entity."""
- self._attributes = attributes
+ self._attributes = attributes or {}
self._name = device
self._location_name = location_name
self._gps = gps
self._unsub_dispatcher = None
+ self._unique_id = device
@property
def device_state_attributes(self):
@@ -74,6 +94,19 @@ 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': {(GF_DOMAIN, self._unique_id)},
+ }
+
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
@@ -81,12 +114,27 @@ def source_type(self):
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)
+ if self._attributes:
+ return
+
+ state = await self.async_get_last_state()
+
+ if state is None:
+ self._gps = (None, None)
+ return
+
+ attr = state.attributes
+ self._gps = (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE))
+
async def async_will_remove_from_hass(self):
"""Clean up after entity before removal."""
+ await super().async_will_remove_from_hass()
self._unsub_dispatcher()
+ self.hass.data[GF_DOMAIN]['devices'].remove(self._unique_id)
@callback
def _async_receive_data(self, device, gps, location_name, attributes):
diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py
index 54cbf34fdfc2c8..1d59a5e4f21a5e 100644
--- a/homeassistant/components/gitlab_ci/sensor.py
+++ b/homeassistant/components/gitlab_ci/sensor.py
@@ -30,7 +30,7 @@
ICON_HAPPY = 'mdi:emoticon-happy'
ICON_OTHER = 'mdi:git'
-ICON_SAD = 'mdi:emoticon-happy'
+ICON_SAD = 'mdi:emoticon-sad'
SCAN_INTERVAL = timedelta(seconds=300)
diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py
index e9bbf3f96cdd9f..027a6b2f56863a 100644
--- a/homeassistant/components/google/__init__.py
+++ b/homeassistant/components/google/__init__.py
@@ -1,4 +1,5 @@
"""Support for Google - Calendar Event Devices."""
+from datetime import timedelta, datetime
import logging
import os
import yaml
@@ -35,17 +36,32 @@
DEFAULT_CONF_TRACK_NEW = True
DEFAULT_CONF_OFFSET = '!!'
+EVENT_CALENDAR_ID = 'calendar_id'
+EVENT_DESCRIPTION = 'description'
+EVENT_END_CONF = 'end'
+EVENT_END_DATE = 'end_date'
+EVENT_END_DATETIME = 'end_date_time'
+EVENT_IN = 'in'
+EVENT_IN_DAYS = 'days'
+EVENT_IN_WEEKS = 'weeks'
+EVENT_START_CONF = 'start'
+EVENT_START_DATE = 'start_date'
+EVENT_START_DATETIME = 'start_date_time'
+EVENT_SUMMARY = 'summary'
+EVENT_TYPES_CONF = 'event_types'
+
NOTIFICATION_ID = 'google_calendar_notification'
-NOTIFICATION_TITLE = 'Google Calendar Setup'
+NOTIFICATION_TITLE = "Google Calendar Setup"
GROUP_NAME_ALL_CALENDARS = "Google Calendar Sensors"
SERVICE_SCAN_CALENDARS = 'scan_for_calendars'
SERVICE_FOUND_CALENDARS = 'found_calendar'
+SERVICE_ADD_EVENT = 'add_event'
DATA_INDEX = 'google_calendars'
YAML_DEVICES = '{}_calendars.yaml'.format(DOMAIN)
-SCOPES = 'https://www.googleapis.com/auth/calendar.readonly'
+SCOPES = 'https://www.googleapis.com/auth/calendar'
TOKEN_FILE = '.{}.token'.format(DOMAIN)
@@ -73,6 +89,27 @@
vol.All(cv.ensure_list, [_SINGLE_CALSEARCH_CONFIG]),
}, extra=vol.ALLOW_EXTRA)
+_EVENT_IN_TYPES = vol.Schema(
+ {
+ vol.Exclusive(EVENT_IN_DAYS, EVENT_TYPES_CONF): cv.positive_int,
+ vol.Exclusive(EVENT_IN_WEEKS, EVENT_TYPES_CONF): cv.positive_int,
+ }
+)
+
+ADD_EVENT_SERVICE_SCHEMA = vol.Schema(
+ {
+ vol.Required(EVENT_CALENDAR_ID): cv.string,
+ vol.Required(EVENT_SUMMARY): cv.string,
+ vol.Optional(EVENT_DESCRIPTION, default=""): cv.string,
+ vol.Exclusive(EVENT_START_DATE, EVENT_START_CONF): cv.date,
+ vol.Exclusive(EVENT_END_DATE, EVENT_END_CONF): cv.date,
+ vol.Exclusive(EVENT_START_DATETIME, EVENT_START_CONF): cv.datetime,
+ vol.Exclusive(EVENT_END_DATETIME, EVENT_END_CONF): cv.datetime,
+ vol.Exclusive(EVENT_IN, EVENT_START_CONF, EVENT_END_CONF):
+ _EVENT_IN_TYPES
+ }
+)
+
def do_authentication(hass, hass_config, config):
"""Notify user of actions and authenticate.
@@ -87,10 +124,9 @@ def do_authentication(hass, hass_config, config):
oauth = OAuth2WebServerFlow(
client_id=config[CONF_CLIENT_ID],
client_secret=config[CONF_CLIENT_SECRET],
- scope='https://www.googleapis.com/auth/calendar.readonly',
+ scope='https://www.googleapis.com/auth/calendar',
redirect_uri='Home-Assistant.io',
)
-
try:
dev_flow = oauth.step1_get_device_and_user_codes()
except OAuth2DeviceCodeError as err:
@@ -155,8 +191,20 @@ def setup(hass, config):
if not os.path.isfile(token_file):
do_authentication(hass, config, conf)
else:
- do_setup(hass, config, conf)
+ if not check_correct_scopes(token_file):
+ do_authentication(hass, config, conf)
+ else:
+ do_setup(hass, config, conf)
+
+ return True
+
+def check_correct_scopes(token_file):
+ """Check for the correct scopes in file."""
+ tokenfile = open(token_file, "r").read()
+ if "readonly" in tokenfile:
+ _LOGGER.warning("Please re-authenticate with Google.")
+ return False
return True
@@ -195,6 +243,61 @@ def _scan_for_calendars(service):
hass.services.register(
DOMAIN, SERVICE_SCAN_CALENDARS, _scan_for_calendars)
+
+ def _add_event(call):
+ """Add a new event to calendar."""
+ service = calendar_service.get()
+ start = {}
+ end = {}
+
+ if EVENT_IN in call.data:
+ if EVENT_IN_DAYS in call.data[EVENT_IN]:
+ now = datetime.now()
+
+ start_in = now + timedelta(
+ days=call.data[EVENT_IN][EVENT_IN_DAYS])
+ end_in = start_in + timedelta(days=1)
+
+ start = {'date': start_in.strftime('%Y-%m-%d')}
+ end = {'date': end_in.strftime('%Y-%m-%d')}
+
+ elif EVENT_IN_WEEKS in call.data[EVENT_IN]:
+ now = datetime.now()
+
+ start_in = now + timedelta(
+ weeks=call.data[EVENT_IN][EVENT_IN_WEEKS])
+ end_in = start_in + timedelta(days=1)
+
+ start = {'date': start_in.strftime('%Y-%m-%d')}
+ end = {'date': end_in.strftime('%Y-%m-%d')}
+
+ elif EVENT_START_DATE in call.data:
+ start = {'date': str(call.data[EVENT_START_DATE])}
+ end = {'date': str(call.data[EVENT_END_DATE])}
+
+ elif EVENT_START_DATETIME in call.data:
+ start_dt = str(call.data[EVENT_START_DATETIME]
+ .strftime('%Y-%m-%dT%H:%M:%S'))
+ end_dt = str(call.data[EVENT_END_DATETIME]
+ .strftime('%Y-%m-%dT%H:%M:%S'))
+ start = {'dateTime': start_dt,
+ 'timeZone': str(hass.config.time_zone)}
+ end = {'dateTime': end_dt,
+ 'timeZone': str(hass.config.time_zone)}
+
+ event = {
+ 'summary': call.data[EVENT_SUMMARY],
+ 'description': call.data[EVENT_DESCRIPTION],
+ 'start': start,
+ 'end': end,
+ }
+ service_data = {'calendarId': call.data[EVENT_CALENDAR_ID],
+ 'body': event}
+ event = service.events().insert(**service_data).execute()
+
+ hass.services.register(
+ DOMAIN, SERVICE_ADD_EVENT, _add_event, schema=ADD_EVENT_SERVICE_SCHEMA
+ )
return True
diff --git a/homeassistant/components/google/services.yaml b/homeassistant/components/google/services.yaml
index 34eecb33fd5e47..048e886dc4e565 100644
--- a/homeassistant/components/google/services.yaml
+++ b/homeassistant/components/google/services.yaml
@@ -2,3 +2,30 @@ found_calendar:
description: Add calendar if it has not been already discovered.
scan_for_calendars:
description: Scan for new calendars.
+add_event:
+ description: Add a new calendar event.
+ fields:
+ calendar_id:
+ description: The id of the calendar you want.
+ example: 'Your email'
+ summary:
+ description: Acts as the title of the event.
+ example: 'Bowling'
+ description:
+ description: The description of the event. Optional.
+ example: 'Birthday bowling'
+ start_date_time:
+ description: The date and time the event should start.
+ example: '2019-03-22 20:00:00'
+ end_date_time:
+ description: The date and time the event should end.
+ example: '2019-03-22 22:00:00'
+ start_date:
+ description: The date the whole day event should start.
+ example: '2019-03-10'
+ end_date:
+ description: The date the whole day event should end.
+ example: '2019-03-11'
+ in:
+ description: Days or weeks that you want to create the event in.
+ example: '"days": 2 or "weeks": 2'
\ No newline at end of file
diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py
index 92afe90a5ac4e9..ebded79447e725 100644
--- a/homeassistant/components/google_assistant/const.py
+++ b/homeassistant/components/google_assistant/const.py
@@ -12,6 +12,7 @@
media_player,
scene,
script,
+ sensor,
switch,
vacuum,
)
@@ -108,6 +109,7 @@
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR,
(media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV,
(media_player.DOMAIN, media_player.DEVICE_CLASS_SPEAKER): TYPE_SPEAKER,
+ (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR,
}
CHALLENGE_ACK_NEEDED = 'ackNeeded'
diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py
index 4d3f2855b31deb..770a502ad5dbdb 100644
--- a/homeassistant/components/google_assistant/helpers.py
+++ b/homeassistant/components/google_assistant/helpers.py
@@ -1,17 +1,18 @@
"""Helper classes for Google Assistant integration."""
from asyncio import gather
from collections.abc import Mapping
+from typing import List
from homeassistant.core import Context, callback
from homeassistant.const import (
CONF_NAME, STATE_UNAVAILABLE, ATTR_SUPPORTED_FEATURES,
- ATTR_DEVICE_CLASS
+ ATTR_DEVICE_CLASS, CLOUD_NEVER_EXPOSED_ENTITIES
)
from . import trait
from .const import (
DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED,
- DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT,
+ DEVICE_CLASS_TO_GOOGLE_TYPES, CONF_ROOM_HINT
)
from .error import SmartHomeError
@@ -21,15 +22,20 @@ class Config:
def __init__(self, should_expose,
entity_config=None, secure_devices_pin=None,
- agent_user_id=None):
+ agent_user_id=None, should_2fa=None):
"""Initialize the configuration."""
self.should_expose = should_expose
self.entity_config = entity_config or {}
self.secure_devices_pin = secure_devices_pin
+ self._should_2fa = should_2fa
# Agent User Id to use for query responses
self.agent_user_id = agent_user_id
+ def should_2fa(self, state):
+ """If an entity should have 2FA checked."""
+ return self._should_2fa is None or self._should_2fa(state)
+
class RequestData:
"""Hold data associated with a particular request."""
@@ -79,6 +85,22 @@ def traits(self):
if Trait.supported(domain, features, device_class)]
return self._traits
+ @callback
+ def is_supported(self) -> bool:
+ """Return if the entity is supported by Google."""
+ return self.state.state != STATE_UNAVAILABLE and bool(self.traits())
+
+ @callback
+ def might_2fa(self) -> bool:
+ """Return if the entity might encounter 2FA."""
+ state = self.state
+ domain = state.domain
+ features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
+ device_class = state.attributes.get(ATTR_DEVICE_CLASS)
+
+ return any(trait.might_2fa(domain, features, device_class)
+ for trait in self.traits())
+
async def sync_serialize(self):
"""Serialize entity for a SYNC response.
@@ -86,27 +108,13 @@ async def sync_serialize(self):
"""
state = self.state
- # When a state is unavailable, the attributes that describe
- # capabilities will be stripped. For example, a light entity will miss
- # the min/max mireds. Therefore they will be excluded from a sync.
- if state.state == STATE_UNAVAILABLE:
- return None
-
entity_config = self.config.entity_config.get(state.entity_id, {})
name = (entity_config.get(CONF_NAME) or state.name).strip()
domain = state.domain
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
- # If an empty string
- if not name:
- return None
-
traits = self.traits()
- # Found no supported traits for this entity
- if not traits:
- return None
-
device_type = get_google_type(domain,
device_class)
@@ -213,3 +221,19 @@ def deep_update(target, source):
else:
target[key] = value
return target
+
+
+@callback
+def async_get_entities(hass, config) -> List[GoogleEntity]:
+ """Return all entities that are supported by Google."""
+ entities = []
+ for state in hass.states.async_all():
+ if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
+ continue
+
+ entity = GoogleEntity(hass, config, state)
+
+ if entity.is_supported():
+ entities.append(entity)
+
+ return entities
diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py
index 1ec47bbedd60af..07548ee95eb8e0 100644
--- a/homeassistant/components/google_assistant/smart_home.py
+++ b/homeassistant/components/google_assistant/smart_home.py
@@ -1,17 +1,17 @@
"""Support for Google Assistant Smart Home API."""
+import asyncio
from itertools import product
import logging
from homeassistant.util.decorator import Registry
-from homeassistant.const import (
- CLOUD_NEVER_EXPOSED_ENTITIES, ATTR_ENTITY_ID)
+from homeassistant.const import ATTR_ENTITY_ID
from .const import (
ERR_PROTOCOL_ERROR, ERR_DEVICE_OFFLINE, ERR_UNKNOWN_ERROR,
EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED, EVENT_QUERY_RECEIVED
)
-from .helpers import RequestData, GoogleEntity
+from .helpers import RequestData, GoogleEntity, async_get_entities
from .error import SmartHomeError
HANDLERS = Registry()
@@ -81,22 +81,11 @@ async def async_devices_sync(hass, data, payload):
{'request_id': data.request_id},
context=data.context)
- devices = []
- for state in hass.states.async_all():
- if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
- continue
-
- if not data.config.should_expose(state):
- continue
-
- entity = GoogleEntity(hass, data.config, state)
- serialized = await entity.sync_serialize()
-
- if serialized is None:
- _LOGGER.debug("No mapping for %s domain", entity.state)
- continue
-
- devices.append(serialized)
+ devices = await asyncio.gather(*[
+ entity.sync_serialize() for entity in
+ async_get_entities(hass, data.config)
+ if data.config.should_expose(entity.state)
+ ])
response = {
'agentUserId': data.config.agent_user_id or data.context.user_id,
diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py
index cb2bf688ad0e77..7776daf65c954f 100644
--- a/homeassistant/components/google_assistant/trait.py
+++ b/homeassistant/components/google_assistant/trait.py
@@ -13,6 +13,7 @@
lock,
scene,
script,
+ sensor,
switch,
vacuum,
)
@@ -104,6 +105,11 @@ class _Trait:
commands = []
+ @staticmethod
+ def might_2fa(domain, features, device_class):
+ """Return if the trait might ask for 2FA."""
+ return False
+
def __init__(self, hass, state, config):
"""Initialize a trait for a state."""
self.hass = hass
@@ -545,89 +551,126 @@ class TemperatureSettingTrait(_Trait):
@staticmethod
def supported(domain, features, device_class):
"""Test if state is supported."""
- if domain != climate.DOMAIN:
- return False
+ if domain == climate.DOMAIN:
+ return features & climate.SUPPORT_OPERATION_MODE
- return features & climate.SUPPORT_OPERATION_MODE
+ return (domain == sensor.DOMAIN
+ and device_class == sensor.DEVICE_CLASS_TEMPERATURE)
def sync_attributes(self):
"""Return temperature point and modes attributes for a sync request."""
- modes = []
- supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
+ response = {}
+ attrs = self.state.attributes
+ domain = self.state.domain
+ response['thermostatTemperatureUnit'] = _google_temp_unit(
+ self.hass.config.units.temperature_unit)
- if supported & climate.SUPPORT_ON_OFF != 0:
- modes.append(STATE_OFF)
- modes.append(STATE_ON)
+ if domain == sensor.DOMAIN:
+ device_class = attrs.get(ATTR_DEVICE_CLASS)
+ if device_class == sensor.DEVICE_CLASS_TEMPERATURE:
+ response["queryOnlyTemperatureSetting"] = True
- if supported & climate.SUPPORT_OPERATION_MODE != 0:
- for mode in self.state.attributes.get(climate.ATTR_OPERATION_LIST,
- []):
- google_mode = self.hass_to_google.get(mode)
- if google_mode and google_mode not in modes:
- modes.append(google_mode)
+ elif domain == climate.DOMAIN:
+ modes = []
+ supported = attrs.get(ATTR_SUPPORTED_FEATURES)
- return {
- 'availableThermostatModes': ','.join(modes),
- 'thermostatTemperatureUnit': _google_temp_unit(
- self.hass.config.units.temperature_unit)
- }
+ if supported & climate.SUPPORT_ON_OFF != 0:
+ modes.append(STATE_OFF)
+ modes.append(STATE_ON)
+
+ if supported & climate.SUPPORT_OPERATION_MODE != 0:
+ for mode in attrs.get(climate.ATTR_OPERATION_LIST, []):
+ google_mode = self.hass_to_google.get(mode)
+ if google_mode and google_mode not in modes:
+ modes.append(google_mode)
+ response['availableThermostatModes'] = ','.join(modes)
+
+ return response
def query_attributes(self):
"""Return temperature point and modes query attributes."""
- attrs = self.state.attributes
response = {}
-
- operation = attrs.get(climate.ATTR_OPERATION_MODE)
- supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES)
-
- if (supported & climate.SUPPORT_ON_OFF
- and self.state.state == STATE_OFF):
- response['thermostatMode'] = 'off'
- elif (supported & climate.SUPPORT_OPERATION_MODE and
- operation in self.hass_to_google):
- response['thermostatMode'] = self.hass_to_google[operation]
- elif supported & climate.SUPPORT_ON_OFF:
- response['thermostatMode'] = 'on'
-
+ attrs = self.state.attributes
+ domain = self.state.domain
unit = self.hass.config.units.temperature_unit
-
- current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE)
- if current_temp is not None:
- response['thermostatTemperatureAmbient'] = \
- round(temp_util.convert(current_temp, unit, TEMP_CELSIUS), 1)
-
- current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY)
- if current_humidity is not None:
- response['thermostatHumidityAmbient'] = current_humidity
-
- if operation == climate.STATE_AUTO:
- if (supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and
- supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
- response['thermostatTemperatureSetpointHigh'] = \
+ if domain == sensor.DOMAIN:
+ device_class = attrs.get(ATTR_DEVICE_CLASS)
+ if device_class == sensor.DEVICE_CLASS_TEMPERATURE:
+ current_temp = self.state.state
+ if current_temp is not None:
+ response['thermostatTemperatureAmbient'] = \
+ round(temp_util.convert(
+ float(current_temp),
+ unit,
+ TEMP_CELSIUS
+ ), 1)
+
+ elif domain == climate.DOMAIN:
+ operation = attrs.get(climate.ATTR_OPERATION_MODE)
+ supported = attrs.get(ATTR_SUPPORTED_FEATURES)
+
+ if (supported & climate.SUPPORT_ON_OFF
+ and self.state.state == STATE_OFF):
+ response['thermostatMode'] = 'off'
+ elif (supported & climate.SUPPORT_OPERATION_MODE
+ and operation in self.hass_to_google):
+ response['thermostatMode'] = self.hass_to_google[operation]
+ elif supported & climate.SUPPORT_ON_OFF:
+ response['thermostatMode'] = 'on'
+
+ current_temp = attrs.get(climate.ATTR_CURRENT_TEMPERATURE)
+ if current_temp is not None:
+ response['thermostatTemperatureAmbient'] = \
round(temp_util.convert(
- attrs[climate.ATTR_TARGET_TEMP_HIGH],
- unit, TEMP_CELSIUS), 1)
- response['thermostatTemperatureSetpointLow'] = \
- round(temp_util.convert(
- attrs[climate.ATTR_TARGET_TEMP_LOW],
- unit, TEMP_CELSIUS), 1)
+ current_temp,
+ unit,
+ TEMP_CELSIUS
+ ), 1)
+
+ current_humidity = attrs.get(climate.ATTR_CURRENT_HUMIDITY)
+ if current_humidity is not None:
+ response['thermostatHumidityAmbient'] = current_humidity
+
+ if operation == climate.STATE_AUTO:
+ if (supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and
+ supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
+ response['thermostatTemperatureSetpointHigh'] = \
+ round(temp_util.convert(
+ attrs[climate.ATTR_TARGET_TEMP_HIGH],
+ unit, TEMP_CELSIUS), 1)
+ response['thermostatTemperatureSetpointLow'] = \
+ round(temp_util.convert(
+ attrs[climate.ATTR_TARGET_TEMP_LOW],
+ unit, TEMP_CELSIUS), 1)
+ else:
+ target_temp = attrs.get(ATTR_TEMPERATURE)
+ if target_temp is not None:
+ target_temp = round(
+ temp_util.convert(
+ target_temp,
+ unit,
+ TEMP_CELSIUS
+ ), 1)
+ response['thermostatTemperatureSetpointHigh'] = \
+ target_temp
+ response['thermostatTemperatureSetpointLow'] = \
+ target_temp
else:
target_temp = attrs.get(ATTR_TEMPERATURE)
if target_temp is not None:
- target_temp = round(
+ response['thermostatTemperatureSetpoint'] = round(
temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1)
- response['thermostatTemperatureSetpointHigh'] = target_temp
- response['thermostatTemperatureSetpointLow'] = target_temp
- else:
- target_temp = attrs.get(ATTR_TEMPERATURE)
- if target_temp is not None:
- response['thermostatTemperatureSetpoint'] = round(
- temp_util.convert(target_temp, unit, TEMP_CELSIUS), 1)
return response
async def execute(self, command, data, params, challenge):
"""Execute a temperature point or mode command."""
+ domain = self.state.domain
+ if domain == sensor.DOMAIN:
+ raise SmartHomeError(
+ ERR_NOT_SUPPORTED,
+ 'Execute is not supported by sensor')
+
# All sent in temperatures are always in Celsius
unit = self.hass.config.units.temperature_unit
min_temp = self.state.attributes[climate.ATTR_MIN_TEMP]
@@ -682,8 +725,8 @@ async def execute(self, command, data, params, challenge):
ATTR_ENTITY_ID: self.state.entity_id,
}
- if(supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH and
- supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
+ if(supported & climate.SUPPORT_TARGET_TEMPERATURE_HIGH
+ and supported & climate.SUPPORT_TARGET_TEMPERATURE_LOW):
svc_data[climate.ATTR_TARGET_TEMP_HIGH] = temp_high
svc_data[climate.ATTR_TARGET_TEMP_LOW] = temp_low
else:
@@ -732,6 +775,11 @@ def supported(domain, features, device_class):
"""Test if state is supported."""
return domain == lock.DOMAIN
+ @staticmethod
+ def might_2fa(domain, features, device_class):
+ """Return if the trait might ask for 2FA."""
+ return True
+
def sync_attributes(self):
"""Return LockUnlock attributes for a sync request."""
return {}
@@ -745,7 +793,7 @@ async def execute(self, command, data, params, challenge):
if params['lock']:
service = lock.SERVICE_LOCK
else:
- _verify_pin_challenge(data, challenge)
+ _verify_pin_challenge(data, self.state, challenge)
service = lock.SERVICE_UNLOCK
await self.hass.services.async_call(lock.DOMAIN, service, {
@@ -1021,6 +1069,9 @@ class OpenCloseTrait(_Trait):
https://developers.google.com/actions/smarthome/traits/openclose
"""
+ # Cover device classes that require 2FA
+ COVER_2FA = (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE)
+
name = TRAIT_OPENCLOSE
commands = [
COMMAND_OPENCLOSE
@@ -1042,6 +1093,12 @@ def supported(domain, features, device_class):
binary_sensor.DEVICE_CLASS_WINDOW,
)
+ @staticmethod
+ def might_2fa(domain, features, device_class):
+ """Return if the trait might ask for 2FA."""
+ return (domain == cover.DOMAIN and
+ device_class in OpenCloseTrait.COVER_2FA)
+
def sync_attributes(self):
"""Return opening direction."""
response = {}
@@ -1114,9 +1171,8 @@ async def execute(self, command, data, params, challenge):
if (should_verify and
self.state.attributes.get(ATTR_DEVICE_CLASS)
- in (cover.DEVICE_CLASS_DOOR,
- cover.DEVICE_CLASS_GARAGE)):
- _verify_pin_challenge(data, challenge)
+ in OpenCloseTrait.COVER_2FA):
+ _verify_pin_challenge(data, self.state, challenge)
await self.hass.services.async_call(
cover.DOMAIN, service, svc_params,
@@ -1202,8 +1258,11 @@ async def execute(self, command, data, params, challenge):
ERR_NOT_SUPPORTED, 'Command not supported')
-def _verify_pin_challenge(data, challenge):
+def _verify_pin_challenge(data, state, challenge):
"""Verify a pin challenge."""
+ if not data.config.should_2fa(state):
+ return
+
if not data.config.secure_devices_pin:
raise SmartHomeError(
ERR_CHALLENGE_NOT_SETUP, 'Challenge is not set up')
@@ -1217,7 +1276,7 @@ def _verify_pin_challenge(data, challenge):
raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED)
-def _verify_ack_challenge(data, challenge):
+def _verify_ack_challenge(data, state, challenge):
"""Verify a pin challenge."""
if not challenge or not challenge.get('ack'):
raise ChallengeNeeded(CHALLENGE_ACK_NEEDED)
diff --git a/homeassistant/components/gpslogger/device_tracker.py b/homeassistant/components/gpslogger/device_tracker.py
index 81a4fb3e7f8b81..49d421cbc8c851 100644
--- a/homeassistant/components/gpslogger/device_tracker.py
+++ b/homeassistant/components/gpslogger/device_tracker.py
@@ -45,6 +45,7 @@ def __init__(
self._battery = battery
self._location = location
self._unsub_dispatcher = None
+ self._unique_id = device
@property
def battery_level(self):
@@ -81,6 +82,19 @@ 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': {(GPL_DOMAIN, self._unique_id)},
+ }
+
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
diff --git a/homeassistant/components/hassio/addon_panel.py b/homeassistant/components/hassio/addon_panel.py
index 7291a87e9544fd..e85c8f12247112 100644
--- a/homeassistant/components/hassio/addon_panel.py
+++ b/homeassistant/components/hassio/addon_panel.py
@@ -61,7 +61,7 @@ async def post(self, request, addon):
async def delete(self, request, addon):
"""Handle remove add-on panel requests."""
- # Currently not supported by backend / frontend
+ self.hass.components.frontend.async_remove_panel(addon)
return web.Response()
async def get_panels(self):
diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py
index 824dee86fadaa0..250d50681dce85 100644
--- a/homeassistant/components/hassio/ingress.py
+++ b/homeassistant/components/hassio/ingress.py
@@ -119,8 +119,12 @@ async def _handle_request(
source_header = _init_header(request, token)
async with self._websession.request(
- request.method, url, headers=source_header,
- params=request.query, data=data
+ request.method,
+ url,
+ headers=source_header,
+ params=request.query,
+ allow_redirects=False,
+ data=data
) as result:
headers = _response_header(result)
diff --git a/homeassistant/components/heos/.translations/fr.json b/homeassistant/components/heos/.translations/fr.json
index 274075af7499b7..549cd00e8e0cbc 100644
--- a/homeassistant/components/heos/.translations/fr.json
+++ b/homeassistant/components/heos/.translations/fr.json
@@ -9,7 +9,8 @@
"step": {
"user": {
"data": {
- "access_token": "H\u00f4te"
+ "access_token": "H\u00f4te",
+ "host": "H\u00f4te"
},
"description": "Veuillez saisir le nom d\u2019h\u00f4te ou l\u2019adresse IP d\u2019un p\u00e9riph\u00e9rique Heos (de pr\u00e9f\u00e9rence connect\u00e9 au r\u00e9seau filaire).",
"title": "Se connecter \u00e0 Heos"
diff --git a/homeassistant/components/heos/.translations/hu.json b/homeassistant/components/heos/.translations/hu.json
index 8cd10b3c2466ff..20ae78ae3161f7 100644
--- a/homeassistant/components/heos/.translations/hu.json
+++ b/homeassistant/components/heos/.translations/hu.json
@@ -7,6 +7,7 @@
"host": "Kiszolg\u00e1l\u00f3"
}
}
- }
+ },
+ "title": "HEOS"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/heos/.translations/pt-BR.json b/homeassistant/components/heos/.translations/pt-BR.json
new file mode 100644
index 00000000000000..ac860059b5df3f
--- /dev/null
+++ b/homeassistant/components/heos/.translations/pt-BR.json
@@ -0,0 +1,13 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "access_token": "Host",
+ "host": "Host"
+ },
+ "title": "Conecte-se a Heos"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py
index 7efe4f2beb20ab..d0dd098638f62f 100644
--- a/homeassistant/components/history/__init__.py
+++ b/homeassistant/components/history/__init__.py
@@ -252,7 +252,7 @@ async def async_setup(hass, config):
use_include_order = conf.get(CONF_ORDER)
hass.http.register_view(HistoryPeriodView(filters, use_include_order))
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'history', 'history', 'hass:poll-box')
return True
diff --git a/homeassistant/components/homekit_controller/.translations/ca.json b/homeassistant/components/homekit_controller/.translations/ca.json
index 8765a859418f59..f2ed4bd0c21596 100644
--- a/homeassistant/components/homekit_controller/.translations/ca.json
+++ b/homeassistant/components/homekit_controller/.translations/ca.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "No s'ha pogut vincular, no s'ha trobat el dispositiu.",
"already_configured": "Accessori ja configurat amb aquest controlador.",
+ "already_in_progress": "El flux de dades pel dispositiu ja est\u00e0 en curs.",
"already_paired": "Aquest accessori ja est\u00e0 vinculat amb un altre dispositiu. Reinicia l'accessori i torna-ho a provar.",
"ignored_model": "La disponibilitat de HomeKit per aquest model est\u00e0 bloquejada ja que, de moment, no hi ha una integraci\u00f3 nativa completa.",
"invalid_config_entry": "Aquest dispositiu s'est\u00e0 mostrant com a llest per a ser vinculat per\u00f2, hi ha una entrada de configuraci\u00f3 conflictiva a Home Assistant que s'ha d'eliminar primer.",
diff --git a/homeassistant/components/homekit_controller/.translations/de.json b/homeassistant/components/homekit_controller/.translations/de.json
index d13d2bb7e2a760..22420b79661e58 100644
--- a/homeassistant/components/homekit_controller/.translations/de.json
+++ b/homeassistant/components/homekit_controller/.translations/de.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "Die Kopplung kann nicht durchgef\u00fchrt werden, da das Ger\u00e4t nicht mehr gefunden werden kann.",
"already_configured": "Das Zubeh\u00f6r ist mit diesem Controller bereits konfiguriert.",
+ "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.",
"already_paired": "Dieses Zubeh\u00f6r ist bereits mit einem anderen Ger\u00e4t gekoppelt. Setzen Sie das Zubeh\u00f6r zur\u00fcck und versuchen Sie es erneut.",
"ignored_model": "Die Unterst\u00fctzung von HomeKit f\u00fcr dieses Modell ist blockiert, da eine vollst\u00e4ndige native Integration verf\u00fcgbar ist.",
"invalid_config_entry": "Dieses Ger\u00e4t wird als bereit zum Koppeln angezeigt, es gibt jedoch bereits einen widerspr\u00fcchlichen Konfigurationseintrag in Home Assistant, der zuerst entfernt werden muss.",
diff --git a/homeassistant/components/homekit_controller/.translations/en.json b/homeassistant/components/homekit_controller/.translations/en.json
index 059f0f7afe7933..31731a52203a21 100644
--- a/homeassistant/components/homekit_controller/.translations/en.json
+++ b/homeassistant/components/homekit_controller/.translations/en.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "Cannot add pairing as device can no longer be found.",
"already_configured": "Accessory is already configured with this controller.",
+ "already_in_progress": "Config flow for device is already in progress.",
"already_paired": "This accessory is already paired to another device. Please reset the accessory and try again.",
"ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.",
"invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.",
diff --git a/homeassistant/components/homekit_controller/.translations/fr.json b/homeassistant/components/homekit_controller/.translations/fr.json
index 5e1bea42bdcf00..15e50a4012701c 100644
--- a/homeassistant/components/homekit_controller/.translations/fr.json
+++ b/homeassistant/components/homekit_controller/.translations/fr.json
@@ -1,7 +1,9 @@
{
"config": {
"abort": {
+ "accessory_not_found_error": "Impossible d'ajouter le couplage car l'appareil est introuvable.",
"already_configured": "L'accessoire est d\u00e9j\u00e0 configur\u00e9 avec ce contr\u00f4leur.",
+ "already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.",
"already_paired": "Cet accessoire est d\u00e9j\u00e0 associ\u00e9 \u00e0 un autre appareil. R\u00e9initialisez l\u2019accessoire et r\u00e9essayez.",
"ignored_model": "La prise en charge de HomeKit pour ce mod\u00e8le est bloqu\u00e9e car une int\u00e9gration native plus compl\u00e8te est disponible.",
"invalid_config_entry": "Cet appareil est pr\u00eat \u00e0 \u00eatre coupl\u00e9, mais il existe d\u00e9j\u00e0 une entr\u00e9e de configuration en conflit dans Home Assistant \u00e0 supprimer.",
@@ -9,10 +11,14 @@
},
"error": {
"authentication_error": "Code HomeKit incorrect. S'il vous pla\u00eet v\u00e9rifier et essayez \u00e0 nouveau.",
+ "busy_error": "L'appareil a refus\u00e9 d'ajouter le couplage car il est d\u00e9j\u00e0 coupl\u00e9 avec un autre contr\u00f4leur.",
+ "max_peers_error": "L'appareil a refus\u00e9 d'ajouter le couplage car il ne dispose pas de stockage de couplage libre.",
+ "max_tries_error": "Le p\u00e9riph\u00e9rique a refus\u00e9 d'ajouter le couplage car il a re\u00e7u plus de 100 tentatives d'authentification infructueuses.",
"pairing_failed": "Une erreur non g\u00e9r\u00e9e s'est produite lors de la tentative d'appairage avec cet appareil. Il se peut qu'il s'agisse d'une panne temporaire ou que votre appareil ne soit pas pris en charge actuellement.",
"unable_to_pair": "Impossible d'appairer, veuillez r\u00e9essayer.",
"unknown_error": "L'appareil a signal\u00e9 une erreur inconnue. L'appairage a \u00e9chou\u00e9."
},
+ "flow_title": "Accessoire HomeKit: {name}",
"step": {
"pair": {
"data": {
diff --git a/homeassistant/components/homekit_controller/.translations/hu.json b/homeassistant/components/homekit_controller/.translations/hu.json
new file mode 100644
index 00000000000000..60bd173dc8ecc2
--- /dev/null
+++ b/homeassistant/components/homekit_controller/.translations/hu.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "step": {
+ "user": {
+ "data": {
+ "device": "Eszk\u00f6z"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/.translations/ko.json b/homeassistant/components/homekit_controller/.translations/ko.json
index 5ee62ad62b4268..6f494120f1da53 100644
--- a/homeassistant/components/homekit_controller/.translations/ko.json
+++ b/homeassistant/components/homekit_controller/.translations/ko.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "\uae30\uae30\ub97c \ub354 \uc774\uc0c1 \ucc3e\uc744 \uc218 \uc5c6\uc73c\ubbc0\ub85c \ud398\uc5b4\ub9c1\uc744 \ucd94\uac00 \ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.",
"already_configured": "\uc561\uc138\uc11c\ub9ac\uac00 \ucee8\ud2b8\ub864\ub7ec\uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
+ "already_in_progress": "\uae30\uae30 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.",
"already_paired": "\uc774 \uc561\uc138\uc11c\ub9ac\ub294 \uc774\ubbf8 \ub2e4\ub978 \uae30\uae30\uc640 \ud398\uc5b4\ub9c1\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4. \uc561\uc138\uc11c\ub9ac\ub97c \uc7ac\uc124\uc815\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.",
"ignored_model": "\uc774 \ubaa8\ub378\uc5d0 \ub300\ud55c HomeKit \uc9c0\uc6d0\uc740 \ub354 \ub9ce\uc740 \uae30\ub2a5\uc744 \uc81c\uacf5\ud558\ub294 \uae30\ubcf8 \uad6c\uc131\uc694\uc18c\ub85c \uc778\ud574 \ucc28\ub2e8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.",
"invalid_config_entry": "\uc774 \uae30\uae30\ub294 \ud398\uc5b4\ub9c1 \ud560 \uc900\ube44\uac00 \ub418\uc5c8\uc9c0\ub9cc Home Assistant \uc5d0 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \ucda9\ub3cc\ud558\ub294 \uad6c\uc131\uc694\uc18c\uac00 \uc788\uc2b5\ub2c8\ub2e4. \uba3c\uc800 \ud574\ub2f9 \uad6c\uc131\uc694\uc18c\ub97c \uc81c\uac70\ud574\uc8fc\uc138\uc694.",
diff --git a/homeassistant/components/homekit_controller/.translations/no.json b/homeassistant/components/homekit_controller/.translations/no.json
index e7ec6c279fa1f6..8dd293dc7c8c8c 100644
--- a/homeassistant/components/homekit_controller/.translations/no.json
+++ b/homeassistant/components/homekit_controller/.translations/no.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "Kan ikke legge til sammenkobling da enheten ikke lenger kan bli funnet.",
"already_configured": "Tilbeh\u00f8r er allerede konfigurert med denne kontrolleren.",
+ "already_in_progress": "Konfigurasjonsflyt for enhet p\u00e5g\u00e5r allerede.",
"already_paired": "Dette tilbeh\u00f8ret er allerede sammenkoblet med en annen enhet. Vennligst tilbakestill tilbeh\u00f8ret og pr\u00f8v igjen.",
"ignored_model": "HomeKit st\u00f8tte for denne modellen er blokkert da en mer funksjonsrik standard integrering er tilgjengelig.",
"invalid_config_entry": "Denne enheten vises som klar til \u00e5 sammenkoble, men det er allerede en motstridende konfigurasjonsoppf\u00f8ring for den i Home Assistant som m\u00e5 fjernes f\u00f8rst.",
diff --git a/homeassistant/components/homekit_controller/.translations/pl.json b/homeassistant/components/homekit_controller/.translations/pl.json
index a0489aa083a3a5..031a7440ed0129 100644
--- a/homeassistant/components/homekit_controller/.translations/pl.json
+++ b/homeassistant/components/homekit_controller/.translations/pl.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "Nie mo\u017cna rozpocz\u0105\u0107 parowania, poniewa\u017c nie znaleziono urz\u0105dzenia.",
"already_configured": "Akcesorium jest ju\u017c skonfigurowane z tym kontrolerem.",
+ "already_in_progress": "Konfigurowanie urz\u0105dzenia jest ju\u017c w toku.",
"already_paired": "To akcesorium jest ju\u017c sparowane z innym urz\u0105dzeniem. Zresetuj akcesorium i spr\u00f3buj ponownie.",
"ignored_model": "Obs\u0142uga HomeKit dla tego modelu jest zablokowana, poniewa\u017c dost\u0119pna jest pe\u0142niejsza integracja natywna.",
"invalid_config_entry": "To urz\u0105dzenie jest wy\u015bwietlane jako gotowe do sparowania, ale istnieje ju\u017c konfliktowy wpis konfiguracyjny dla niego w Home Assistant, kt\u00f3ry musi zosta\u0107 najpierw usuni\u0119ty.",
diff --git a/homeassistant/components/homekit_controller/.translations/pt-BR.json b/homeassistant/components/homekit_controller/.translations/pt-BR.json
new file mode 100644
index 00000000000000..f13ca355b2e1be
--- /dev/null
+++ b/homeassistant/components/homekit_controller/.translations/pt-BR.json
@@ -0,0 +1,9 @@
+{
+ "config": {
+ "abort": {
+ "accessory_not_found_error": "N\u00e3o \u00e9 poss\u00edvel adicionar o emparelhamento, pois o dispositivo n\u00e3o pode mais ser encontrado.",
+ "already_in_progress": "O fluxo de configura\u00e7\u00e3o para o dispositivo j\u00e1 est\u00e1 em andamento."
+ },
+ "flow_title": "Acess\u00f3rio HomeKit: {name}"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/homekit_controller/.translations/ru.json b/homeassistant/components/homekit_controller/.translations/ru.json
index 44b4faf455f941..c7770c6a064b34 100644
--- a/homeassistant/components/homekit_controller/.translations/ru.json
+++ b/homeassistant/components/homekit_controller/.translations/ru.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043e.",
"already_configured": "\u0410\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u044d\u0442\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c.",
+ "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.",
"already_paired": "\u042d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440 \u0443\u0436\u0435 \u0441\u0432\u044f\u0437\u0430\u043d \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u0431\u0440\u043e\u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.",
"ignored_model": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 HomeKit \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u043c\u043e\u0434\u0435\u043b\u0438 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u043b\u043d\u0430\u044f \u043d\u0430\u0442\u0438\u0432\u043d\u0430\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f.",
"invalid_config_entry": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043a\u0430\u043a \u0433\u043e\u0442\u043e\u0432\u043e\u0435 \u043a \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044e, \u043d\u043e \u0432 Home Assistant \u0443\u0436\u0435 \u0435\u0441\u0442\u044c \u043a\u043e\u043d\u0444\u043b\u0438\u043a\u0442\u0443\u044e\u0449\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043d\u0435\u0433\u043e, \u043a\u043e\u0442\u043e\u0440\u0443\u044e \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0443\u0434\u0430\u043b\u0438\u0442\u044c.",
diff --git a/homeassistant/components/homekit_controller/.translations/sl.json b/homeassistant/components/homekit_controller/.translations/sl.json
index afee189216d365..0404dd7beb5434 100644
--- a/homeassistant/components/homekit_controller/.translations/sl.json
+++ b/homeassistant/components/homekit_controller/.translations/sl.json
@@ -1,6 +1,7 @@
{
"config": {
"abort": {
+ "accessory_not_found_error": "Seznanjanja ni mogo\u010de dodati, ker naprave ni ve\u010d mogo\u010de najti.",
"already_configured": "Dodatna oprema je \u017ee konfigurirana s tem krmilnikom.",
"already_paired": "Ta dodatna oprema je \u017ee povezana z drugo napravo. Ponastavite dodatno opremo in poskusite znova.",
"ignored_model": "Podpora za HomeKit za ta model je blokirana, saj je na voljo ve\u010d funkcij popolne nativne integracije.",
@@ -9,15 +10,20 @@
},
"error": {
"authentication_error": "Nepravilna koda HomeKit. Preverite in poskusite znova.",
+ "busy_error": "Naprava je zavrnila seznanjanje, saj se \u017ee povezuje z drugim krmilnikom.",
+ "max_peers_error": "Naprava je zavrnila seznanjanje, saj nima prostega pomnilnika za seznanjanje.",
+ "max_tries_error": "Napravaje zavrnila seznanjanje, saj je prejela ve\u010d kot 100 neuspe\u0161nih poskusov overjanja.",
+ "pairing_failed": "Pri poskusu seznanjanja s to napravo je pri\u0161lo do napake. To je lahko za\u010dasna napaka ali pa naprava trenutno ni podprta.",
"unable_to_pair": "Ni mogo\u010de seznaniti. Poskusite znova.",
"unknown_error": "Naprava je sporo\u010dila neznano napako. Seznanjanje ni uspelo."
},
+ "flow_title": "HomeKit Oprema: {name}",
"step": {
"pair": {
"data": {
"pairing_code": "Koda za seznanjanje"
},
- "description": "Vnesi HomeKit kodo, \u010de \u017eeli\u0161 uporabiti to dodatno opremo",
+ "description": "\u010ce \u017eeli\u0161 uporabiti to dodatno opremo, vnesi HomeKit kodo.",
"title": "Seznanite s HomeKit Opremo"
},
"user": {
diff --git a/homeassistant/components/homekit_controller/.translations/sv.json b/homeassistant/components/homekit_controller/.translations/sv.json
index 264fca2de504dc..302f71d4ccfc67 100644
--- a/homeassistant/components/homekit_controller/.translations/sv.json
+++ b/homeassistant/components/homekit_controller/.translations/sv.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "Kan inte genomf\u00f6ra parningsf\u00f6rs\u00f6ket eftersom enheten inte l\u00e4ngre kan hittas.",
"already_configured": "Tillbeh\u00f6ret \u00e4r redan konfigurerat med denna kontroller.",
+ "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r enheten p\u00e5g\u00e5r redan.",
"already_paired": "Det h\u00e4r tillbeh\u00f6ret \u00e4r redan kopplat till en annan enhet. \u00c5terst\u00e4ll tillbeh\u00f6ret och f\u00f6rs\u00f6k igen.",
"ignored_model": "HomeKit-st\u00f6d f\u00f6r den h\u00e4r modellen blockeras eftersom en mer komplett inbyggd integration \u00e4r tillg\u00e4nglig.",
"invalid_config_entry": "Den h\u00e4r enheten visas som redo att paras ihop, men det finns redan en motstridig konfigurations-post f\u00f6r den i Home Assistant som f\u00f6rst m\u00e5ste tas bort.",
diff --git a/homeassistant/components/homekit_controller/.translations/zh-Hant.json b/homeassistant/components/homekit_controller/.translations/zh-Hant.json
index 25ca625d7df19c..aaa2c9eda8f7de 100644
--- a/homeassistant/components/homekit_controller/.translations/zh-Hant.json
+++ b/homeassistant/components/homekit_controller/.translations/zh-Hant.json
@@ -3,6 +3,7 @@
"abort": {
"accessory_not_found_error": "\u627e\u4e0d\u5230\u88dd\u7f6e\uff0c\u7121\u6cd5\u65b0\u589e\u914d\u5c0d\u3002",
"already_configured": "\u914d\u4ef6\u5df2\u7d93\u7531\u6b64\u63a7\u5236\u5668\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "\u88dd\u7f6e\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
"already_paired": "\u914d\u4ef6\u5df2\u7d93\u8207\u5176\u4ed6\u88dd\u7f6e\u914d\u5c0d\uff0c\u8acb\u91cd\u7f6e\u914d\u4ef6\u5f8c\u518d\u8a66\u4e00\u6b21\u3002",
"ignored_model": "\u7531\u65bc\u6b64\u578b\u865f\u53ef\u539f\u751f\u652f\u63f4\u66f4\u5b8c\u6574\u529f\u80fd\uff0c\u56e0\u6b64 Homekit \u652f\u63f4\u5df2\u88ab\u7981\u6b62\u3002",
"invalid_config_entry": "\u88dd\u7f6e\u986f\u793a\u7b49\u5f85\u9032\u884c\u914d\u5c0d\uff0c\u4f46 Home Assistant \u986f\u793a\u6709\u76f8\u885d\u7a81\u8a2d\u5b9a\u7269\u4ef6\u5fc5\u9808\u5148\u884c\u79fb\u9664\u3002",
diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py
index 1fcbddbb400789..f1ddf1faacfc77 100644
--- a/homeassistant/components/homekit_controller/__init__.py
+++ b/homeassistant/components/homekit_controller/__init__.py
@@ -95,7 +95,8 @@ async def async_update(self):
"""Obtain a HomeKit device's state."""
# pylint: disable=import-error
from homekit.exceptions import (
- AccessoryDisconnectedError, AccessoryNotFoundError)
+ AccessoryDisconnectedError, AccessoryNotFoundError,
+ EncryptionError)
try:
new_values_dict = await self._accessory.get_characteristics(
@@ -106,7 +107,7 @@ async def async_update(self):
# visible on the network.
self._available = False
return
- except AccessoryDisconnectedError:
+ except (AccessoryDisconnectedError, EncryptionError):
# Temporary connection failure. Device is still available but our
# connection was dropped.
return
diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py
index deefb59631072a..2ce8c0db6b785a 100644
--- a/homeassistant/components/homekit_controller/config_flow.py
+++ b/homeassistant/components/homekit_controller/config_flow.py
@@ -109,7 +109,7 @@ async def async_step_user(self, user_input=None):
})
)
- async def async_step_discovery(self, discovery_info):
+ async def async_step_zeroconf(self, discovery_info):
"""Handle a discovered HomeKit accessory.
This flow is triggered by the discovery component.
@@ -126,15 +126,24 @@ async def async_step_discovery(self, discovery_info):
# It changes if a device is factory reset.
hkid = properties['id']
model = properties['md']
-
+ name = discovery_info['name'].replace('._hap._tcp.local.', '')
status_flags = int(properties['sf'])
paired = not status_flags & 0x01
+ _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid)
+
# pylint: disable=unsupported-assignment-operation
+ self.context['hkid'] = hkid
self.context['title_placeholders'] = {
- 'name': discovery_info['name'],
+ 'name': name,
}
+ # If multiple HomekitControllerFlowHandler end up getting created
+ # for the same accessory dont let duplicates hang around
+ active_flows = self._async_in_progress()
+ if any(hkid == flow['context']['hkid'] for flow in active_flows):
+ return self.async_abort(reason='already_in_progress')
+
# The configuration number increases every time the characteristic map
# needs updating. Some devices use a slightly off-spec name so handle
# both cases.
diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json
index 8b0dfd199bb998..62dbf3740a3f7a 100644
--- a/homeassistant/components/homekit_controller/manifest.json
+++ b/homeassistant/components/homekit_controller/manifest.json
@@ -7,6 +7,7 @@
"homekit[IP]==0.14.0"
],
"dependencies": [],
+ "zeroconf": ["_hap._tcp.local."],
"codeowners": [
"@Jc2k"
]
diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json
index eceaa624b0fc62..b51dcb1f6d8549 100644
--- a/homeassistant/components/homekit_controller/strings.json
+++ b/homeassistant/components/homekit_controller/strings.json
@@ -33,7 +33,8 @@
"ignored_model": "HomeKit support for this model is blocked as a more feature complete native integration is available.",
"already_configured": "Accessory is already configured with this controller.",
"invalid_config_entry": "This device is showing as ready to pair but there is already a conflicting config entry for it in Home Assistant that must first be removed.",
- "accessory_not_found_error": "Cannot add pairing as device can no longer be found."
+ "accessory_not_found_error": "Cannot add pairing as device can no longer be found.",
+ "already_in_progress": "Config flow for device is already in progress."
}
}
}
diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
index 1e072c6784c1fa..ccd19f26d68707 100644
--- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py
+++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py
@@ -12,7 +12,7 @@
STATE_ALARM_TRIGGERED)
from homeassistant.core import HomeAssistant
-from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice
+from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID
_LOGGER = logging.getLogger(__name__)
@@ -34,12 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
for group in home.groups:
if isinstance(group, AsyncSecurityZoneGroup):
security_zones.append(group)
- # To be removed in a later release.
- devices.append(HomematicipSecurityZone(home, group))
- _LOGGER.warning("Homematic IP: alarm_control_panel.%s is "
- "deprecated. Please switch to "
- "alarm_control_panel.*hmip_alarm_control_panel.",
- group.label)
+
if security_zones:
devices.append(HomematicipAlarmControlPanel(home, security_zones))
@@ -47,45 +42,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry,
async_add_entities(devices)
-class HomematicipSecurityZone(HomematicipGenericDevice, AlarmControlPanel):
- """Representation of an HomematicIP Cloud security zone group."""
-
- def __init__(self, home: AsyncHome, device) -> None:
- """Initialize the security zone group."""
- device.modelType = 'Group-SecurityZone'
- device.windowState = None
- super().__init__(home, device)
-
- @property
- def state(self) -> str:
- """Return the state of the device."""
- if self._device.active:
- if (self._device.sabotage or self._device.motionDetected or
- self._device.windowState == WindowState.OPEN or
- self._device.windowState == WindowState.TILTED):
- return STATE_ALARM_TRIGGERED
-
- active = self._home.get_security_zones_activation()
- if active == (True, True):
- return STATE_ALARM_ARMED_AWAY
- if active == (False, True):
- return STATE_ALARM_ARMED_HOME
-
- return STATE_ALARM_DISARMED
-
- async def async_alarm_disarm(self, code=None):
- """Send disarm command."""
- await self._home.set_security_zones_activation(False, False)
-
- async def async_alarm_arm_home(self, code=None):
- """Send arm home command."""
- await self._home.set_security_zones_activation(False, True)
-
- async def async_alarm_arm_away(self, code=None):
- """Send arm away command."""
- await self._home.set_security_zones_activation(True, True)
-
-
class HomematicipAlarmControlPanel(AlarmControlPanel):
"""Representation of an alarm control panel."""
diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py
index 1ef70b5e0225bc..419b62be2c63ed 100644
--- a/homeassistant/components/http/cors.py
+++ b/homeassistant/components/http/cors.py
@@ -1,4 +1,5 @@
"""Provide CORS support for the HTTP component."""
+from aiohttp.web_urldispatcher import Resource, ResourceRoute
from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION
from homeassistant.const import (
@@ -8,6 +9,7 @@
ALLOWED_CORS_HEADERS = [
ORIGIN, ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE,
HTTP_HEADER_HA_AUTH, AUTHORIZATION]
+VALID_CORS_TYPES = (Resource, ResourceRoute)
@callback
@@ -31,6 +33,9 @@ def _allow_cors(route, config=None):
else:
path = route
+ if not isinstance(path, VALID_CORS_TYPES):
+ return
+
path = path.canonical
if path in cors_added:
diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json
index 2e096343b0921c..bfdc6f167aa668 100644
--- a/homeassistant/components/huawei_lte/manifest.json
+++ b/homeassistant/components/huawei_lte/manifest.json
@@ -3,7 +3,7 @@
"name": "Huawei lte",
"documentation": "https://www.home-assistant.io/components/huawei_lte",
"requirements": [
- "huawei-lte-api==1.1.5"
+ "huawei-lte-api==1.2.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/hue/.translations/ca.json b/homeassistant/components/hue/.translations/ca.json
index a37d4ef1518a69..471ce2181fb828 100644
--- a/homeassistant/components/hue/.translations/ca.json
+++ b/homeassistant/components/hue/.translations/ca.json
@@ -3,9 +3,11 @@
"abort": {
"all_configured": "Tots els enlla\u00e7os Philips Hue ja estan configurats",
"already_configured": "L'enlla\u00e7 ja est\u00e0 configurat",
+ "already_in_progress": "El flux de dades de configuraci\u00f3 per l'enlla\u00e7 ja est\u00e0 en curs.",
"cannot_connect": "No s'ha pogut connectar amb l'enlla\u00e7",
"discover_timeout": "No s'han pogut descobrir enlla\u00e7os Hue",
"no_bridges": "No s'han trobat enlla\u00e7os Philips Hue",
+ "not_hue_bridge": "No \u00e9s un enlla\u00e7 Hue",
"unknown": "S'ha produ\u00eft un error desconegut"
},
"error": {
diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json
index a0bd50d8514dfc..bb78566a12be09 100644
--- a/homeassistant/components/hue/.translations/de.json
+++ b/homeassistant/components/hue/.translations/de.json
@@ -3,6 +3,7 @@
"abort": {
"all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert",
"already_configured": "Bridge ist bereits konfiguriert",
+ "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt.",
"cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich",
"discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken",
"no_bridges": "Keine Philips Hue Bridges entdeckt",
diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json
index cea8d8be10af34..350360285af414 100644
--- a/homeassistant/components/hue/.translations/en.json
+++ b/homeassistant/components/hue/.translations/en.json
@@ -3,9 +3,11 @@
"abort": {
"all_configured": "All Philips Hue bridges are already configured",
"already_configured": "Bridge is already configured",
+ "already_in_progress": "Config flow for bridge is already in progress.",
"cannot_connect": "Unable to connect to the bridge",
"discover_timeout": "Unable to discover Hue bridges",
"no_bridges": "No Philips Hue bridges discovered",
+ "not_hue_bridge": "Not a Hue bridge",
"unknown": "Unknown error occurred"
},
"error": {
diff --git a/homeassistant/components/hue/.translations/fr.json b/homeassistant/components/hue/.translations/fr.json
index 5414bf01ea7738..ddb647c18ed5a2 100644
--- a/homeassistant/components/hue/.translations/fr.json
+++ b/homeassistant/components/hue/.translations/fr.json
@@ -3,6 +3,7 @@
"abort": {
"all_configured": "Tous les ponts Philips Hue sont d\u00e9j\u00e0 configur\u00e9s",
"already_configured": "Ce pont est d\u00e9j\u00e0 configur\u00e9",
+ "already_in_progress": "Le flux de configuration pour le pont est d\u00e9j\u00e0 en cours.",
"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",
diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json
index a4a8051663e5bb..3879eb5b962403 100644
--- a/homeassistant/components/hue/.translations/ko.json
+++ b/homeassistant/components/hue/.translations/ko.json
@@ -3,6 +3,7 @@
"abort": {
"all_configured": "\ubaa8\ub4e0 \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
"already_configured": "\ube0c\ub9bf\uc9c0\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4",
+ "already_in_progress": "\ube0c\ub9bf\uc9c0 \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.",
"cannot_connect": "\ube0c\ub9ac\uc9c0\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"discover_timeout": "Hue \ube0c\ub9bf\uc9c0\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4",
"no_bridges": "\ubc1c\uacac\ub41c \ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0\uac00 \uc5c6\uc2b5\ub2c8\ub2e4",
diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json
index 02dd6ef7128c84..e8718fe778b8ee 100644
--- a/homeassistant/components/hue/.translations/no.json
+++ b/homeassistant/components/hue/.translations/no.json
@@ -3,9 +3,11 @@
"abort": {
"all_configured": "Alle Philips Hue Bridger er allerede konfigurert",
"already_configured": "Bridge er allerede konfigurert",
+ "already_in_progress": "Konfigurasjonsflyt for bro p\u00e5g\u00e5r allerede.",
"cannot_connect": "Kan ikke koble til Bridge",
"discover_timeout": "Kunne ikke oppdage Hue Bridger",
"no_bridges": "Ingen Philips Hue Bridger oppdaget",
+ "not_hue_bridge": "Ikke en Hue bro",
"unknown": "Ukjent feil oppstod"
},
"error": {
diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json
index 63cbbe016a21ee..8eec1aa662aebc 100644
--- a/homeassistant/components/hue/.translations/pl.json
+++ b/homeassistant/components/hue/.translations/pl.json
@@ -3,6 +3,7 @@
"abort": {
"all_configured": "Wszystkie mostki Hue s\u0105 ju\u017c skonfigurowane",
"already_configured": "Mostek jest ju\u017c skonfigurowany",
+ "already_in_progress": "Konfigurowanie mostka jest ju\u017c w toku.",
"cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z mostkiem",
"discover_timeout": "Nie mo\u017cna wykry\u0107 \u017cadnych mostk\u00f3w Hue",
"no_bridges": "Nie wykryto \u017cadnych mostk\u00f3w Hue",
diff --git a/homeassistant/components/hue/.translations/pt-BR.json b/homeassistant/components/hue/.translations/pt-BR.json
index b30764c92393ff..2b78d2f127825a 100644
--- a/homeassistant/components/hue/.translations/pt-BR.json
+++ b/homeassistant/components/hue/.translations/pt-BR.json
@@ -6,6 +6,7 @@
"cannot_connect": "N\u00e3o \u00e9 poss\u00edvel conectar-se \u00e0 ponte",
"discover_timeout": "Incapaz de descobrir pontes Hue",
"no_bridges": "N\u00e3o h\u00e1 pontes Philips Hue descobertas",
+ "not_hue_bridge": "N\u00e3o \u00e9 uma ponte Hue",
"unknown": "Ocorreu um erro desconhecido"
},
"error": {
diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json
index ce71fb670be424..be5d2b7159d40b 100644
--- a/homeassistant/components/hue/.translations/ru.json
+++ b/homeassistant/components/hue/.translations/ru.json
@@ -3,9 +3,11 @@
"abort": {
"all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b",
"already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430",
+ "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.",
"cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443",
"discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d",
"no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b",
+ "not_hue_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c Hue",
"unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430"
},
"error": {
diff --git a/homeassistant/components/hue/.translations/sl.json b/homeassistant/components/hue/.translations/sl.json
index 7ad7a2e6ade034..fc3142ba8201a1 100644
--- a/homeassistant/components/hue/.translations/sl.json
+++ b/homeassistant/components/hue/.translations/sl.json
@@ -3,6 +3,7 @@
"abort": {
"all_configured": "Vsi mostovi Philips Hue so \u017ee konfigurirani",
"already_configured": "Most je \u017ee konfiguriran",
+ "already_in_progress": "Konfiguracijski tok za most je \u017ee v teku.",
"cannot_connect": "Ni mogo\u010de vzpostaviti povezave z mostom",
"discover_timeout": "Ni bilo mogo\u010de odkriti Hue mostov",
"no_bridges": "Ni odkritih mostov Philips Hue",
diff --git a/homeassistant/components/hue/.translations/sv.json b/homeassistant/components/hue/.translations/sv.json
index a7ffc7bacb2b6e..7e5b7c52dd55d9 100644
--- a/homeassistant/components/hue/.translations/sv.json
+++ b/homeassistant/components/hue/.translations/sv.json
@@ -3,9 +3,11 @@
"abort": {
"all_configured": "Alla Philips Hue-bryggor \u00e4r redan konfigurerade",
"already_configured": "Bryggan \u00e4r redan konfigurerad",
+ "already_in_progress": "Konfigurations fl\u00f6det f\u00f6r bryggan p\u00e5g\u00e5r redan.",
"cannot_connect": "Det gick inte att ansluta till bryggan",
"discover_timeout": "Det gick inte att uppt\u00e4cka n\u00e5gra Hue-bryggor",
"no_bridges": "Inga Philips Hue-bryggor uppt\u00e4cktes",
+ "not_hue_bridge": "Inte en Hue-brygga",
"unknown": "Ett ok\u00e4nt fel intr\u00e4ffade"
},
"error": {
diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json
index eae4c09da497de..a585cfd38c36d3 100644
--- a/homeassistant/components/hue/.translations/zh-Hant.json
+++ b/homeassistant/components/hue/.translations/zh-Hant.json
@@ -3,6 +3,7 @@
"abort": {
"all_configured": "\u6240\u6709 Philips Hue Bridge \u7686\u5df2\u8a2d\u5b9a\u5b8c\u6210",
"already_configured": "Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210",
+ "already_in_progress": "Bridge \u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002",
"cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3 Bridge",
"discover_timeout": "\u7121\u6cd5\u641c\u5c0b\u5230 Hue Bridge",
"no_bridges": "\u672a\u641c\u5c0b\u5230 Philips Hue Bridge",
diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py
index 89dc0b9aa675d7..9c81d144d1c210 100644
--- a/homeassistant/components/hue/config_flow.py
+++ b/homeassistant/components/hue/config_flow.py
@@ -8,6 +8,7 @@
import voluptuous as vol
from homeassistant import config_entries
+from homeassistant.components.ssdp import ATTR_MANUFACTURERURL
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
@@ -15,6 +16,8 @@
from .const import DOMAIN, LOGGER
from .errors import AuthenticationRequired, CannotConnect
+HUE_MANUFACTURERURL = 'http://www.philips.com'
+
@callback
def configured_hosts(hass):
@@ -137,17 +140,25 @@ async def async_step_link(self, user_input=None):
errors=errors,
)
- async def async_step_discovery(self, discovery_info):
+ async def async_step_ssdp(self, discovery_info):
"""Handle a discovered Hue bridge.
- This flow is triggered by the discovery component. It will check if the
+ This flow is triggered by the SSDP component. It will check if the
host is already configured and delegate to the import step if not.
"""
+ if discovery_info[ATTR_MANUFACTURERURL] != HUE_MANUFACTURERURL:
+ return self.async_abort(reason='not_hue_bridge')
+
# Filter out emulated Hue
if "HASS Bridge" in discovery_info.get('name', ''):
return self.async_abort(reason='already_configured')
- host = discovery_info.get('host')
+ # 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()):
+ return self.async_abort(reason='already_in_progress')
if host in configured_hosts(self.hass):
return self.async_abort(reason='already_configured')
diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json
index d035e4468e4668..d16988529b18d9 100644
--- a/homeassistant/components/hue/manifest.json
+++ b/homeassistant/components/hue/manifest.json
@@ -6,6 +6,11 @@
"requirements": [
"aiohue==1.9.1"
],
+ "ssdp": {
+ "manufacturer": [
+ "Royal Philips Electronics"
+ ]
+ },
"dependencies": [],
"codeowners": [
"@balloob"
diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json
index f8873894a01bf0..78b990d5f4276f 100644
--- a/homeassistant/components/hue/strings.json
+++ b/homeassistant/components/hue/strings.json
@@ -23,7 +23,9 @@
"all_configured": "All Philips Hue bridges are already configured",
"unknown": "Unknown error occurred",
"cannot_connect": "Unable to connect to the bridge",
- "already_configured": "Bridge is already configured"
+ "already_configured": "Bridge is already configured",
+ "already_in_progress": "Config flow for bridge is already in progress.",
+ "not_hue_bridge": "Not a Hue bridge"
}
}
}
diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py
index 3a72c81fa11ac0..6aa0f5ad5f2a0a 100644
--- a/homeassistant/components/integration/sensor.py
+++ b/homeassistant/components/integration/sensor.py
@@ -40,7 +40,7 @@
'h': 60*60,
'd': 24*60*60}
-ICON = 'mdi:char-histogram'
+ICON = 'mdi:chart-histogram'
DEFAULT_ROUND = 3
diff --git a/homeassistant/components/ipma/.translations/fr.json b/homeassistant/components/ipma/.translations/fr.json
index 1ca5353ec7eb14..64d03c6ae71d77 100644
--- a/homeassistant/components/ipma/.translations/fr.json
+++ b/homeassistant/components/ipma/.translations/fr.json
@@ -10,6 +10,7 @@
"longitude": "Longitude",
"name": "Nom"
},
+ "description": "Instituto Portugu\u00eas do Mar e Atmosfera",
"title": "Emplacement"
}
},
diff --git a/homeassistant/components/iqvia/.translations/pt-BR.json b/homeassistant/components/iqvia/.translations/pt-BR.json
new file mode 100644
index 00000000000000..b9f716e8d3eae7
--- /dev/null
+++ b/homeassistant/components/iqvia/.translations/pt-BR.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "C\u00f3digo postal j\u00e1 registado",
+ "invalid_zip_code": "C\u00f3digo postal inv\u00e1lido"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "zip_code": "C\u00f3digo postal"
+ },
+ "description": "Preencha o seu CEP dos EUA ou Canad\u00e1.",
+ "title": "IQVIA"
+ }
+ },
+ "title": "IQVIA"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/iqvia/.translations/sl.json b/homeassistant/components/iqvia/.translations/sl.json
new file mode 100644
index 00000000000000..fa04c00c7a2c3c
--- /dev/null
+++ b/homeassistant/components/iqvia/.translations/sl.json
@@ -0,0 +1,18 @@
+{
+ "config": {
+ "error": {
+ "identifier_exists": "Po\u0161tna \u0161tevilka je \u017ee registrirana",
+ "invalid_zip_code": "Po\u0161tna \u0161tevilka ni veljavna"
+ },
+ "step": {
+ "user": {
+ "data": {
+ "zip_code": "Po\u0161tna \u0161tevilka"
+ },
+ "description": "Izpolnite svojo ameri\u0161ko ali kanadsko po\u0161tno \u0161tevilko.",
+ "title": "IQVIA"
+ }
+ },
+ "title": "IQVIA"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py
index 77ba00e451d410..cf21f705b31844 100644
--- a/homeassistant/components/lcn/__init__.py
+++ b/homeassistant/components/lcn/__init__.py
@@ -16,11 +16,15 @@
from .const import (
BINSENSOR_PORTS, CONF_CLIMATES, CONF_CONNECTIONS, CONF_DIM_MODE,
CONF_DIMMABLE, CONF_LOCKABLE, CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_MOTOR,
- CONF_OUTPUT, CONF_SETPOINT, CONF_SK_NUM_TRIES, CONF_SOURCE,
- CONF_TRANSITION, DATA_LCN, DIM_MODES, DOMAIN, KEYS, LED_PORTS,
- LOGICOP_PORTS, MOTOR_PORTS, OUTPUT_PORTS, RELAY_PORTS, S0_INPUTS,
- SETPOINTS, THRESHOLDS, VAR_UNITS, VARIABLES)
+ CONF_OUTPUT, CONF_OUTPUTS, CONF_REGISTER, CONF_SCENE, CONF_SCENES,
+ CONF_SETPOINT, CONF_SK_NUM_TRIES, CONF_SOURCE, CONF_TRANSITION, DATA_LCN,
+ DIM_MODES, DOMAIN, KEYS, LED_PORTS, LOGICOP_PORTS, MOTOR_PORTS,
+ OUTPUT_PORTS, RELAY_PORTS, S0_INPUTS, SETPOINTS, THRESHOLDS, VAR_UNITS,
+ VARIABLES)
from .helpers import has_unique_connection_names, is_address
+from .services import (
+ DynText, Led, LockKeys, LockRegulator, OutputAbs, OutputRel, OutputToggle,
+ Pck, Relays, SendKeys, VarAbs, VarRel, VarReset)
_LOGGER = logging.getLogger(__name__)
@@ -61,6 +65,20 @@
lambda value: value * 1000),
})
+SCENES_SCHEMA = vol.Schema({
+ vol.Required(CONF_NAME): cv.string,
+ vol.Required(CONF_ADDRESS): is_address,
+ vol.Required(CONF_REGISTER): vol.All(vol.Coerce(int), vol.Range(0, 9)),
+ vol.Required(CONF_SCENE): vol.All(vol.Coerce(int), vol.Range(0, 9)),
+ vol.Optional(CONF_OUTPUTS): vol.All(
+ cv.ensure_list, [vol.All(vol.Upper,
+ vol.In(OUTPUT_PORTS + RELAY_PORTS))]),
+ vol.Optional(CONF_TRANSITION, default=None):
+ vol.Any(vol.All(vol.Coerce(int), vol.Range(min=0., max=486.),
+ lambda value: value * 1000),
+ None)
+})
+
SENSORS_SCHEMA = vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_ADDRESS): is_address,
@@ -102,6 +120,8 @@
cv.ensure_list, [COVERS_SCHEMA]),
vol.Optional(CONF_LIGHTS): vol.All(
cv.ensure_list, [LIGHTS_SCHEMA]),
+ vol.Optional(CONF_SCENES): vol.All(
+ cv.ensure_list, [SCENES_SCHEMA]),
vol.Optional(CONF_SENSORS): vol.All(
cv.ensure_list, [SENSORS_SCHEMA]),
vol.Optional(CONF_SWITCHES): vol.All(
@@ -149,12 +169,31 @@ async def async_setup(hass, config):
('climate', CONF_CLIMATES),
('cover', CONF_COVERS),
('light', CONF_LIGHTS),
+ ('scene', CONF_SCENES),
('sensor', CONF_SENSORS),
('switch', CONF_SWITCHES)):
if conf_key in config[DOMAIN]:
hass.async_create_task(
async_load_platform(hass, component, DOMAIN,
config[DOMAIN][conf_key], config))
+
+ # register service calls
+ for service_name, service in (('output_abs', OutputAbs),
+ ('output_rel', OutputRel),
+ ('output_toggle', OutputToggle),
+ ('relays', Relays),
+ ('var_abs', VarAbs),
+ ('var_reset', VarReset),
+ ('var_rel', VarRel),
+ ('lock_regulator', LockRegulator),
+ ('led', Led),
+ ('send_keys', SendKeys),
+ ('lock_keys', LockKeys),
+ ('dyn_text', DynText),
+ ('pck', Pck)):
+ hass.services.async_register(DOMAIN, service_name,
+ service(hass), service.schema)
+
return True
diff --git a/homeassistant/components/lcn/const.py b/homeassistant/components/lcn/const.py
index 45dc04a491e01b..1cf88851456e04 100644
--- a/homeassistant/components/lcn/const.py
+++ b/homeassistant/components/lcn/const.py
@@ -15,12 +15,27 @@
CONF_DIMMABLE = 'dimmable'
CONF_TRANSITION = 'transition'
CONF_MOTOR = 'motor'
+CONF_LOCKABLE = 'lockable'
+CONF_VARIABLE = 'variable'
+CONF_VALUE = 'value'
+CONF_RELVARREF = 'value_reference'
CONF_SOURCE = 'source'
CONF_SETPOINT = 'setpoint'
-CONF_LOCKABLE = 'lockable'
+CONF_LED = 'led'
+CONF_KEYS = 'keys'
+CONF_TIME = 'time'
+CONF_TIME_UNIT = 'time_unit'
+CONF_TABLE = 'table'
+CONF_ROW = 'row'
+CONF_TEXT = 'text'
+CONF_PCK = 'pck'
CONF_CLIMATES = 'climates'
CONF_MAX_TEMP = 'max_temp'
CONF_MIN_TEMP = 'min_temp'
+CONF_SCENES = 'scenes'
+CONF_REGISTER = 'register'
+CONF_SCENE = 'scene'
+CONF_OUTPUTS = 'outputs'
DIM_MODES = ['STEPS50', 'STEPS200']
@@ -36,6 +51,8 @@
LED_PORTS = ['LED1', 'LED2', 'LED3', 'LED4', 'LED5', 'LED6',
'LED7', 'LED8', 'LED9', 'LED10', 'LED11', 'LED12']
+LED_STATUS = ['OFF', 'ON', 'BLINK', 'FLICKER']
+
LOGICOP_PORTS = ['LOGICOP1', 'LOGICOP2', 'LOGICOP3', 'LOGICOP4']
BINSENSOR_PORTS = ['BINSENSOR1', 'BINSENSOR2', 'BINSENSOR3', 'BINSENSOR4',
@@ -70,3 +87,12 @@
'VOLT', 'V',
'AMPERE', 'AMP', 'A',
'DEGREE', '°']
+
+RELVARREF = ['CURRENT', 'PROG']
+
+SENDKEYCOMMANDS = ['HIT', 'MAKE', 'BREAK', 'DONTSEND']
+
+TIME_UNITS = ['SECONDS', 'SECOND', 'SEC', 'S',
+ 'MINUTES', 'MINUTE', 'MIN', 'M',
+ 'HOURS', 'HOUR', 'H',
+ 'DAYS', 'DAY', 'D']
diff --git a/homeassistant/components/lcn/helpers.py b/homeassistant/components/lcn/helpers.py
index 701b6e2436e239..d663a6320b11cb 100644
--- a/homeassistant/components/lcn/helpers.py
+++ b/homeassistant/components/lcn/helpers.py
@@ -65,3 +65,43 @@ def is_address(value):
conn_id = matcher.group('conn_id')
return addr, conn_id
raise vol.error.Invalid('Not a valid address string.')
+
+
+def is_relays_states_string(states_string):
+ """Validate the given states string and return states list."""
+ if len(states_string) == 8:
+ states = []
+ for state_string in states_string:
+ if state_string == '1':
+ state = 'ON'
+ elif state_string == '0':
+ state = 'OFF'
+ elif state_string == 'T':
+ state = 'TOGGLE'
+ elif state_string == '-':
+ state = 'NOCHANGE'
+ else:
+ raise vol.error.Invalid('Not a valid relay state string.')
+ states.append(state)
+ return states
+ raise vol.error.Invalid('Wrong length of relay state string.')
+
+
+def is_key_lock_states_string(states_string):
+ """Validate the given states string and returns states list."""
+ if len(states_string) == 8:
+ states = []
+ for state_string in states_string:
+ if state_string == '1':
+ state = 'ON'
+ elif state_string == '0':
+ state = 'OFF'
+ elif state_string == 'T':
+ state = 'TOGGLE'
+ elif state_string == '-':
+ state = 'NOCHANGE'
+ else:
+ raise vol.error.Invalid('Not a valid key lock state string.')
+ states.append(state)
+ return states
+ raise vol.error.Invalid('Wrong length of key lock state string.')
diff --git a/homeassistant/components/lcn/scene.py b/homeassistant/components/lcn/scene.py
new file mode 100755
index 00000000000000..09f0292758a026
--- /dev/null
+++ b/homeassistant/components/lcn/scene.py
@@ -0,0 +1,66 @@
+"""Support for LCN scenes."""
+import pypck
+
+from homeassistant.components.scene import Scene
+from homeassistant.const import CONF_ADDRESS
+
+from . import LcnDevice
+from .const import (
+ CONF_CONNECTIONS, CONF_OUTPUTS, CONF_REGISTER, CONF_SCENE, CONF_TRANSITION,
+ DATA_LCN, OUTPUT_PORTS)
+from .helpers import get_connection
+
+
+async def async_setup_platform(hass, hass_config, async_add_entities,
+ discovery_info=None):
+ """Set up the LCN scene platform."""
+ if discovery_info is None:
+ return
+
+ devices = []
+ for config in discovery_info:
+ address, connection_id = config[CONF_ADDRESS]
+ addr = pypck.lcn_addr.LcnAddr(*address)
+ connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
+ connection = get_connection(connections, connection_id)
+ address_connection = connection.get_address_conn(addr)
+
+ devices.append(LcnScene(config, address_connection))
+
+ async_add_entities(devices)
+
+
+class LcnScene(LcnDevice, Scene):
+ """Representation of a LCN scene."""
+
+ def __init__(self, config, address_connection):
+ """Initialize the LCN scene."""
+ super().__init__(config, address_connection)
+
+ self.register_id = config[CONF_REGISTER]
+ self.scene_id = config[CONF_SCENE]
+ self.output_ports = []
+ self.relay_ports = []
+
+ for port in config[CONF_OUTPUTS]:
+ if port in OUTPUT_PORTS:
+ self.output_ports.append(pypck.lcn_defs.OutputPort[port])
+ else: # in RELEAY_PORTS
+ self.relay_ports.append(pypck.lcn_defs.RelayPort[port])
+
+ if config[CONF_TRANSITION] is None:
+ self.transition = None
+ else:
+ self.transition = pypck.lcn_defs.time_to_ramp_value(
+ config[CONF_TRANSITION])
+
+ async def async_added_to_hass(self):
+ """Run when entity about to be added to hass."""
+
+ async def async_activate(self):
+ """Activate scene."""
+ self.address_connection.activate_scene(self.register_id,
+ self.scene_id,
+ self.output_ports,
+ self.relay_ports,
+ self.transition)
diff --git a/homeassistant/components/lcn/services.py b/homeassistant/components/lcn/services.py
new file mode 100755
index 00000000000000..78a887a80c101e
--- /dev/null
+++ b/homeassistant/components/lcn/services.py
@@ -0,0 +1,326 @@
+"""Service calls related dependencies for LCN component."""
+import pypck
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_ADDRESS, CONF_BRIGHTNESS, CONF_STATE, CONF_UNIT_OF_MEASUREMENT)
+import homeassistant.helpers.config_validation as cv
+
+from .const import (
+ CONF_CONNECTIONS, CONF_KEYS, CONF_LED, CONF_OUTPUT, CONF_PCK,
+ CONF_RELVARREF, CONF_ROW, CONF_SETPOINT, CONF_TABLE, CONF_TEXT, CONF_TIME,
+ CONF_TIME_UNIT, CONF_TRANSITION, CONF_VALUE, CONF_VARIABLE, DATA_LCN,
+ LED_PORTS, LED_STATUS, OUTPUT_PORTS, RELVARREF, SENDKEYCOMMANDS, SETPOINTS,
+ THRESHOLDS, TIME_UNITS, VAR_UNITS, VARIABLES)
+from .helpers import (
+ get_connection, is_address, is_key_lock_states_string,
+ is_relays_states_string)
+
+
+class LcnServiceCall():
+ """Parent class for all LCN service calls."""
+
+ schema = vol.Schema({
+ vol.Required(CONF_ADDRESS): is_address
+ })
+
+ def __init__(self, hass):
+ """Initialize service call."""
+ self.connections = hass.data[DATA_LCN][CONF_CONNECTIONS]
+
+ def get_address_connection(self, call):
+ """Get address connection object."""
+ addr, connection_id = call.data[CONF_ADDRESS]
+ addr = pypck.lcn_addr.LcnAddr(*addr)
+ if connection_id is None:
+ connection = self.connections[0]
+ else:
+ connection = get_connection(self.connections, connection_id)
+
+ return connection.get_address_conn(addr)
+
+
+class OutputAbs(LcnServiceCall):
+ """Set absolute brightness of output port in percent."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)),
+ vol.Required(CONF_BRIGHTNESS):
+ vol.All(vol.Coerce(int), vol.Range(min=0, max=100)),
+ vol.Optional(CONF_TRANSITION, default=0):
+ vol.All(vol.Coerce(float), vol.Range(min=0., max=486.))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]]
+ brightness = call.data[CONF_BRIGHTNESS]
+ transition = pypck.lcn_defs.time_to_ramp_value(
+ call.data[CONF_TRANSITION] * 1000)
+
+ address_connection = self.get_address_connection(call)
+ address_connection.dim_output(output.value, brightness, transition)
+
+
+class OutputRel(LcnServiceCall):
+ """Set relative brightness of output port in percent."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)),
+ vol.Required(CONF_BRIGHTNESS):
+ vol.All(vol.Coerce(int), vol.Range(min=-100, max=100))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]]
+ brightness = call.data[CONF_BRIGHTNESS]
+
+ address_connection = self.get_address_connection(call)
+ address_connection.rel_output(output.value, brightness)
+
+
+class OutputToggle(LcnServiceCall):
+ """Toggle output port."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_OUTPUT): vol.All(vol.Upper, vol.In(OUTPUT_PORTS)),
+ vol.Optional(CONF_TRANSITION, default=0):
+ vol.All(vol.Coerce(float), vol.Range(min=0., max=486.))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ output = pypck.lcn_defs.OutputPort[call.data[CONF_OUTPUT]]
+ transition = pypck.lcn_defs.time_to_ramp_value(
+ call.data[CONF_TRANSITION] * 1000)
+
+ address_connection = self.get_address_connection(call)
+ address_connection.toggle_output(output.value, transition)
+
+
+class Relays(LcnServiceCall):
+ """Set the relays status."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_STATE): is_relays_states_string})
+
+ def __call__(self, call):
+ """Execute service call."""
+ states = [pypck.lcn_defs.RelayStateModifier[state]
+ for state in call.data[CONF_STATE]]
+
+ address_connection = self.get_address_connection(call)
+ address_connection.control_relays(states)
+
+
+class Led(LcnServiceCall):
+ """Set the led state."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_LED): vol.All(vol.Upper, vol.In(LED_PORTS)),
+ vol.Required(CONF_STATE): vol.All(vol.Upper, vol.In(LED_STATUS))})
+
+ def __call__(self, call):
+ """Execute service call."""
+ led = pypck.lcn_defs.LedPort[call.data[CONF_LED]]
+ led_state = pypck.lcn_defs.LedStatus[
+ call.data[CONF_STATE]]
+
+ address_connection = self.get_address_connection(call)
+ address_connection.control_led(led, led_state)
+
+
+class VarAbs(LcnServiceCall):
+ """Set absolute value of a variable or setpoint.
+
+ Variable has to be set as counter!
+ Reguator setpoints can also be set using R1VARSETPOINT, R2VARSETPOINT.
+ """
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_VARIABLE): vol.All(vol.Upper,
+ vol.In(VARIABLES + SETPOINTS)),
+ vol.Optional(CONF_VALUE, default=0):
+ vol.All(vol.Coerce(int), vol.Range(min=0)),
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT, default='native'):
+ vol.All(vol.Upper, vol.In(VAR_UNITS))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]]
+ value = call.data[CONF_VALUE]
+ unit = pypck.lcn_defs.VarUnit.parse(
+ call.data[CONF_UNIT_OF_MEASUREMENT])
+
+ address_connection = self.get_address_connection(call)
+ address_connection.var_abs(var, value, unit)
+
+
+class VarReset(LcnServiceCall):
+ """Reset value of variable or setpoint."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_VARIABLE): vol.All(vol.Upper,
+ vol.In(VARIABLES + SETPOINTS))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]]
+
+ address_connection = self.get_address_connection(call)
+ address_connection.var_reset(var)
+
+
+class VarRel(LcnServiceCall):
+ """Shift value of a variable, setpoint or threshold."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_VARIABLE):
+ vol.All(vol.Upper, vol.In(VARIABLES + SETPOINTS + THRESHOLDS)),
+ vol.Optional(CONF_VALUE, default=0): int,
+ vol.Optional(CONF_UNIT_OF_MEASUREMENT, default='native'):
+ vol.All(vol.Upper, vol.In(VAR_UNITS)),
+ vol.Optional(CONF_RELVARREF, default='current'):
+ vol.All(vol.Upper, vol.In(RELVARREF))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ var = pypck.lcn_defs.Var[call.data[CONF_VARIABLE]]
+ value = call.data[CONF_VALUE]
+ unit = pypck.lcn_defs.VarUnit.parse(
+ call.data[CONF_UNIT_OF_MEASUREMENT])
+ value_ref = pypck.lcn_defs.RelVarRef[
+ call.data[CONF_RELVARREF]]
+
+ address_connection = self.get_address_connection(call)
+ address_connection.var_rel(var, value, unit, value_ref)
+
+
+class LockRegulator(LcnServiceCall):
+ """Locks a regulator setpoint."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_SETPOINT): vol.All(vol.Upper, vol.In(SETPOINTS)),
+ vol.Optional(CONF_STATE, default=False): bool,
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ setpoint = pypck.lcn_defs.Var[call.data[CONF_SETPOINT]]
+ state = call.data[CONF_STATE]
+
+ reg_id = pypck.lcn_defs.Var.to_set_point_id(setpoint)
+ address_connection = self.get_address_connection(call)
+ address_connection.lock_regulator(reg_id, state)
+
+
+class SendKeys(LcnServiceCall):
+ """Sends keys (which executes bound commands)."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_KEYS): cv.matches_regex(r'^([a-dA-D][1-8])+$'),
+ vol.Optional(CONF_STATE, default='hit'):
+ vol.All(vol.Upper, vol.In(SENDKEYCOMMANDS)),
+ vol.Optional(CONF_TIME, default=0): vol.All(int, vol.Range(min=0)),
+ vol.Optional(CONF_TIME_UNIT, default='s'): vol.All(vol.Upper,
+ vol.In(TIME_UNITS))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ address_connection = self.get_address_connection(call)
+
+ keys = [[False] * 8 for i in range(4)]
+
+ key_strings = zip(call.data[CONF_KEYS][::2],
+ call.data[CONF_KEYS][1::2])
+
+ for table, key in key_strings:
+ table_id = ord(table) - 65
+ key_id = int(key) - 1
+ keys[table_id][key_id] = True
+
+ delay_time = call.data[CONF_TIME]
+ if delay_time != 0:
+ hit = pypck.lcn_defs.SendKeyCommand.HIT
+ if pypck.lcn_defs.SendKeyCommand[
+ call.data[CONF_STATE]] != hit:
+ raise ValueError('Only hit command is allowed when sending'
+ ' deferred keys.')
+ delay_unit = pypck.lcn_defs.TimeUnit.parse(
+ call.data[CONF_TIME_UNIT])
+ address_connection.send_keys_hit_deferred(
+ keys, delay_time, delay_unit)
+ else:
+ state = pypck.lcn_defs.SendKeyCommand[
+ call.data[CONF_STATE]]
+ address_connection.send_keys(keys, state)
+
+
+class LockKeys(LcnServiceCall):
+ """Lock keys."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Optional(CONF_TABLE, default='a'): cv.matches_regex(r'^[a-dA-D]$'),
+ vol.Required(CONF_STATE): is_key_lock_states_string,
+ vol.Optional(CONF_TIME, default=0): vol.All(int, vol.Range(min=0)),
+ vol.Optional(CONF_TIME_UNIT, default='s'): vol.All(vol.Upper,
+ vol.In(TIME_UNITS))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ address_connection = self.get_address_connection(call)
+
+ states = [pypck.lcn_defs.KeyLockStateModifier[state]
+ for state in call.data[CONF_STATE]]
+ table_id = ord(call.data[CONF_TABLE]) - 65
+
+ delay_time = call.data[CONF_TIME]
+ if delay_time != 0:
+ if table_id != 0:
+ raise ValueError('Only table A is allowed when locking keys'
+ ' for a specific time.')
+ delay_unit = pypck.lcn_defs.TimeUnit.parse(
+ call.data[CONF_TIME_UNIT])
+ address_connection.lock_keys_tab_a_temporary(
+ delay_time, delay_unit, states)
+ else:
+ address_connection.lock_keys(table_id, states)
+
+ address_connection.request_status_locked_keys_timeout()
+
+
+class DynText(LcnServiceCall):
+ """Send dynamic text to LCN-GTxD displays."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_ROW): vol.All(int, vol.Range(min=1, max=4)),
+ vol.Required(CONF_TEXT): vol.All(str, vol.Length(max=60))
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ row_id = call.data[CONF_ROW] - 1
+ text = call.data[CONF_TEXT]
+
+ address_connection = self.get_address_connection(call)
+ address_connection.dyn_text(row_id, text)
+
+
+class Pck(LcnServiceCall):
+ """Send arbitrary PCK command."""
+
+ schema = LcnServiceCall.schema.extend({
+ vol.Required(CONF_PCK): str
+ })
+
+ def __call__(self, call):
+ """Execute service call."""
+ pck = call.data[CONF_PCK]
+ address_connection = self.get_address_connection(call)
+ address_connection.pck(pck)
diff --git a/homeassistant/components/lcn/services.yaml b/homeassistant/components/lcn/services.yaml
new file mode 100755
index 00000000000000..b8f4fbb20a7dd9
--- /dev/null
+++ b/homeassistant/components/lcn/services.yaml
@@ -0,0 +1,201 @@
+# Describes the format for available LCN services
+
+output_abs:
+ description: Set absolute brightness of output port in percent.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ output:
+ description: Output port
+ example: "output1"
+ brightness:
+ description: Absolute brightness in percent (0..100)
+ example: 50
+ transition:
+ description: Transition time in seconds
+ example: 5
+
+output_rel:
+ description: Set relative brightness of output port in percent.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ output:
+ description: Output port
+ example: "output1"
+ brightness:
+ description: Relative brightness in percent (-100..100)
+ example: 50
+ transition:
+ description: Transition time in seconds
+ example: 5
+
+output_toggle:
+ description: Toggle output port.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ output:
+ description: Output port
+ example: "output1"
+ transition:
+ description: Transition time in seconds
+ example: 5
+
+relays:
+ description: Set the relays status.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ state:
+ description: Relays states as string (1=on, 2=off, t=toggle, -=nochange)
+ example: "t---001-"
+
+led:
+ description: Set the led state.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ led:
+ description: Led
+ example: "led6"
+ state:
+ description: Led state
+ example: 'blink'
+ values:
+ - on
+ - off
+ - blink
+ - flicker
+
+var_abs:
+ description: Set absolute value of a variable or setpoint.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ variable:
+ description: Variable or setpoint name
+ example: 'var1'
+ value:
+ description: Value to set
+ example: '50'
+ unit_of_measurement:
+ description: Unit of value
+ example: 'celsius'
+
+var_reset:
+ description: Reset value of variable or setpoint.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ variable:
+ description: Variable or setpoint name
+ example: 'var1'
+
+var_rel:
+ description: Shift value of a variable, setpoint or threshold.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ variable:
+ description: Variable or setpoint name
+ example: 'var1'
+ value:
+ description: Shift value
+ example: '50'
+ unit_of_measurement:
+ description: Unit of value
+ example: 'celsius'
+ value_reference:
+ description: Reference value (current or programmed) for setpoint and threshold
+ example: 'current'
+ values:
+ - current
+ - prog
+
+lock_regulator:
+ description: Lock a regulator setpoint.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ setpoint:
+ description: Setpoint name
+ example: 'r1varsetpoint'
+ state:
+ description: New setpoint state
+ example: true
+
+send_keys:
+ description: Send keys (which executes bound commands).
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ keys:
+ description: Keys to send
+ example: 'a1a5d8'
+ state:
+ description: 'Key state upon sending (optional, must be hit for deferred)'
+ example: 'hit'
+ values:
+ - hit
+ - make
+ - break
+ time:
+ description: Send delay (optional)
+ example: 10
+ time_unit:
+ description: Time unit of send delay (optional)
+ example: 's'
+
+lock_keys:
+ description: Lock keys.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ table:
+ description: 'Table with keys to lock (optional, must be A for interval).'
+ example: 'A5'
+ state:
+ description: Key lock states as string (1=on, 2=off, T=toggle, -=nochange)
+ example: '1---t0--'
+ time:
+ description: Lock interval (optional)
+ example: 10
+ time_unit:
+ description: Time unit of lock interval (optional)
+ example: 's'
+
+dyn_text:
+ description: Send dynamic text to LCN-GTxD displays.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ row:
+ description: Text row 1..4 (support of 4 independent text rows)
+ example: 1
+ text:
+ description: Text to send (up to 60 characters encoded as UTF-8)
+ example: 'text up to 60 characters'
+
+pck:
+ description: Send arbitrary PCK command.
+ fields:
+ address:
+ description: Module address
+ example: 'myhome.s0.m7'
+ pck:
+ description: PCK command (without address header)
+ example: 'PIN4'
+
\ No newline at end of file
diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json
index ca9b578432b4a2..fd74d9831fca0a 100644
--- a/homeassistant/components/lifx/manifest.json
+++ b/homeassistant/components/lifx/manifest.json
@@ -7,6 +7,11 @@
"aiolifx==0.6.7",
"aiolifx_effects==0.2.2"
],
+ "homekit": {
+ "models": [
+ "LIFX"
+ ]
+ },
"dependencies": [],
"codeowners": [
"@amelchio"
diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py
index 70fe31e64d6808..43fe9cb2d52eaa 100644
--- a/homeassistant/components/logbook/__init__.py
+++ b/homeassistant/components/logbook/__init__.py
@@ -102,7 +102,7 @@ def log_message(service):
hass.http.register_view(LogbookView(config.get(DOMAIN, {})))
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'logbook', 'logbook', 'hass:format-list-bulleted-type')
hass.services.async_register(
diff --git a/homeassistant/components/logi_circle/.translations/fr.json b/homeassistant/components/logi_circle/.translations/fr.json
index 85e8edc6bb1fd4..7f8a2f2a098a8a 100644
--- a/homeassistant/components/logi_circle/.translations/fr.json
+++ b/homeassistant/components/logi_circle/.translations/fr.json
@@ -13,6 +13,20 @@
"auth_error": "L'autorisation de l'API a \u00e9chou\u00e9.",
"auth_timeout": "L'autorisation a expir\u00e9 lors de la demande du jeton d'acc\u00e8s.",
"follow_link": "Veuillez suivre le lien et vous authentifier avant d'appuyer sur Soumettre."
- }
+ },
+ "step": {
+ "auth": {
+ "description": "Suivez le lien ci-dessous et acceptez acc\u00e8s \u00e0 votre compte Logi Circle, puis revenez et appuyez sur Envoyer ci-dessous. \n\n [Lien] ( {authorization_url} )",
+ "title": "Authentifier avec Logi Circle"
+ },
+ "user": {
+ "data": {
+ "flow_impl": "Fournisseur"
+ },
+ "description": "Choisissez via quel fournisseur d'authentification vous souhaitez vous authentifier avec Logi Circle.",
+ "title": "Fournisseur d'authentification"
+ }
+ },
+ "title": "Logi Circle"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/logi_circle/.translations/pt-BR.json b/homeassistant/components/logi_circle/.translations/pt-BR.json
new file mode 100644
index 00000000000000..fd742194c6962a
--- /dev/null
+++ b/homeassistant/components/logi_circle/.translations/pt-BR.json
@@ -0,0 +1,22 @@
+{
+ "config": {
+ "create_entry": {
+ "default": "Autenticado com sucesso com o Logi Circle."
+ },
+ "error": {
+ "auth_error": "Falha na autoriza\u00e7\u00e3o da API."
+ },
+ "step": {
+ "auth": {
+ "title": "Autenticar com o Logi Circle"
+ },
+ "user": {
+ "data": {
+ "flow_impl": "Provedor"
+ },
+ "title": "Provedor de Autentica\u00e7\u00e3o"
+ }
+ },
+ "title": "C\u00edrculo Logi"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py
index 996e3f7b296e75..b1b9cf1a524078 100644
--- a/homeassistant/components/lovelace/__init__.py
+++ b/homeassistant/components/lovelace/__init__.py
@@ -26,6 +26,7 @@
}),
}, extra=vol.ALLOW_EXTRA)
+EVENT_LOVELACE_UPDATED = 'lovelace_updated'
LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml'
@@ -52,7 +53,7 @@ async def async_setup(hass, config):
# Pass in default to `get` because defaults not set if loaded as dep
mode = config.get(DOMAIN, {}).get(CONF_MODE, MODE_STORAGE)
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
DOMAIN, config={
'mode': mode
})
@@ -83,6 +84,7 @@ def __init__(self, hass):
"""Initialize Lovelace config based on storage helper."""
self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY)
self._data = None
+ self._hass = hass
async def async_get_info(self):
"""Return the YAML storage mode."""
@@ -115,6 +117,8 @@ async def async_save(self, config):
self._data['config'] = config
await self._store.async_save(self._data)
+ self._hass.bus.async_fire(EVENT_LOVELACE_UPDATED)
+
async def _load(self):
"""Load the config."""
data = await self._store.async_load()
diff --git a/homeassistant/components/mailbox/__init__.py b/homeassistant/components/mailbox/__init__.py
index 939cc4a2aa2a23..3b5012ec160e87 100644
--- a/homeassistant/components/mailbox/__init__.py
+++ b/homeassistant/components/mailbox/__init__.py
@@ -30,7 +30,7 @@
async def async_setup(hass, config):
"""Track states and offer events for mailboxes."""
mailboxes = []
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'mailbox', 'mailbox', 'mdi:mailbox')
hass.http.register_view(MailboxPlatformsView(mailboxes))
hass.http.register_view(MailboxMessageView(mailboxes))
diff --git a/homeassistant/components/map/__init__.py b/homeassistant/components/map/__init__.py
index df8ac49a6d5692..ab89ccf23ce369 100644
--- a/homeassistant/components/map/__init__.py
+++ b/homeassistant/components/map/__init__.py
@@ -4,6 +4,6 @@
async def async_setup(hass, config):
"""Register the built-in map panel."""
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'map', 'map', 'hass:tooltip-account')
return True
diff --git a/homeassistant/components/meteo_france/manifest.json b/homeassistant/components/meteo_france/manifest.json
index 20ad5e46fe628b..301d9538c20142 100644
--- a/homeassistant/components/meteo_france/manifest.json
+++ b/homeassistant/components/meteo_france/manifest.json
@@ -3,8 +3,10 @@
"name": "Meteo france",
"documentation": "https://www.home-assistant.io/components/meteo_france",
"requirements": [
- "meteofrance==0.3.4"
+ "meteofrance==0.3.7"
],
"dependencies": [],
- "codeowners": []
+ "codeowners": [
+ "@victorcerutti"
+ ]
}
diff --git a/homeassistant/components/meteoalarm/binary_sensor.py b/homeassistant/components/meteoalarm/binary_sensor.py
index 8af43d3b087883..e1ffbe1d9ad0ff 100644
--- a/homeassistant/components/meteoalarm/binary_sensor.py
+++ b/homeassistant/components/meteoalarm/binary_sensor.py
@@ -6,26 +6,22 @@
from homeassistant.components.binary_sensor import (
PLATFORM_SCHEMA, BinarySensorDevice)
-from homeassistant.const import (
- ATTR_ATTRIBUTION, CONF_NAME)
+from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME
import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__)
+ATTRIBUTION = "Information provided by MeteoAlarm"
+
CONF_COUNTRY = 'country'
-CONF_PROVINCE = 'province'
CONF_LANGUAGE = 'language'
+CONF_PROVINCE = 'province'
-ATTRIBUTION = "Information provided by MeteoAlarm."
-
-DEFAULT_NAME = 'meteoalarm'
DEFAULT_DEVICE_CLASS = 'safety'
-
-ICON = 'mdi:alert'
+DEFAULT_NAME = 'meteoalarm'
SCAN_INTERVAL = timedelta(minutes=30)
-
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_COUNTRY): cv.string,
vol.Required(CONF_PROVINCE): cv.string,
@@ -46,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
try:
api = Meteoalert(country, province, language)
except KeyError():
- _LOGGER.error("Wrong country digits, or province name")
+ _LOGGER.error("Wrong country digits or province name")
return
add_entities([MeteoAlertBinarySensor(api, name)], True)
@@ -78,14 +74,9 @@ def device_state_attributes(self):
self._attributes[ATTR_ATTRIBUTION] = ATTRIBUTION
return self._attributes
- @property
- def icon(self):
- """Icon to use in the frontend."""
- return ICON
-
@property
def device_class(self):
- """Return the class of this binary sensor."""
+ """Return the device class of this binary sensor."""
return DEFAULT_DEVICE_CLASS
def update(self):
diff --git a/homeassistant/components/meteoalarm/manifest.json b/homeassistant/components/meteoalarm/manifest.json
index d84749547ae6c0..015033a0e38a94 100644
--- a/homeassistant/components/meteoalarm/manifest.json
+++ b/homeassistant/components/meteoalarm/manifest.json
@@ -3,7 +3,7 @@
"name": "meteoalarm",
"documentation": "https://www.home-assistant.io/components/meteoalarm",
"requirements": [
- "meteoalertapi==0.0.8"
+ "meteoalertapi==0.1.3"
],
"dependencies": [],
"codeowners": ["@rolfberkenbosch"]
diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py
index abb7bcb7628c57..1d34babe3acf7b 100644
--- a/homeassistant/components/mobile_app/__init__.py
+++ b/homeassistant/components/mobile_app/__init__.py
@@ -7,13 +7,15 @@
from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME,
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION,
DATA_BINARY_SENSOR, DATA_CONFIG_ENTRIES, DATA_DELETED_IDS,
- DATA_DEVICES, DATA_SENSOR, DATA_STORE, DOMAIN, STORAGE_KEY,
- STORAGE_VERSION)
+ DATA_DEVICES, DATA_SENSOR, DATA_STORE,
+ DOMAIN, STORAGE_KEY, STORAGE_VERSION)
from .http_api import RegistrationsView
from .webhook import handle_webhook
from .websocket_api import register_websocket_handlers
+PLATFORMS = 'sensor', 'binary_sensor', 'device_tracker'
+
async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the mobile app component."""
@@ -24,7 +26,6 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType):
DATA_BINARY_SENSOR: {},
DATA_CONFIG_ENTRIES: {},
DATA_DELETED_IDS: [],
- DATA_DEVICES: {},
DATA_SENSOR: {}
}
@@ -83,10 +84,8 @@ async def async_setup_entry(hass, entry):
webhook_register(hass, DOMAIN, registration_name, webhook_id,
handle_webhook)
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry,
- DATA_BINARY_SENSOR))
- hass.async_create_task(
- hass.config_entries.async_forward_entry_setup(entry, DATA_SENSOR))
+ for domain in PLATFORMS:
+ hass.async_create_task(
+ hass.config_entries.async_forward_entry_setup(entry, domain))
return True
diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py
index 8b33406216e496..922835c1d40d32 100644
--- a/homeassistant/components/mobile_app/const.py
+++ b/homeassistant/components/mobile_app/const.py
@@ -160,6 +160,7 @@
COMBINED_CLASSES = sorted(set(BINARY_SENSOR_CLASSES + SENSOR_CLASSES))
SIGNAL_SENSOR_UPDATE = DOMAIN + '_sensor_update'
+SIGNAL_LOCATION_UPDATE = DOMAIN + '_location_update_{}'
REGISTER_SENSOR_SCHEMA = vol.Schema({
vol.Optional(ATTR_SENSOR_ATTRIBUTES, default={}): dict,
diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py
new file mode 100644
index 00000000000000..7fb76f3af413b8
--- /dev/null
+++ b/homeassistant/components/mobile_app/device_tracker.py
@@ -0,0 +1,167 @@
+"""Device tracker platform that adds support for OwnTracks over MQTT."""
+import logging
+
+from homeassistant.core import callback
+from homeassistant.const import (
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ ATTR_BATTERY_LEVEL,
+)
+from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS
+from homeassistant.components.device_tracker.config_entry import (
+ DeviceTrackerEntity
+)
+from homeassistant.helpers.restore_state import RestoreEntity
+from .const import (
+ ATTR_ALTITUDE,
+ ATTR_BATTERY,
+ ATTR_COURSE,
+ ATTR_DEVICE_ID,
+ ATTR_DEVICE_NAME,
+ ATTR_GPS_ACCURACY,
+ ATTR_GPS,
+ ATTR_LOCATION_NAME,
+ ATTR_SPEED,
+ ATTR_VERTICAL_ACCURACY,
+
+ SIGNAL_LOCATION_UPDATE,
+)
+from .helpers import device_info
+
+_LOGGER = logging.getLogger(__name__)
+ATTR_KEYS = (
+ ATTR_ALTITUDE,
+ ATTR_COURSE,
+ ATTR_SPEED,
+ ATTR_VERTICAL_ACCURACY
+)
+
+
+async def async_setup_entry(hass, entry, async_add_entities):
+ """Set up OwnTracks based off an entry."""
+ entity = MobileAppEntity(entry)
+ async_add_entities([entity])
+ return True
+
+
+class MobileAppEntity(DeviceTrackerEntity, RestoreEntity):
+ """Represent a tracked device."""
+
+ def __init__(self, entry, data=None):
+ """Set up OwnTracks entity."""
+ self._entry = entry
+ self._data = data
+ self._dispatch_unsub = None
+
+ @property
+ def unique_id(self):
+ """Return the unique ID."""
+ return self._entry.data[ATTR_DEVICE_ID]
+
+ @property
+ def battery_level(self):
+ """Return the battery level of the device."""
+ return self._data.get(ATTR_BATTERY)
+
+ @property
+ def device_state_attributes(self):
+ """Return device specific attributes."""
+ attrs = {}
+ for key in ATTR_KEYS:
+ value = self._data.get(key)
+ if value is not None:
+ attrs[key] = value
+
+ return attrs
+
+ @property
+ def location_accuracy(self):
+ """Return the gps accuracy of the device."""
+ return self._data.get(ATTR_GPS_ACCURACY)
+
+ @property
+ def latitude(self):
+ """Return latitude value of the device."""
+ gps = self._data.get(ATTR_GPS)
+
+ if gps is None:
+ return None
+
+ return gps[0]
+
+ @property
+ def longitude(self):
+ """Return longitude value of the device."""
+ gps = self._data.get(ATTR_GPS)
+
+ if gps is None:
+ return None
+
+ return gps[1]
+
+ @property
+ def location_name(self):
+ """Return a location name for the current location of the device."""
+ return self._data.get(ATTR_LOCATION_NAME)
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._entry.data[ATTR_DEVICE_NAME]
+
+ @property
+ def should_poll(self):
+ """No polling needed."""
+ return False
+
+ @property
+ def source_type(self):
+ """Return the source type, eg gps or router, of the device."""
+ return SOURCE_TYPE_GPS
+
+ @property
+ def device_info(self):
+ """Return the device info."""
+ return device_info(self._entry.data)
+
+ async def async_added_to_hass(self):
+ """Call when entity about to be added to Home Assistant."""
+ await super().async_added_to_hass()
+ self._dispatch_unsub = \
+ self.hass.helpers.dispatcher.async_dispatcher_connect(
+ SIGNAL_LOCATION_UPDATE.format(self._entry.entry_id),
+ self.update_data
+ )
+
+ # Don't restore if we got set up with data.
+ if self._data is not None:
+ return
+
+ state = await self.async_get_last_state()
+
+ if state is None:
+ self._data = {}
+ return
+
+ attr = state.attributes
+ data = {
+ ATTR_GPS: (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)),
+ ATTR_GPS_ACCURACY: attr.get(ATTR_GPS_ACCURACY),
+ ATTR_BATTERY: attr.get(ATTR_BATTERY_LEVEL),
+ }
+ data.update({key: attr[key] for key in attr if key in ATTR_KEYS})
+ self._data = data
+
+ async def async_will_remove_from_hass(self):
+ """Call when entity is being removed from hass."""
+ await super().async_will_remove_from_hass()
+
+ if self._dispatch_unsub:
+ self._dispatch_unsub()
+ self._dispatch_unsub = None
+
+ @callback
+ def update_data(self, data):
+ """Mark the device as seen."""
+ self._data = data
+ self.async_write_ha_state()
diff --git a/homeassistant/components/mobile_app/entity.py b/homeassistant/components/mobile_app/entity.py
index eca9d2b024bd40..8c1747d6f2b4f7 100644
--- a/homeassistant/components/mobile_app/entity.py
+++ b/homeassistant/components/mobile_app/entity.py
@@ -6,11 +6,11 @@
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
-from .const import (ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
- ATTR_MODEL, ATTR_OS_VERSION, ATTR_SENSOR_ATTRIBUTES,
+from .const import (ATTR_SENSOR_ATTRIBUTES,
ATTR_SENSOR_DEVICE_CLASS, ATTR_SENSOR_ICON,
ATTR_SENSOR_NAME, ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID,
DOMAIN, SIGNAL_SENSOR_UPDATE)
+from .helpers import device_info
def sensor_id(webhook_id, unique_id):
@@ -76,17 +76,7 @@ def unique_id(self):
@property
def device_info(self):
"""Return device registry information for this entity."""
- return {
- 'identifiers': {
- (ATTR_DEVICE_ID, self._registration[ATTR_DEVICE_ID]),
- (CONF_WEBHOOK_ID, self._registration[CONF_WEBHOOK_ID])
- },
- 'manufacturer': self._registration[ATTR_MANUFACTURER],
- 'model': self._registration[ATTR_MODEL],
- 'device_name': self._registration[ATTR_DEVICE_NAME],
- 'sw_version': self._registration[ATTR_OS_VERSION],
- 'config_entries': self._device.config_entries
- }
+ return device_info(self._registration)
async def async_update(self):
"""Get the latest state of the sensor."""
diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py
index 6aec43074648ad..30c111fe0b4702 100644
--- a/homeassistant/components/mobile_app/helpers.py
+++ b/homeassistant/components/mobile_app/helpers.py
@@ -9,7 +9,7 @@
from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.typing import HomeAssistantType
-from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME,
+from .const import (ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, ATTR_DEVICE_ID,
ATTR_APP_VERSION, ATTR_DEVICE_NAME, ATTR_MANUFACTURER,
ATTR_MODEL, ATTR_OS_VERSION, ATTR_SUPPORTS_ENCRYPTION,
CONF_SECRET, CONF_USER_ID, DATA_BINARY_SENSOR,
@@ -148,3 +148,16 @@ def webhook_response(data, *, registration: Dict, status: int = 200,
return Response(text=data, status=status, content_type='application/json',
headers=headers)
+
+
+def device_info(registration: Dict) -> Dict:
+ """Return the device info for this registration."""
+ return {
+ 'identifiers': {
+ (DOMAIN, registration[ATTR_DEVICE_ID]),
+ },
+ 'manufacturer': registration[ATTR_MANUFACTURER],
+ 'model': registration[ATTR_MODEL],
+ 'device_name': registration[ATTR_DEVICE_NAME],
+ 'sw_version': registration[ATTR_OS_VERSION],
+ }
diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json
index 969817b62c7292..85c6231daa8839 100644
--- a/homeassistant/components/mobile_app/manifest.json
+++ b/homeassistant/components/mobile_app/manifest.json
@@ -7,7 +7,6 @@
"PyNaCl==1.3.0"
],
"dependencies": [
- "device_tracker",
"http",
"webhook"
],
diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py
index 721751e69a8c81..e10ebf13c4c17d 100644
--- a/homeassistant/components/mobile_app/notify.py
+++ b/homeassistant/components/mobile_app/notify.py
@@ -89,13 +89,12 @@ async def async_send_message(self, message="", **kwargs):
targets = kwargs.get(ATTR_TARGET)
if not targets:
- targets = push_registrations(self.hass)
+ targets = push_registrations(self.hass).values()
if kwargs.get(ATTR_DATA) is not None:
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
for target in targets:
-
entry = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target]
entry_data = entry.data
diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py
index 1ef5f4ce531764..40002b5cfec5d4 100644
--- a/homeassistant/components/mobile_app/webhook.py
+++ b/homeassistant/components/mobile_app/webhook.py
@@ -6,10 +6,6 @@
from homeassistant.components.cloud import (async_remote_ui_url,
CloudNotAvailable)
-from homeassistant.components.device_tracker import (ATTR_ATTRIBUTES,
- ATTR_DEV_ID,
- DOMAIN as DT_DOMAIN,
- SERVICE_SEE as DT_SEE)
from homeassistant.components.frontend import MANIFEST_JSON
from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN
@@ -24,13 +20,12 @@
from homeassistant.helpers.template import attach
from homeassistant.helpers.typing import HomeAssistantType
-from .const import (ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID,
+from .const import (ATTR_DEVICE_ID,
ATTR_DEVICE_NAME, ATTR_EVENT_DATA, ATTR_EVENT_TYPE,
- ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME,
ATTR_MANUFACTURER, ATTR_MODEL, ATTR_OS_VERSION,
- ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID, ATTR_SPEED,
+ ATTR_SENSOR_TYPE, ATTR_SENSOR_UNIQUE_ID,
ATTR_SUPPORTS_ENCRYPTION, ATTR_TEMPLATE,
- ATTR_TEMPLATE_VARIABLES, ATTR_VERTICAL_ACCURACY,
+ ATTR_TEMPLATE_VARIABLES,
ATTR_WEBHOOK_DATA, ATTR_WEBHOOK_ENCRYPTED,
ATTR_WEBHOOK_ENCRYPTED_DATA, ATTR_WEBHOOK_TYPE,
CONF_CLOUDHOOK_URL, CONF_REMOTE_UI_URL, CONF_SECRET,
@@ -43,7 +38,7 @@
WEBHOOK_TYPE_REGISTER_SENSOR, WEBHOOK_TYPE_RENDER_TEMPLATE,
WEBHOOK_TYPE_UPDATE_LOCATION,
WEBHOOK_TYPE_UPDATE_REGISTRATION,
- WEBHOOK_TYPE_UPDATE_SENSOR_STATES)
+ WEBHOOK_TYPE_UPDATE_SENSOR_STATES, SIGNAL_LOCATION_UPDATE)
from .helpers import (_decrypt_payload, empty_okay_response, error_response,
@@ -149,37 +144,9 @@ async def handle_webhook(hass: HomeAssistantType, webhook_id: str,
headers=headers)
if webhook_type == WEBHOOK_TYPE_UPDATE_LOCATION:
- see_payload = {
- ATTR_DEV_ID: registration[ATTR_DEVICE_ID],
- ATTR_GPS: data[ATTR_GPS],
- ATTR_GPS_ACCURACY: data[ATTR_GPS_ACCURACY],
- }
-
- for key in (ATTR_LOCATION_NAME, ATTR_BATTERY):
- value = data.get(key)
- if value is not None:
- see_payload[key] = value
-
- attrs = {}
-
- for key in (ATTR_ALTITUDE, ATTR_COURSE,
- ATTR_SPEED, ATTR_VERTICAL_ACCURACY):
- value = data.get(key)
- if value is not None:
- attrs[key] = value
-
- if attrs:
- see_payload[ATTR_ATTRIBUTES] = attrs
-
- try:
- await hass.services.async_call(DT_DOMAIN,
- DT_SEE, see_payload,
- blocking=True, context=context)
- # noqa: E722 pylint: disable=broad-except
- except (vol.Invalid, ServiceNotFound, Exception) as ex:
- _LOGGER.error("Error when updating location during mobile_app "
- "webhook (device name: %s): %s",
- registration[ATTR_DEVICE_NAME], ex)
+ hass.helpers.dispatcher.async_dispatcher_send(
+ SIGNAL_LOCATION_UPDATE.format(config_entry.entry_id), data
+ )
return empty_okay_response(headers=headers)
if webhook_type == WEBHOOK_TYPE_UPDATE_REGISTRATION:
diff --git a/homeassistant/components/mqtt/.translations/ca.json b/homeassistant/components/mqtt/.translations/ca.json
index 1fc3ea628bb866..47dc4d344bcee4 100644
--- a/homeassistant/components/mqtt/.translations/ca.json
+++ b/homeassistant/components/mqtt/.translations/ca.json
@@ -22,7 +22,7 @@
"data": {
"discovery": "Habilitar descobriment autom\u00e0tic"
},
- "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de hass.io {addon}?",
+ "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb el broker MQTT proporcionat pel complement de Hass.io: {addon}?",
"title": "Broker MQTT a trav\u00e9s del complement de Hass.io"
}
},
diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json
index aacac084b198d6..ac27652cbdd6b6 100644
--- a/homeassistant/components/mqtt/.translations/ru.json
+++ b/homeassistant/components/mqtt/.translations/ru.json
@@ -13,7 +13,7 @@
"discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432",
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
- "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f"
+ "username": "\u041b\u043e\u0433\u0438\u043d"
},
"description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0412\u0430\u0448\u0435\u043c\u0443 \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT.",
"title": "MQTT"
diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py
index 33ad34b25ff3af..a49c83d2dd97d6 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -344,8 +344,8 @@ def get_room_ids(self):
"""Return all module available on the API as a list."""
if not self.setup():
return []
- for key in self.homestatus.rooms:
- self.room_ids.append(key)
+ for room in self.homestatus.rooms:
+ self.room_ids.append(room)
return self.room_ids
def setup(self):
@@ -365,6 +365,7 @@ def setup(self):
def update(self):
"""Call the NetAtmo API to update the data."""
import pyatmo
+
try:
self.homestatus = pyatmo.HomeStatus(self.auth, home=self.home)
except TypeError:
@@ -372,40 +373,52 @@ def update(self):
return
_LOGGER.debug("Following is the debugging output for homestatus:")
_LOGGER.debug(self.homestatus.rawData)
- for key in self.homestatus.rooms:
- roomstatus = {}
- homestatus_room = self.homestatus.rooms[key]
- homedata_room = self.homedata.rooms[self.home][key]
- roomstatus['roomID'] = homestatus_room['id']
- roomstatus['roomname'] = homedata_room['name']
- roomstatus['target_temperature'] = \
- homestatus_room['therm_setpoint_temperature']
- roomstatus['setpoint_mode'] = \
- homestatus_room['therm_setpoint_mode']
- roomstatus['current_temperature'] = \
- homestatus_room['therm_measured_temperature']
- roomstatus['module_type'] = \
- self.homestatus.thermostatType(self.home, key)
- roomstatus['module_id'] = None
- roomstatus['heating_status'] = None
- roomstatus['heating_power_request'] = None
- for module_id in homedata_room['module_ids']:
- if self.homedata.modules[self.home][module_id]['type'] == \
- NA_THERM or roomstatus['module_id'] is None:
- roomstatus['module_id'] = module_id
- if roomstatus['module_type'] == NA_THERM:
- self.boilerstatus = self.homestatus.boilerStatus(
- rid=roomstatus['module_id'])
- roomstatus['heating_status'] = self.boilerstatus
- elif roomstatus['module_type'] == NA_VALVE:
- roomstatus['heating_power_request'] = \
- homestatus_room['heating_power_request']
- roomstatus['heating_status'] = \
- roomstatus['heating_power_request'] > 0
- if self.boilerstatus is not None:
- roomstatus['heating_status'] = \
- self.boilerstatus and roomstatus['heating_status']
- self.room_status[key] = roomstatus
+ for room in self.homestatus.rooms:
+ try:
+ roomstatus = {}
+ homestatus_room = self.homestatus.rooms[room]
+ homedata_room = self.homedata.rooms[self.home][room]
+ roomstatus["roomID"] = homestatus_room["id"]
+ roomstatus["roomname"] = homedata_room["name"]
+ roomstatus["target_temperature"] = homestatus_room[
+ "therm_setpoint_temperature"
+ ]
+ roomstatus["setpoint_mode"] = homestatus_room[
+ "therm_setpoint_mode"
+ ]
+ roomstatus["current_temperature"] = homestatus_room[
+ "therm_measured_temperature"
+ ]
+ roomstatus["module_type"] = self.homestatus.thermostatType(
+ self.home, room
+ )
+ roomstatus["module_id"] = None
+ roomstatus["heating_status"] = None
+ roomstatus["heating_power_request"] = None
+ for module_id in homedata_room["module_ids"]:
+ if (self.homedata.modules[self.home][module_id]["type"]
+ == NA_THERM
+ or roomstatus["module_id"] is None):
+ roomstatus["module_id"] = module_id
+ if roomstatus["module_type"] == NA_THERM:
+ self.boilerstatus = self.homestatus.boilerStatus(
+ rid=roomstatus["module_id"]
+ )
+ roomstatus["heating_status"] = self.boilerstatus
+ elif roomstatus["module_type"] == NA_VALVE:
+ roomstatus["heating_power_request"] = homestatus_room[
+ "heating_power_request"
+ ]
+ roomstatus["heating_status"] = (
+ roomstatus["heating_power_request"] > 0
+ )
+ if self.boilerstatus is not None:
+ roomstatus["heating_status"] = (
+ self.boilerstatus and roomstatus["heating_status"]
+ )
+ self.room_status[room] = roomstatus
+ except KeyError as err:
+ _LOGGER.error("Update of room %s failed. Error: %s", room, err)
self.away_temperature = self.homestatus.getAwaytemp(self.home)
self.hg_temperature = self.homestatus.getHgtemp(self.home)
self.setpoint_duration = self.homedata.setpoint_duration[self.home]
diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py
index 22ca9e696f3970..dabfb827aea0e7 100644
--- a/homeassistant/components/netatmo/sensor.py
+++ b/homeassistant/components/netatmo/sensor.py
@@ -145,7 +145,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
# Only create sensors for monitored properties
for condition in monitored_conditions:
dev.append(NetatmoSensor(
- data, module_name, condition.lower()))
+ data, module_name, condition.lower(),
+ config.get(CONF_STATION)))
for module_name, _ in not_handled.items():
_LOGGER.error('Module name: "%s" not found', module_name)
@@ -164,13 +165,14 @@ def all_product_classes():
class NetatmoSensor(Entity):
"""Implementation of a Netatmo sensor."""
- def __init__(self, netatmo_data, module_name, sensor_type):
+ def __init__(self, netatmo_data, module_name, sensor_type, station):
"""Initialize the sensor."""
self._name = 'Netatmo {} {}'.format(module_name,
SENSOR_TYPES[sensor_type][0])
self.netatmo_data = netatmo_data
self.module_name = module_name
self.type = sensor_type
+ self.station_name = station
self._state = None
self._device_class = SENSOR_TYPES[self.type][3]
self._icon = SENSOR_TYPES[self.type][2]
@@ -178,7 +180,8 @@ def __init__(self, netatmo_data, module_name, sensor_type):
self._module_type = self.netatmo_data. \
station_data.moduleByName(module=module_name)['type']
module_id = self.netatmo_data. \
- station_data.moduleByName(module=module_name)['_id']
+ station_data.moduleByName(station=self.station_name,
+ module=module_name)['_id']
self._unique_id = '{}-{}'.format(module_id, self.type)
@property
diff --git a/homeassistant/components/onboarding/.translations/fr.json b/homeassistant/components/onboarding/.translations/fr.json
index 8a8ff47a48a417..d8ae0b34033b60 100644
--- a/homeassistant/components/onboarding/.translations/fr.json
+++ b/homeassistant/components/onboarding/.translations/fr.json
@@ -2,6 +2,6 @@
"area": {
"bedroom": "Chambre",
"kitchen": "Cuisine",
- "living_room": "Salle De S\u00e9jour"
+ "living_room": "Salon"
}
}
\ No newline at end of file
diff --git a/homeassistant/components/onboarding/.translations/hu.json b/homeassistant/components/onboarding/.translations/hu.json
new file mode 100644
index 00000000000000..262fca71470885
--- /dev/null
+++ b/homeassistant/components/onboarding/.translations/hu.json
@@ -0,0 +1,7 @@
+{
+ "area": {
+ "bedroom": "H\u00e1l\u00f3szoba",
+ "kitchen": "Konyha",
+ "living_room": "Nappali"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onboarding/.translations/pt-BR.json b/homeassistant/components/onboarding/.translations/pt-BR.json
new file mode 100644
index 00000000000000..d5a09a0b24002e
--- /dev/null
+++ b/homeassistant/components/onboarding/.translations/pt-BR.json
@@ -0,0 +1,7 @@
+{
+ "area": {
+ "bedroom": "Quarto",
+ "kitchen": "Cozinha",
+ "living_room": "Sala de estar"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/onboarding/.translations/sl.json b/homeassistant/components/onboarding/.translations/sl.json
new file mode 100644
index 00000000000000..c340a26a5c8446
--- /dev/null
+++ b/homeassistant/components/onboarding/.translations/sl.json
@@ -0,0 +1,7 @@
+{
+ "area": {
+ "bedroom": "Spalnica",
+ "kitchen": "Kuhinja",
+ "living_room": "Dnevna soba"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py
index a4df4303fa8784..1cc7a050aec321 100644
--- a/homeassistant/components/owntracks/__init__.py
+++ b/homeassistant/components/owntracks/__init__.py
@@ -192,6 +192,7 @@ def __init__(self, hass, secret, max_gps_accuracy, import_waypoints,
self.region_mapping = region_mapping
self.events_only = events_only
self.mqtt_topic = mqtt_topic
+ self._pending_msg = []
@callback
def async_valid_accuracy(self, message):
@@ -222,10 +223,19 @@ def async_valid_accuracy(self, message):
return True
+ @callback
+ def set_async_see(self, func):
+ """Set a new async_see function."""
+ self.async_see = func
+ for msg in self._pending_msg:
+ func(**msg)
+ self._pending_msg.clear()
+
+ # pylint: disable=method-hidden
@callback
def async_see(self, **data):
"""Send a see message to the device tracker."""
- raise NotImplementedError
+ self._pending_msg.append(data)
@callback
def async_see_beacons(self, hass, dev_id, kwargs_param):
diff --git a/homeassistant/components/owntracks/device_tracker.py b/homeassistant/components/owntracks/device_tracker.py
index fb9fedf26faa3d..742b7c34435387 100644
--- a/homeassistant/components/owntracks/device_tracker.py
+++ b/homeassistant/components/owntracks/device_tracker.py
@@ -2,10 +2,19 @@
import logging
from homeassistant.core import callback
-from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT
+from homeassistant.const import (
+ ATTR_GPS_ACCURACY,
+ ATTR_LATITUDE,
+ ATTR_LONGITUDE,
+ ATTR_BATTERY_LEVEL,
+)
+from homeassistant.components.device_tracker.const import (
+ ENTITY_ID_FORMAT, ATTR_SOURCE_TYPE)
from homeassistant.components.device_tracker.config_entry import (
DeviceTrackerEntity
)
+from homeassistant.helpers.restore_state import RestoreEntity
+from homeassistant.helpers import device_registry
from . import DOMAIN as OT_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -14,53 +23,52 @@
async def async_setup_entry(hass, entry, async_add_entities):
"""Set up OwnTracks based off an entry."""
@callback
- def _receive_data(dev_id, host_name, gps, attributes, gps_accuracy=None,
- battery=None, source_type=None, location_name=None):
+ def _receive_data(dev_id, **data):
"""Receive set location."""
- device = hass.data[OT_DOMAIN]['devices'].get(dev_id)
-
- if device is not None:
- device.update_data(
- host_name=host_name,
- gps=gps,
- attributes=attributes,
- gps_accuracy=gps_accuracy,
- battery=battery,
- source_type=source_type,
- location_name=location_name,
- )
+ entity = hass.data[OT_DOMAIN]['devices'].get(dev_id)
+
+ if entity is not None:
+ entity.update_data(data)
return
- device = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity(
- dev_id=dev_id,
- host_name=host_name,
- gps=gps,
- attributes=attributes,
- gps_accuracy=gps_accuracy,
- battery=battery,
- source_type=source_type,
- location_name=location_name,
+ entity = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity(
+ dev_id, data
)
- async_add_entities([device])
+ async_add_entities([entity])
+
+ hass.data[OT_DOMAIN]['context'].set_async_see(_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] == OT_DOMAIN
+ }
+
+ if not dev_ids:
+ return True
+
+ entities = []
+ for dev_id in dev_ids:
+ entity = hass.data[OT_DOMAIN]['devices'][dev_id] = OwnTracksEntity(
+ dev_id
+ )
+ entities.append(entity)
+
+ async_add_entities(entities)
- hass.data[OT_DOMAIN]['context'].async_see = _receive_data
return True
-class OwnTracksEntity(DeviceTrackerEntity):
+class OwnTracksEntity(DeviceTrackerEntity, RestoreEntity):
"""Represent a tracked device."""
- def __init__(self, dev_id, host_name, gps, attributes, gps_accuracy,
- battery, source_type, location_name):
+ def __init__(self, dev_id, data=None):
"""Set up OwnTracks entity."""
self._dev_id = dev_id
- self._host_name = host_name
- self._gps = gps
- self._gps_accuracy = gps_accuracy
- self._location_name = location_name
- self._attributes = attributes
- self._battery = battery
- self._source_type = source_type
+ self._data = data or {}
self.entity_id = ENTITY_ID_FORMAT.format(dev_id)
@property
@@ -71,43 +79,45 @@ def unique_id(self):
@property
def battery_level(self):
"""Return the battery level of the device."""
- return self._battery
+ return self._data.get('battery')
@property
def device_state_attributes(self):
"""Return device specific attributes."""
- return self._attributes
+ return self._data.get('attributes')
@property
def location_accuracy(self):
"""Return the gps accuracy of the device."""
- return self._gps_accuracy
+ return self._data.get('gps_accuracy')
@property
def latitude(self):
"""Return latitude value of the device."""
- if self._gps is not None:
- return self._gps[0]
+ # Check with "get" instead of "in" because value can be None
+ if self._data.get('gps'):
+ return self._data['gps'][0]
return None
@property
def longitude(self):
"""Return longitude value of the device."""
- if self._gps is not None:
- return self._gps[1]
+ # Check with "get" instead of "in" because value can be None
+ if self._data.get('gps'):
+ return self._data['gps'][1]
return None
@property
def location_name(self):
"""Return a location name for the current location of the device."""
- return self._location_name
+ return self._data.get('location_name')
@property
def name(self):
"""Return the name of the device."""
- return self._host_name
+ return self._data.get('host_name')
@property
def should_poll(self):
@@ -117,26 +127,40 @@ def should_poll(self):
@property
def source_type(self):
"""Return the source type, eg gps or router, of the device."""
- return self._source_type
+ return self._data.get('source_type')
@property
def device_info(self):
"""Return the device info."""
return {
- 'name': self._host_name,
+ 'name': self.name,
'identifiers': {(OT_DOMAIN, self._dev_id)},
}
+ async def async_added_to_hass(self):
+ """Call when entity about to be added to Home Assistant."""
+ await super().async_added_to_hass()
+
+ # Don't restore if we got set up with data.
+ if self._data:
+ return
+
+ state = await self.async_get_last_state()
+
+ if state is None:
+ return
+
+ attr = state.attributes
+ self._data = {
+ 'host_name': state.name,
+ 'gps': (attr.get(ATTR_LATITUDE), attr.get(ATTR_LONGITUDE)),
+ 'gps_accuracy': attr.get(ATTR_GPS_ACCURACY),
+ 'battery': attr.get(ATTR_BATTERY_LEVEL),
+ 'source_type': attr.get(ATTR_SOURCE_TYPE),
+ }
+
@callback
- def update_data(self, host_name, gps, attributes, gps_accuracy,
- battery, source_type, location_name):
+ def update_data(self, data):
"""Mark the device as seen."""
- self._host_name = host_name
- self._gps = gps
- self._gps_accuracy = gps_accuracy
- self._location_name = location_name
- self._attributes = attributes
- self._battery = battery
- self._source_type = source_type
-
+ self._data = data
self.async_write_ha_state()
diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py
index f6a4fcdb733930..275d80facf46a8 100644
--- a/homeassistant/components/panel_custom/__init__.py
+++ b/homeassistant/components/panel_custom/__init__.py
@@ -112,7 +112,7 @@ async def async_register_panel(
config['_panel_custom'] = custom_panel_config
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
component_name='custom',
sidebar_title=sidebar_title,
sidebar_icon=sidebar_icon,
diff --git a/homeassistant/components/panel_iframe/__init__.py b/homeassistant/components/panel_iframe/__init__.py
index f4038c82f71880..fca33b1cf98406 100644
--- a/homeassistant/components/panel_iframe/__init__.py
+++ b/homeassistant/components/panel_iframe/__init__.py
@@ -32,7 +32,7 @@
async def async_setup(hass, config):
"""Set up the iFrame frontend panels."""
for url_path, info in config[DOMAIN].items():
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'iframe', info.get(CONF_TITLE), info.get(CONF_ICON),
url_path, {'url': info[CONF_URL]},
require_admin=info[CONF_REQUIRE_ADMIN])
diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py
index 1d71b59cde16bc..5ce375ffe03d21 100644
--- a/homeassistant/components/plex/media_player.py
+++ b/homeassistant/components/plex/media_player.py
@@ -6,7 +6,6 @@
import requests
import voluptuous as vol
-from homeassistant import util
from homeassistant.components.media_player import (
MediaPlayerDevice, PLATFORM_SCHEMA)
from homeassistant.components.media_player.const import (
@@ -16,16 +15,13 @@
from homeassistant.const import (
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
from homeassistant.helpers import config_validation as cv
-from homeassistant.helpers.event import track_utc_time_change
+from homeassistant.helpers.event import track_time_interval
from homeassistant.util import dt as dt_util
from homeassistant.util.json import load_json, save_json
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
-MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
-MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
-
NAME_FORMAT = 'Plex {}'
PLEX_CONFIG_FILE = 'plex.conf'
PLEX_DATA = 'plex'
@@ -131,9 +127,9 @@ def setup_plexserver(
plex_clients = hass.data[PLEX_DATA]
plex_sessions = {}
- track_utc_time_change(hass, lambda now: update_devices(), second=30)
+ track_time_interval(
+ hass, lambda now: update_devices(), timedelta(seconds=10))
- @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_devices():
"""Update the devices objects."""
try:
@@ -211,6 +207,9 @@ def update_devices():
or client.machine_identifier
in plex_sessions)
+ if client not in new_plex_clients:
+ client.schedule_update_ha_state()
+
if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) \
or client.available:
continue
@@ -226,8 +225,6 @@ def update_devices():
if new_plex_clients:
add_entities_callback(new_plex_clients)
- update_devices()
-
def request_configuration(host, hass, config, add_entities_callback):
"""Request configuration steps from the user."""
@@ -497,6 +494,11 @@ def force_idle(self):
self._session = None
self._clear_media_details()
+ @property
+ def should_poll(self):
+ """Return True if entity has to be polled for state."""
+ return False
+
@property
def unique_id(self):
"""Return the id of this plex client."""
@@ -542,10 +544,6 @@ def state(self):
"""Return the state of the device."""
return self._state
- def update(self):
- """Get the latest details."""
- self.update_devices(no_throttle=True)
-
@property
def _active_media_plexapi_type(self):
"""Get the active media type required by PlexAPI commands."""
@@ -688,6 +686,7 @@ def set_volume_level(self, volume):
self.device.setVolume(
int(volume * 100), self._active_media_plexapi_type)
self._volume_level = volume # store since we can't retrieve
+ self.update_devices()
@property
def volume_level(self):
@@ -724,16 +723,19 @@ def media_play(self):
"""Send play command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self.device.play(self._active_media_plexapi_type)
+ self.update_devices()
def media_pause(self):
"""Send pause command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self.device.pause(self._active_media_plexapi_type)
+ self.update_devices()
def media_stop(self):
"""Send stop command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self.device.stop(self._active_media_plexapi_type)
+ self.update_devices()
def turn_off(self):
"""Turn the client off."""
@@ -744,11 +746,13 @@ def media_next_track(self):
"""Send next track command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self.device.skipNext(self._active_media_plexapi_type)
+ self.update_devices()
def media_previous_track(self):
"""Send previous track command."""
if self.device and 'playback' in self._device_protocol_capabilities:
self.device.skipPrevious(self._active_media_plexapi_type)
+ self.update_devices()
def play_media(self, media_type, media_id, **kwargs):
"""Play a piece of media."""
@@ -852,6 +856,7 @@ def _client_play_media(self, media, delete=False, **params):
'/playQueues/{}?window=100&own=1'.format(
playqueue.playQueueID),
}, **params))
+ self.update_devices()
@property
def device_state_attributes(self):
diff --git a/homeassistant/components/ps4/.translations/fr.json b/homeassistant/components/ps4/.translations/fr.json
index cfd65c910d9b67..03baf0c032e3a3 100644
--- a/homeassistant/components/ps4/.translations/fr.json
+++ b/homeassistant/components/ps4/.translations/fr.json
@@ -8,6 +8,7 @@
"port_997_bind_error": "Impossible de se connecter au port 997."
},
"error": {
+ "credential_timeout": "Le service d'informations d'identification a expir\u00e9. Appuyez sur soumettre pour red\u00e9marrer.",
"login_failed": "\u00c9chec de l'association \u00e0 la PlayStation 4. V\u00e9rifiez que le code PIN est correct.",
"no_ipaddress": "Entrez l'adresse IP de la PlayStation 4 que vous souhaitez configurer.",
"not_ready": "PlayStation 4 n'est pas allum\u00e9e ou connect\u00e9e au r\u00e9seau."
diff --git a/homeassistant/components/ps4/.translations/hu.json b/homeassistant/components/ps4/.translations/hu.json
index 6a0008957232e8..77b13f33a51c3d 100644
--- a/homeassistant/components/ps4/.translations/hu.json
+++ b/homeassistant/components/ps4/.translations/hu.json
@@ -1,6 +1,16 @@
{
"config": {
"step": {
+ "creds": {
+ "title": "PlayStation 4"
+ },
+ "link": {
+ "data": {
+ "ip_address": "IP-c\u00edm",
+ "name": "N\u00e9v",
+ "region": "R\u00e9gi\u00f3"
+ }
+ },
"mode": {
"data": {
"mode": "Konfigur\u00e1ci\u00f3s m\u00f3d"
diff --git a/homeassistant/components/ps4/.translations/pt-BR.json b/homeassistant/components/ps4/.translations/pt-BR.json
new file mode 100644
index 00000000000000..e74254727872a1
--- /dev/null
+++ b/homeassistant/components/ps4/.translations/pt-BR.json
@@ -0,0 +1,7 @@
+{
+ "config": {
+ "error": {
+ "credential_timeout": "Servi\u00e7o de credencial expirou. Pressione Submit para reiniciar."
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/ps4/.translations/sl.json b/homeassistant/components/ps4/.translations/sl.json
index 429a409fb7ec22..f51bc45e0e8001 100644
--- a/homeassistant/components/ps4/.translations/sl.json
+++ b/homeassistant/components/ps4/.translations/sl.json
@@ -8,6 +8,7 @@
"port_997_bind_error": "Ne morem se povezati z vrati 997. Dodatne informacije najdete v [dokumentaciji] (https://www.home-assistant.io/components/ps4/)."
},
"error": {
+ "credential_timeout": "Storitev poverilnic je potekla. Pritisnite Po\u0161lji za ponovni zagon.",
"login_failed": "Neuspelo seznanjanje s PlayStation 4. Preverite, ali je koda PIN pravilna.",
"no_ipaddress": "Vnesite IP naslov PlayStation-a 4, ki ga \u017eelite konfigurirati.",
"not_ready": "PlayStation 4 ni vklopljen ali povezan z omre\u017ejem."
diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py
index f08abf5fd4a4cd..568ea8ece325f9 100644
--- a/homeassistant/components/remote/__init__.py
+++ b/homeassistant/components/remote/__init__.py
@@ -24,6 +24,8 @@
ATTR_NUM_REPEATS = 'num_repeats'
ATTR_DELAY_SECS = 'delay_secs'
ATTR_HOLD_SECS = 'hold_secs'
+ATTR_ALTERNATIVE = 'alternative'
+ATTR_TIMEOUT = 'timeout'
DOMAIN = 'remote'
SCAN_INTERVAL = timedelta(seconds=30)
@@ -36,12 +38,15 @@
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
SERVICE_SEND_COMMAND = 'send_command'
+SERVICE_LEARN_COMMAND = 'learn_command'
SERVICE_SYNC = 'sync'
DEFAULT_NUM_REPEATS = 1
DEFAULT_DELAY_SECS = 0.4
DEFAULT_HOLD_SECS = 0
+SUPPORT_LEARN_COMMAND = 1
+
REMOTE_SERVICE_SCHEMA = vol.Schema({
vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids,
})
@@ -59,6 +64,13 @@
vol.Optional(ATTR_HOLD_SECS, default=DEFAULT_HOLD_SECS): vol.Coerce(float),
})
+REMOTE_SERVICE_LEARN_COMMAND_SCHEMA = REMOTE_SERVICE_SCHEMA.extend({
+ vol.Optional(ATTR_DEVICE): cv.string,
+ vol.Optional(ATTR_COMMAND): vol.All(cv.ensure_list, [cv.string]),
+ vol.Optional(ATTR_ALTERNATIVE): cv.boolean,
+ vol.Optional(ATTR_TIMEOUT): cv.positive_int
+})
+
@bind_hass
def is_on(hass, entity_id=None):
@@ -93,12 +105,22 @@ async def async_setup(hass, config):
'async_send_command'
)
+ component.async_register_entity_service(
+ SERVICE_LEARN_COMMAND, REMOTE_SERVICE_LEARN_COMMAND_SCHEMA,
+ 'async_learn_command'
+ )
+
return True
class RemoteDevice(ToggleEntity):
"""Representation of a remote."""
+ @property
+ def supported_features(self):
+ """Flag supported features."""
+ return 0
+
def send_command(self, command, **kwargs):
"""Send a command to a device."""
raise NotImplementedError()
@@ -108,5 +130,17 @@ def async_send_command(self, command, **kwargs):
This method must be run in the event loop and returns a coroutine.
"""
- return self.hass.async_add_job(ft.partial(
- self.send_command, command, **kwargs))
+ return self.hass.async_add_executor_job(
+ ft.partial(self.send_command, command, **kwargs))
+
+ def learn_command(self, **kwargs):
+ """Learn a command from a device."""
+ raise NotImplementedError()
+
+ def async_learn_command(self, **kwargs):
+ """Learn a command from a device.
+
+ This method must be run in the event loop and returns a coroutine.
+ """
+ return self.hass.async_add_executor_job(
+ ft.partial(self.learn_command, **kwargs))
diff --git a/homeassistant/components/remote/services.yaml b/homeassistant/components/remote/services.yaml
index 62615f28714c76..a551ba18ed4587 100644
--- a/homeassistant/components/remote/services.yaml
+++ b/homeassistant/components/remote/services.yaml
@@ -25,7 +25,7 @@ turn_off:
example: 'remote.family_room'
send_command:
- description: Sends a single command to a single device.
+ description: Sends a command or a list of commands to a device.
fields:
entity_id:
description: Name(s) of entities to send command from.
@@ -46,6 +46,25 @@ send_command:
description: An optional value that specifies that number of seconds you want to have it held before the release is send. If not specified, the release will be send immediately after the press.
example: '2.5'
+learn_command:
+ description: Learns a command or a list of commands from a device.
+ fields:
+ entity_id:
+ description: Name(s) of entities to learn command from.
+ example: 'remote.bedroom'
+ device:
+ description: Device ID to learn command from.
+ example: 'television'
+ command:
+ description: A single command or a list of commands to learn.
+ example: 'Turn on'
+ alternative:
+ description: If code must be stored as alternative (useful for discrete remotes).
+ example: 'True'
+ timeout:
+ description: Timeout, in seconds, for the command to be learned.
+ example: '30'
+
harmony_sync:
description: Syncs the remote's configuration.
diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py
new file mode 100755
index 00000000000000..24382b2f12d124
--- /dev/null
+++ b/homeassistant/components/repetier/__init__.py
@@ -0,0 +1,248 @@
+"""Support for Repetier-Server sensors."""
+import logging
+from datetime import timedelta
+
+import voluptuous as vol
+
+from homeassistant.const import (
+ CONF_API_KEY, CONF_HOST, CONF_MONITORED_CONDITIONS, CONF_NAME, CONF_PORT,
+ CONF_SENSORS, TEMP_CELSIUS)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.discovery import load_platform
+from homeassistant.helpers.dispatcher import dispatcher_send
+from homeassistant.helpers.event import track_time_interval
+from homeassistant.util import slugify as util_slugify
+
+_LOGGER = logging.getLogger(__name__)
+
+DEFAULT_NAME = 'RepetierServer'
+DOMAIN = 'repetier'
+REPETIER_API = 'repetier_api'
+SCAN_INTERVAL = timedelta(seconds=10)
+UPDATE_SIGNAL = 'repetier_update_signal'
+
+TEMP_DATA = {
+ 'tempset': 'temp_set',
+ 'tempread': 'state',
+ 'output': 'output',
+}
+
+
+API_PRINTER_METHODS = {
+ 'bed_temperature': {
+ 'offline': {'heatedbeds': None, 'state': 'off'},
+ 'state': {'heatedbeds': 'temp_data'},
+ 'temp_data': TEMP_DATA,
+ 'attribute': 'heatedbeds',
+ },
+ 'extruder_temperature': {
+ 'offline': {'extruder': None, 'state': 'off'},
+ 'state': {'extruder': 'temp_data'},
+ 'temp_data': TEMP_DATA,
+ 'attribute': 'extruder',
+ },
+ 'chamber_temperature': {
+ 'offline': {'heatedchambers': None, 'state': 'off'},
+ 'state': {'heatedchambers': 'temp_data'},
+ 'temp_data': TEMP_DATA,
+ 'attribute': 'heatedchambers',
+ },
+ 'current_state': {
+ 'offline': {'state': None},
+ 'state': {
+ 'state': 'state',
+ 'activeextruder': 'active_extruder',
+ 'hasxhome': 'x_homed',
+ 'hasyhome': 'y_homed',
+ 'haszhome': 'z_homed',
+ 'firmware': 'firmware',
+ 'firmwareurl': 'firmware_url',
+ },
+ },
+ 'current_job': {
+ 'offline': {'job': None, 'state': 'off'},
+ 'state': {
+ 'done': 'state',
+ 'job': 'job_name',
+ 'jobid': 'job_id',
+ 'totallines': 'total_lines',
+ 'linessent': 'lines_sent',
+ 'oflayer': 'total_layers',
+ 'layer': 'current_layer',
+ 'speedmultiply': 'feed_rate',
+ 'flowmultiply': 'flow',
+ 'x': 'x',
+ 'y': 'y',
+ 'z': 'z',
+ },
+ },
+ 'job_end': {
+ 'offline': {
+ 'job': None, 'state': 'off', 'start': None, 'printtime': None},
+ 'state': {
+ 'job': 'job_name',
+ 'start': 'start',
+ 'printtime': 'print_time',
+ 'printedtimecomp': 'from_start',
+ },
+ },
+ 'job_start': {
+ 'offline': {
+ 'job': None,
+ 'state': 'off',
+ 'start': None,
+ 'printedtimecomp': None
+ },
+ 'state': {
+ 'job': 'job_name',
+ 'start': 'start',
+ 'printedtimecomp': 'from_start',
+ },
+ },
+}
+
+
+def has_all_unique_names(value):
+ """Validate that printers have an unique name."""
+ names = [util_slugify(printer[CONF_NAME]) for printer in value]
+ vol.Schema(vol.Unique())(names)
+ return value
+
+
+SENSOR_TYPES = {
+ # Type, Unit, Icon
+ 'bed_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer',
+ '_bed_'],
+ 'extruder_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer',
+ '_extruder_'],
+ 'chamber_temperature': ['temperature', TEMP_CELSIUS, 'mdi:thermometer',
+ '_chamber_'],
+ 'current_state': ['state', None, 'mdi:printer-3d', ''],
+ 'current_job': ['progress', '%', 'mdi:file-percent', '_current_job'],
+ 'job_end': ['progress', None, 'mdi:clock-end', '_job_end'],
+ 'job_start': ['progress', None, 'mdi:clock-start', '_job_start'],
+}
+
+SENSOR_SCHEMA = vol.Schema({
+ vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
+ vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+})
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
+ vol.Required(CONF_API_KEY): cv.string,
+ vol.Required(CONF_HOST): cv.string,
+ vol.Optional(CONF_PORT, default=3344): cv.port,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
+ })], has_all_unique_names),
+}, extra=vol.ALLOW_EXTRA)
+
+
+def setup(hass, config):
+ """Set up the Repetier Server component."""
+ import pyrepetier
+
+ hass.data[REPETIER_API] = {}
+
+ for repetier in config[DOMAIN]:
+ _LOGGER.debug("Repetier server config %s", repetier[CONF_HOST])
+
+ url = "http://{}".format(repetier[CONF_HOST])
+ port = repetier[CONF_PORT]
+ api_key = repetier[CONF_API_KEY]
+
+ client = pyrepetier.Repetier(
+ url=url,
+ port=port,
+ apikey=api_key)
+
+ printers = client.getprinters()
+
+ if not printers:
+ return False
+
+ sensors = repetier[CONF_SENSORS][CONF_MONITORED_CONDITIONS]
+ api = PrinterAPI(hass, client, printers, sensors,
+ repetier[CONF_NAME], config)
+ api.update()
+ track_time_interval(hass, api.update, SCAN_INTERVAL)
+
+ hass.data[REPETIER_API][repetier[CONF_NAME]] = api
+
+ return True
+
+
+class PrinterAPI:
+ """Handle the printer API."""
+
+ def __init__(self, hass, client, printers, sensors, conf_name, config):
+ """Set up instance."""
+ self._hass = hass
+ self._client = client
+ self.printers = printers
+ self.sensors = sensors
+ self.conf_name = conf_name
+ self.config = config
+ self._known_entities = set()
+
+ def get_data(self, printer_id, sensor_type, temp_id):
+ """Get data from the state cache."""
+ printer = self.printers[printer_id]
+ methods = API_PRINTER_METHODS[sensor_type]
+ for prop, offline in methods['offline'].items():
+ state = getattr(printer, prop)
+ if state == offline:
+ # if state matches offline, sensor is offline
+ return None
+
+ data = {}
+ for prop, attr in methods['state'].items():
+ prop_data = getattr(printer, prop)
+ if attr == 'temp_data':
+ temp_methods = methods['temp_data']
+ for temp_prop, temp_attr in temp_methods.items():
+ data[temp_attr] = getattr(prop_data[temp_id], temp_prop)
+ else:
+ data[attr] = prop_data
+ return data
+
+ def update(self, now=None):
+ """Update the state cache from the printer API."""
+ for printer in self.printers:
+ printer.get_data()
+ self._load_entities()
+ dispatcher_send(self._hass, UPDATE_SIGNAL)
+
+ def _load_entities(self):
+ sensor_info = []
+ for pidx, printer in enumerate(self.printers):
+ for sensor_type in self.sensors:
+ info = {}
+ info['sensor_type'] = sensor_type
+ info['printer_id'] = pidx
+ info['name'] = printer.slug
+ info['printer_name'] = self.conf_name
+
+ known = '{}-{}'.format(printer.slug, sensor_type)
+ if known in self._known_entities:
+ continue
+
+ methods = API_PRINTER_METHODS[sensor_type]
+ if 'temp_data' in methods['state'].values():
+ prop_data = getattr(printer, methods['attribute'])
+ if prop_data is None:
+ continue
+ for idx, _ in enumerate(prop_data):
+ info['temp_id'] = idx
+ sensor_info.append(info)
+ else:
+ info['temp_id'] = None
+ sensor_info.append(info)
+
+ self._known_entities.add(known)
+
+ if not sensor_info:
+ return
+ load_platform(self._hass, 'sensor', DOMAIN, sensor_info, self.config)
diff --git a/homeassistant/components/repetier/manifest.json b/homeassistant/components/repetier/manifest.json
new file mode 100755
index 00000000000000..14af98cfb641e6
--- /dev/null
+++ b/homeassistant/components/repetier/manifest.json
@@ -0,0 +1,10 @@
+{
+ "domain": "repetier",
+ "name": "Repetier Server",
+ "documentation": "https://www.home-assistant.io/components/repetier",
+ "requirements": [
+ "pyrepetier==3.0.5"
+ ],
+ "dependencies": [],
+ "codeowners": ["@MTrab"]
+}
diff --git a/homeassistant/components/repetier/sensor.py b/homeassistant/components/repetier/sensor.py
new file mode 100755
index 00000000000000..17f999a95cfbff
--- /dev/null
+++ b/homeassistant/components/repetier/sensor.py
@@ -0,0 +1,215 @@
+"""Support for monitoring Repetier Server Sensors."""
+from datetime import datetime
+import logging
+import time
+
+from homeassistant.const import DEVICE_CLASS_TIMESTAMP
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity import Entity
+
+from . import REPETIER_API, SENSOR_TYPES, UPDATE_SIGNAL
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Set up the available Repetier Server sensors."""
+ if discovery_info is None:
+ return
+
+ sensor_map = {
+ 'bed_temperature': RepetierTempSensor,
+ 'extruder_temperature': RepetierTempSensor,
+ 'chamber_temperature': RepetierTempSensor,
+ 'current_state': RepetierSensor,
+ 'current_job': RepetierJobSensor,
+ 'job_end': RepetierJobEndSensor,
+ 'job_start': RepetierJobStartSensor,
+ }
+
+ entities = []
+ for info in discovery_info:
+ printer_name = info['printer_name']
+ api = hass.data[REPETIER_API][printer_name]
+ printer_id = info['printer_id']
+ sensor_type = info['sensor_type']
+ temp_id = info['temp_id']
+ name = info['name']
+ if temp_id is not None:
+ name = '{}{}{}'.format(
+ name, SENSOR_TYPES[sensor_type][3], temp_id)
+ else:
+ name = '{}{}'.format(name, SENSOR_TYPES[sensor_type][3])
+ sensor_class = sensor_map[sensor_type]
+ entity = sensor_class(api, temp_id, name, printer_id, sensor_type)
+ entities.append(entity)
+
+ add_entities(entities, True)
+
+
+class RepetierSensor(Entity):
+ """Class to create and populate a Repetier Sensor."""
+
+ def __init__(self, api, temp_id, name, printer_id, sensor_type):
+ """Init new sensor."""
+ self._api = api
+ self._attributes = {}
+ self._available = False
+ self._temp_id = temp_id
+ self._name = name
+ self._printer_id = printer_id
+ self._sensor_type = sensor_type
+ self._state = None
+
+ @property
+ def available(self) -> bool:
+ """Return True if entity is available."""
+ return self._available
+
+ @property
+ def device_state_attributes(self):
+ """Return sensor attributes."""
+ return self._attributes
+
+ @property
+ def name(self):
+ """Return the name of the sensor."""
+ return self._name
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement of this entity, if any."""
+ return SENSOR_TYPES[self._sensor_type][1]
+
+ @property
+ def icon(self):
+ """Icon to use in the frontend."""
+ return SENSOR_TYPES[self._sensor_type][2]
+
+ @property
+ def should_poll(self):
+ """Return False as entity is updated from the component."""
+ return False
+
+ @property
+ def state(self):
+ """Return sensor state."""
+ return self._state
+
+ @callback
+ def update_callback(self):
+ """Get new data and update state."""
+ self.async_schedule_update_ha_state(True)
+
+ async def async_added_to_hass(self):
+ """Connect update callbacks."""
+ async_dispatcher_connect(
+ self.hass, UPDATE_SIGNAL, self.update_callback)
+
+ def _get_data(self):
+ """Return new data from the api cache."""
+ data = self._api.get_data(
+ self._printer_id, self._sensor_type, self._temp_id)
+ if data is None:
+ _LOGGER.debug(
+ "Data not found for %s and %s",
+ self._sensor_type, self._temp_id)
+ self._available = False
+ return None
+ self._available = True
+ return data
+
+ def update(self):
+ """Update the sensor."""
+ data = self._get_data()
+ if data is None:
+ return
+ state = data.pop('state')
+ _LOGGER.debug("Printer %s State %s", self._name, state)
+ self._attributes.update(data)
+ self._state = state
+
+
+class RepetierTempSensor(RepetierSensor):
+ """Represent a Repetier temp sensor."""
+
+ @property
+ def state(self):
+ """Return sensor state."""
+ if self._state is None:
+ return None
+ return round(self._state, 2)
+
+ def update(self):
+ """Update the sensor."""
+ data = self._get_data()
+ if data is None:
+ return
+ state = data.pop('state')
+ temp_set = data['temp_set']
+ _LOGGER.debug(
+ "Printer %s Setpoint: %s, Temp: %s",
+ self._name, temp_set, state)
+ self._attributes.update(data)
+ self._state = state
+
+
+class RepetierJobSensor(RepetierSensor):
+ """Represent a Repetier job sensor."""
+
+ @property
+ def state(self):
+ """Return sensor state."""
+ if self._state is None:
+ return None
+ return round(self._state, 2)
+
+
+class RepetierJobEndSensor(RepetierSensor):
+ """Class to create and populate a Repetier Job End timestamp Sensor."""
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return DEVICE_CLASS_TIMESTAMP
+
+ def update(self):
+ """Update the sensor."""
+ data = self._get_data()
+ if data is None:
+ return
+ job_name = data['job_name']
+ start = data['start']
+ print_time = data['print_time']
+ from_start = data['from_start']
+ time_end = start + round(print_time, 0)
+ self._state = datetime.utcfromtimestamp(time_end).isoformat()
+ remaining = print_time - from_start
+ remaining_secs = int(round(remaining, 0))
+ _LOGGER.debug(
+ "Job %s remaining %s",
+ job_name, time.strftime('%H:%M:%S', time.gmtime(remaining_secs)))
+
+
+class RepetierJobStartSensor(RepetierSensor):
+ """Class to create and populate a Repetier Job Start timestamp Sensor."""
+
+ @property
+ def device_class(self):
+ """Return the device class."""
+ return DEVICE_CLASS_TIMESTAMP
+
+ def update(self):
+ """Update the sensor."""
+ data = self._get_data()
+ if data is None:
+ return
+ job_name = data['job_name']
+ start = data['start']
+ from_start = data['from_start']
+ self._state = datetime.utcfromtimestamp(start).isoformat()
+ elapsed_secs = int(round(from_start, 0))
+ _LOGGER.debug(
+ "Job %s elapsed %s",
+ job_name, time.strftime('%H:%M:%S', time.gmtime(elapsed_secs)))
diff --git a/homeassistant/components/rflink/manifest.json b/homeassistant/components/rflink/manifest.json
index a3b81f39c55f71..bbdb49ad401240 100644
--- a/homeassistant/components/rflink/manifest.json
+++ b/homeassistant/components/rflink/manifest.json
@@ -3,7 +3,7 @@
"name": "Rflink",
"documentation": "https://www.home-assistant.io/components/rflink",
"requirements": [
- "rflink==0.0.37"
+ "rflink==0.0.46"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py
index 7266b2fb1e5ab2..85d5cd90e08c44 100644
--- a/homeassistant/components/sense/__init__.py
+++ b/homeassistant/components/sense/__init__.py
@@ -1,11 +1,14 @@
"""Support for monitoring a Sense energy sensor."""
import logging
+from datetime import timedelta
import voluptuous as vol
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import async_load_platform
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from homeassistant.helpers.event import async_track_time_interval
_LOGGER = logging.getLogger(__name__)
@@ -15,6 +18,7 @@
DOMAIN = 'sense'
SENSE_DATA = 'sense_data'
+SENSE_DEVICE_UPDATE = 'sense_devices_update'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@@ -27,7 +31,9 @@
async def async_setup(hass, config):
"""Set up the Sense sensor."""
- from sense_energy import ASyncSenseable, SenseAuthenticationException
+ from sense_energy import (
+ ASyncSenseable, SenseAuthenticationException,
+ SenseAPITimeoutException)
username = config[DOMAIN][CONF_EMAIL]
password = config[DOMAIN][CONF_PASSWORD]
@@ -45,4 +51,15 @@ async def async_setup(hass, config):
async_load_platform(hass, 'sensor', DOMAIN, {}, config))
hass.async_create_task(
async_load_platform(hass, 'binary_sensor', DOMAIN, {}, config))
+
+ async def async_sense_update(now):
+ """Retrieve latest state."""
+ try:
+ await hass.data[SENSE_DATA].update_realtime()
+ async_dispatcher_send(hass, SENSE_DEVICE_UPDATE)
+ except SenseAPITimeoutException:
+ _LOGGER.error("Timeout retrieving data")
+
+ async_track_time_interval(hass, async_sense_update,
+ timedelta(seconds=ACTIVE_UPDATE_RATE))
return True
diff --git a/homeassistant/components/sense/binary_sensor.py b/homeassistant/components/sense/binary_sensor.py
index a0f65ac555a585..43a2dc79a89b2b 100644
--- a/homeassistant/components/sense/binary_sensor.py
+++ b/homeassistant/components/sense/binary_sensor.py
@@ -2,8 +2,10 @@
import logging
from homeassistant.components.binary_sensor import BinarySensorDevice
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.core import callback
-from . import SENSE_DATA
+from . import SENSE_DATA, SENSE_DEVICE_UPDATE
_LOGGER = logging.getLogger(__name__)
@@ -75,12 +77,12 @@ def __init__(self, data, device):
self._id = device['id']
self._icon = sense_to_mdi(device['icon'])
self._data = data
- self._state = False
+ self._undo_dispatch_subscription = None
@property
def is_on(self):
"""Return true if the binary sensor is on."""
- return self._state
+ return self._name in self._data.active_devices
@property
def name(self):
@@ -102,12 +104,22 @@ def device_class(self):
"""Return the device class of the binary sensor."""
return BIN_SENSOR_CLASS
- async def async_update(self):
- """Retrieve latest state."""
- from sense_energy.sense_api import SenseAPITimeoutException
- try:
- await self._data.update_realtime()
- except SenseAPITimeoutException:
- _LOGGER.error("Timeout retrieving data")
- return
- self._state = self._name in self._data.active_devices
+ @property
+ def should_poll(self):
+ """Return the deviceshould not poll for updates."""
+ return False
+
+ async def async_added_to_hass(self):
+ """Register callbacks."""
+ @callback
+ def update():
+ """Update the state."""
+ self.async_schedule_update_ha_state(True)
+
+ self._undo_dispatch_subscription = async_dispatcher_connect(
+ self.hass, SENSE_DEVICE_UPDATE, update)
+
+ async def async_will_remove_from_hass(self):
+ """Undo subscription."""
+ if self._undo_dispatch_subscription:
+ self._undo_dispatch_subscription()
diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json
index 272a4a58f33835..8763234c5ed64d 100644
--- a/homeassistant/components/sense/manifest.json
+++ b/homeassistant/components/sense/manifest.json
@@ -6,5 +6,5 @@
"sense_energy==0.7.0"
],
"dependencies": [],
- "codeowners": []
+ "codeowners": ["@kbickar"]
}
diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py
index cfcbfdd4224477..6318d8581c33b8 100644
--- a/homeassistant/components/shopping_list/__init__.py
+++ b/homeassistant/components/shopping_list/__init__.py
@@ -117,7 +117,7 @@ def complete_item_service(call):
'What is on my shopping list'
])
- yield from hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'shopping-list', 'shopping_list', 'mdi:cart')
hass.components.websocket_api.async_register_command(
diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json
index b6bb1285daac75..ac94f5801195fc 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==3.4.1"
+ "simplisafe-python==3.4.2"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/solaredge_local/__init__.py b/homeassistant/components/solaredge_local/__init__.py
new file mode 100644
index 00000000000000..bf9d724dd545e3
--- /dev/null
+++ b/homeassistant/components/solaredge_local/__init__.py
@@ -0,0 +1 @@
+"""The SolarEdge Local Integration."""
diff --git a/homeassistant/components/solaredge_local/manifest.json b/homeassistant/components/solaredge_local/manifest.json
new file mode 100644
index 00000000000000..5fb07011983edd
--- /dev/null
+++ b/homeassistant/components/solaredge_local/manifest.json
@@ -0,0 +1,8 @@
+{
+ "domain": "solaredge_local",
+ "name": "Solar Edge Local",
+ "documentation": "",
+ "dependencies": [],
+ "codeowners": ["@drobtravels"],
+ "requirements": ["solaredge-local==0.1.4"]
+ }
\ No newline at end of file
diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py
new file mode 100644
index 00000000000000..8be4ceda7c7e8a
--- /dev/null
+++ b/homeassistant/components/solaredge_local/sensor.py
@@ -0,0 +1,159 @@
+"""
+Support for SolarEdge Monitoring API.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/sensor.solaredge_local/
+"""
+import logging
+from datetime import timedelta
+
+from requests.exceptions import HTTPError, ConnectTimeout
+from solaredge_local import SolarEdge
+import voluptuous as vol
+
+
+from homeassistant.components.sensor import PLATFORM_SCHEMA
+from homeassistant.const import (
+ CONF_IP_ADDRESS, CONF_NAME, POWER_WATT,
+ ENERGY_WATT_HOUR)
+import homeassistant.helpers.config_validation as cv
+from homeassistant.helpers.entity import Entity
+from homeassistant.util import Throttle
+
+DOMAIN = 'solaredge_local'
+UPDATE_DELAY = timedelta(seconds=10)
+
+# Supported sensor types:
+# Key: ['json_key', 'name', unit, icon]
+SENSOR_TYPES = {
+ 'lifetime_energy': ['energyTotal', "Lifetime energy",
+ ENERGY_WATT_HOUR, 'mdi:solar-power'],
+ 'energy_this_year': ['energyThisYear', "Energy this year",
+ ENERGY_WATT_HOUR, 'mdi:solar-power'],
+ 'energy_this_month': ['energyThisMonth', "Energy this month",
+ ENERGY_WATT_HOUR, 'mdi:solar-power'],
+ 'energy_today': ['energyToday', "Energy today",
+ ENERGY_WATT_HOUR, 'mdi:solar-power'],
+ 'current_power': ['currentPower', "Current Power",
+ POWER_WATT, 'mdi:solar-power']
+}
+
+PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
+ vol.Required(CONF_IP_ADDRESS): cv.string,
+ vol.Optional(CONF_NAME, default='SolarEdge'): cv.string,
+})
+
+_LOGGER = logging.getLogger(__name__)
+
+
+def setup_platform(hass, config, add_entities, discovery_info=None):
+ """Create the SolarEdge Monitoring API sensor."""
+ ip_address = config[CONF_IP_ADDRESS]
+ platform_name = config[CONF_NAME]
+
+ # Create new SolarEdge object to retrieve data
+ api = SolarEdge("http://{}/".format(ip_address))
+
+ # Check if api can be reached and site is active
+ try:
+ status = api.get_status()
+
+ status.energy # pylint: disable=pointless-statement
+ _LOGGER.debug("Credentials correct and site is active")
+ except AttributeError:
+ _LOGGER.error("Missing details data in solaredge response")
+ _LOGGER.debug("Response is: %s", status)
+ return
+ except (ConnectTimeout, HTTPError):
+ _LOGGER.error("Could not retrieve details from SolarEdge API")
+ return
+
+ # Create solaredge data service which will retrieve and update the data.
+ data = SolarEdgeData(hass, api)
+
+ # Create a new sensor for each sensor type.
+ entities = []
+ for sensor_key in SENSOR_TYPES:
+ sensor = SolarEdgeSensor(platform_name, sensor_key, data)
+ entities.append(sensor)
+
+ add_entities(entities, True)
+
+
+class SolarEdgeSensor(Entity):
+ """Representation of an SolarEdge Monitoring API sensor."""
+
+ def __init__(self, platform_name, sensor_key, data):
+ """Initialize the sensor."""
+ self.platform_name = platform_name
+ self.sensor_key = sensor_key
+ self.data = data
+ self._state = None
+
+ self._json_key = SENSOR_TYPES[self.sensor_key][0]
+ self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2]
+
+ @property
+ def name(self):
+ """Return the name."""
+ return "{} ({})".format(self.platform_name,
+ SENSOR_TYPES[self.sensor_key][1])
+
+ @property
+ def unit_of_measurement(self):
+ """Return the unit of measurement."""
+ return self._unit_of_measurement
+
+ @property
+ def icon(self):
+ """Return the sensor icon."""
+ return SENSOR_TYPES[self.sensor_key][3]
+
+ @property
+ def state(self):
+ """Return the state of the sensor."""
+ return self._state
+
+ def update(self):
+ """Get the latest data from the sensor and update the state."""
+ self.data.update()
+ self._state = self.data.data[self._json_key]
+
+
+class SolarEdgeData:
+ """Get and update the latest data."""
+
+ def __init__(self, hass, api):
+ """Initialize the data object."""
+ self.hass = hass
+ self.api = api
+ self.data = {}
+
+ @Throttle(UPDATE_DELAY)
+ def update(self):
+ """Update the data from the SolarEdge Monitoring API."""
+ try:
+ response = self.api.get_status()
+ _LOGGER.debug("response from SolarEdge: %s", response)
+
+ self.data["energyTotal"] = response.energy.total
+ self.data["energyThisYear"] = response.energy.thisYear
+ self.data["energyThisMonth"] = response.energy.thisMonth
+ self.data["energyToday"] = response.energy.today
+ self.data["currentPower"] = response.powerWatt
+
+ _LOGGER.debug("Updated SolarEdge overview data: %s", self.data)
+ except AttributeError:
+ _LOGGER.error("Missing details data in solaredge response")
+ _LOGGER.debug("Response is: %s", response)
+ return
+ except (ConnectTimeout, HTTPError):
+ _LOGGER.error("Could not retrieve data, skipping update")
+ return
+
+ self.data["energyTotal"] = response.energy.total
+ self.data["energyThisYear"] = response.energy.thisYear
+ self.data["energyThisMonth"] = response.energy.thisMonth
+ self.data["energyToday"] = response.energy.today
+ self.data["currentPower"] = response.powerWatt
+ _LOGGER.debug("Updated SolarEdge overview data: %s", self.data)
diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json
index 58fa7b49f884c6..b1f4c924fc4d8b 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.12"
+ "pysonos==0.0.14"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py
index 40369597646278..5f86327e88dfac 100644
--- a/homeassistant/components/sonos/media_player.py
+++ b/homeassistant/components/sonos/media_player.py
@@ -19,7 +19,7 @@
SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK, SUPPORT_SELECT_SOURCE,
SUPPORT_SHUFFLE_SET, SUPPORT_STOP, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET)
from homeassistant.const import (
- ENTITY_MATCH_ALL, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING)
+ ENTITY_MATCH_ALL, STATE_IDLE, STATE_PAUSED, STATE_PLAYING)
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.dt import utcnow
@@ -118,6 +118,9 @@ def _discovered_player(soco):
_discovered_player,
interface_addr=config.get(CONF_INTERFACE_ADDR))
+ for entity in hass.data[DATA_SONOS].entities:
+ entity.check_unseen()
+
hass.helpers.event.call_later(DISCOVERY_INTERVAL, _discovery)
hass.async_add_executor_job(_discovery)
@@ -307,8 +310,6 @@ def state(self):
return STATE_PAUSED
if self._status in ('PLAYING', 'TRANSITIONING'):
return STATE_PLAYING
- if self._status == 'OFF':
- return STATE_OFF
return STATE_IDLE
@property
@@ -330,15 +331,36 @@ def seen(self):
"""Record that this player was seen right now."""
self._seen = time.monotonic()
+ if self._available:
+ return
+
+ self._available = True
+ self._set_basic_information()
+ self._subscribe_to_player_events()
+ self.schedule_update_ha_state()
+
+ def check_unseen(self):
+ """Make this player unavailable if it was not seen recently."""
+ if not self._available:
+ return
+
+ if self._seen < time.monotonic() - 2*DISCOVERY_INTERVAL:
+ self._available = False
+
+ def _unsub(subscriptions):
+ for subscription in subscriptions:
+ subscription.unsubscribe()
+ self.hass.add_job(_unsub, self._subscriptions)
+
+ self._subscriptions = []
+
+ self.schedule_update_ha_state()
+
@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._available
- def _check_available(self):
- """Check that we saw the player recently."""
- return self._seen > time.monotonic() - 2*DISCOVERY_INTERVAL
-
def _set_basic_information(self):
"""Set initial entity information."""
speaker_info = self.soco.get_speaker_info(True)
@@ -352,7 +374,9 @@ def _set_basic_information(self):
def _set_favorites(self):
"""Set available favorites."""
- self._favorites = self.soco.music_library.get_sonos_favorites()
+ favorites = self.soco.music_library.get_sonos_favorites()
+ # Exclude favorites that are non-playable due to no linked resources
+ self._favorites = [f for f in favorites if f.reference.resources]
def _radio_artwork(self, url):
"""Return the private URL with artwork for a radio stream."""
@@ -390,30 +414,7 @@ def subscribe(service, action):
def update(self):
"""Retrieve latest state."""
- available = self._check_available()
- if self._available != available:
- self._available = available
- if available:
- self._set_basic_information()
- self._subscribe_to_player_events()
- else:
- for subscription in self._subscriptions:
- subscription.unsubscribe()
- self._subscriptions = []
-
- self._player_volume = None
- self._player_muted = None
- self._status = 'OFF'
- self._coordinator = None
- self._media_duration = None
- self._media_position = None
- self._media_position_updated_at = None
- self._media_image_url = None
- self._media_artist = None
- self._media_album_name = None
- self._media_title = None
- self._source_name = None
- elif available and not self._receives_events:
+ if self._available and not self._receives_events:
try:
self.update_groups()
self.update_volume()
@@ -433,6 +434,9 @@ def update_media(self, event=None):
self._shuffle = self.soco.shuffle
+ update_position = (new_status != self._status)
+ self._status = new_status
+
if self.soco.is_playing_tv:
self.update_media_linein(SOURCE_TV)
elif self.soco.is_playing_line_in:
@@ -444,11 +448,8 @@ def update_media(self, event=None):
variables = event and event.variables
self.update_media_radio(variables, track_info)
else:
- update_position = (new_status != self._status)
self.update_media_music(update_position, track_info)
- self._status = new_status
-
self.schedule_update_ha_state()
# Also update slaves
@@ -550,7 +551,9 @@ def update_media_music(self, update_media_position, track_info):
self._media_position is None
# position jumped?
- if rel_time is not None and self._media_position is not None:
+ if (self.state == STATE_PLAYING
+ and rel_time is not None
+ and self._media_position is not None):
time_diff = utcnow() - self._media_position_updated_at
time_diff = time_diff.total_seconds()
@@ -800,16 +803,6 @@ def source_list(self):
return sources
- @soco_error()
- def turn_on(self):
- """Turn the media player on."""
- self.media_play()
-
- @soco_error()
- def turn_off(self):
- """Turn off media player."""
- self.media_stop()
-
@soco_error(UPNP_ERRORS_TO_IGNORE)
@soco_coordinator
def media_play(self):
diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py
new file mode 100644
index 00000000000000..79c9cd94871bef
--- /dev/null
+++ b/homeassistant/components/ssdp/__init__.py
@@ -0,0 +1,175 @@
+"""The SSDP integration."""
+import asyncio
+from datetime import timedelta
+import logging
+from urllib.parse import urlparse
+from xml.etree import ElementTree
+
+import aiohttp
+from netdisco import ssdp, util
+
+from homeassistant.helpers.event import async_track_time_interval
+from homeassistant.generated.ssdp import SSDP
+
+DOMAIN = 'ssdp'
+SCAN_INTERVAL = timedelta(seconds=60)
+
+ATTR_HOST = 'host'
+ATTR_PORT = 'port'
+ATTR_SSDP_DESCRIPTION = 'ssdp_description'
+ATTR_ST = 'ssdp_st'
+ATTR_NAME = 'name'
+ATTR_MODEL_NAME = 'model_name'
+ATTR_MODEL_NUMBER = 'model_number'
+ATTR_SERIAL = 'serial_number'
+ATTR_MANUFACTURER = 'manufacturer'
+ATTR_MANUFACTURERURL = 'manufacturerURL'
+ATTR_UDN = 'udn'
+ATTR_UPNP_DEVICE_TYPE = 'upnp_device_type'
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup(hass, config):
+ """Set up the SSDP integration."""
+ async def initialize():
+ scanner = Scanner(hass)
+ await scanner.async_scan(None)
+ async_track_time_interval(hass, scanner.async_scan, SCAN_INTERVAL)
+
+ hass.loop.create_task(initialize())
+
+ return True
+
+
+class Scanner:
+ """Class to manage SSDP scanning."""
+
+ def __init__(self, hass):
+ """Initialize class."""
+ self.hass = hass
+ self.seen = set()
+ self._description_cache = {}
+
+ async def async_scan(self, _):
+ """Scan for new entries."""
+ _LOGGER.debug("Scanning")
+ # Run 3 times as packets can get lost
+ for _ in range(3):
+ entries = await self.hass.async_add_executor_job(ssdp.scan)
+ await self._process_entries(entries)
+
+ # We clear the cache after each run. We track discovered entries
+ # so will never need a description twice.
+ self._description_cache.clear()
+
+ async def _process_entries(self, entries):
+ """Process SSDP entries."""
+ tasks = []
+
+ for entry in entries:
+ key = (entry.st, entry.location)
+
+ if key in self.seen:
+ continue
+
+ self.seen.add(key)
+
+ tasks.append(self._process_entry(entry))
+
+ if not tasks:
+ return
+
+ to_load = [result for result in await asyncio.gather(*tasks)
+ if result is not None]
+
+ if not to_load:
+ return
+
+ tasks = []
+
+ for entry, info, domains in to_load:
+ for domain in domains:
+ _LOGGER.debug("Discovered %s at %s", domain, entry.location)
+ tasks.append(self.hass.config_entries.flow.async_init(
+ domain, context={'source': DOMAIN}, data=info
+ ))
+
+ await asyncio.wait(tasks)
+
+ async def _process_entry(self, entry):
+ """Process a single entry."""
+ domains = set(SSDP["st"].get(entry.st, []))
+
+ xml_location = entry.location
+
+ if not xml_location:
+ if domains:
+ return (entry, info_from_entry(entry, None), domains)
+ return None
+
+ # Multiple entries usally share same location. Make sure
+ # we fetch it only once.
+ info_req = self._description_cache.get(xml_location)
+
+ if info_req is None:
+ info_req = self._description_cache[xml_location] = \
+ self.hass.async_create_task(
+ self._fetch_description(xml_location))
+
+ info = await info_req
+
+ domains.update(SSDP["manufacturer"].get(info.get('manufacturer'), []))
+ domains.update(SSDP["device_type"].get(info.get('deviceType'), []))
+
+ if domains:
+ return (entry, info_from_entry(entry, info), domains)
+
+ return None
+
+ async def _fetch_description(self, xml_location):
+ """Fetch an XML description."""
+ session = self.hass.helpers.aiohttp_client.async_get_clientsession()
+ try:
+ resp = await session.get(xml_location, timeout=5)
+ xml = await resp.text()
+
+ # Samsung Smart TV sometimes returns an empty document the
+ # first time. Retry once.
+ if not xml:
+ resp = await session.get(xml_location, timeout=5)
+ xml = await resp.text()
+ except (aiohttp.ClientError, asyncio.TimeoutError) as err:
+ _LOGGER.debug("Error fetching %s: %s", xml_location, err)
+ return {}
+
+ try:
+ tree = ElementTree.fromstring(xml)
+ except ElementTree.ParseError as err:
+ _LOGGER.debug("Error parsing %s: %s", xml_location, err)
+ return {}
+
+ return util.etree_to_dict(tree).get('root', {}).get('device', {})
+
+
+def info_from_entry(entry, device_info):
+ """Get most important info from an entry."""
+ url = urlparse(entry.location)
+ info = {
+ ATTR_HOST: url.hostname,
+ ATTR_PORT: url.port,
+ ATTR_SSDP_DESCRIPTION: entry.location,
+ ATTR_ST: entry.st,
+ }
+
+ if device_info:
+ info[ATTR_NAME] = device_info.get('friendlyName')
+ info[ATTR_MODEL_NAME] = device_info.get('modelName')
+ info[ATTR_MODEL_NUMBER] = device_info.get('modelNumber')
+ info[ATTR_SERIAL] = device_info.get('serialNumber')
+ info[ATTR_MANUFACTURER] = device_info.get('manufacturer')
+ info[ATTR_MANUFACTURERURL] = device_info.get('manufacturerURL')
+ info[ATTR_UDN] = device_info.get('UDN')
+ info[ATTR_UPNP_DEVICE_TYPE] = device_info.get('deviceType')
+
+ return info
diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json
new file mode 100644
index 00000000000000..ce00bcbc888e5a
--- /dev/null
+++ b/homeassistant/components/ssdp/manifest.json
@@ -0,0 +1,12 @@
+{
+ "domain": "ssdp",
+ "name": "SSDP",
+ "documentation": "https://www.home-assistant.io/components/ssdp",
+ "requirements": [
+ "netdisco==2.6.0"
+ ],
+ "dependencies": [
+ ],
+ "codeowners": [
+ ]
+}
diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py
index 57dbb7134e207c..6330b14f7c41af 100644
--- a/homeassistant/components/synology_srm/device_tracker.py
+++ b/homeassistant/components/synology_srm/device_tracker.py
@@ -80,7 +80,7 @@ def _update_info(self):
"""Check the router for connected devices."""
_LOGGER.debug("Scanning for connected devices")
- devices = self.client.mesh.network_wifidevice()
+ devices = self.client.core.network_nsm_device({'is_online': True})
last_results = []
for device in devices:
diff --git a/homeassistant/components/synology_srm/manifest.json b/homeassistant/components/synology_srm/manifest.json
index fa89577f26ee7e..a790a6c453cd1d 100644
--- a/homeassistant/components/synology_srm/manifest.json
+++ b/homeassistant/components/synology_srm/manifest.json
@@ -1,9 +1,9 @@
{
"domain": "synology_srm",
- "name": "Synology srm",
+ "name": "Synology SRM",
"documentation": "https://www.home-assistant.io/components/synology_srm",
"requirements": [
- "synology-srm==0.0.6"
+ "synology-srm==0.0.7"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/tado/device_tracker.py b/homeassistant/components/tado/device_tracker.py
index 3bb62f328b9e29..31b424b9cd4676 100644
--- a/homeassistant/components/tado/device_tracker.py
+++ b/homeassistant/components/tado/device_tracker.py
@@ -42,6 +42,7 @@ class TadoDeviceScanner(DeviceScanner):
def __init__(self, hass, config):
"""Initialize the scanner."""
+ self.hass = hass
self.last_results = []
self.username = config[CONF_USERNAME]
@@ -60,8 +61,7 @@ def __init__(self, hass, config):
# The API URL always needs a username and password
self.tadoapiurl += '?username={username}&password={password}'
- self.websession = async_create_clientsession(
- hass, cookie_jar=aiohttp.CookieJar(unsafe=True))
+ self.websession = None
self.success_init = asyncio.run_coroutine_threadsafe(
self._async_update_info(), hass.loop
@@ -92,6 +92,10 @@ async def _async_update_info(self):
"""
_LOGGER.debug("Requesting Tado")
+ if self.websession is None:
+ self.websession = async_create_clientsession(
+ self.hass, cookie_jar=aiohttp.CookieJar(unsafe=True))
+
last_results = []
try:
diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json
index 6562fa49cde0b5..922362de1d9ccb 100644
--- a/homeassistant/components/tibber/manifest.json
+++ b/homeassistant/components/tibber/manifest.json
@@ -3,7 +3,7 @@
"name": "Tibber",
"documentation": "https://www.home-assistant.io/components/tibber",
"requirements": [
- "pyTibber==0.10.3"
+ "pyTibber==0.11.5"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/toon/.translations/hu.json b/homeassistant/components/toon/.translations/hu.json
new file mode 100644
index 00000000000000..740e4bd381da5e
--- /dev/null
+++ b/homeassistant/components/toon/.translations/hu.json
@@ -0,0 +1,12 @@
+{
+ "config": {
+ "step": {
+ "authenticate": {
+ "data": {
+ "password": "Jelsz\u00f3",
+ "username": "Felhaszn\u00e1l\u00f3n\u00e9v"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py
index 848202d6ce1445..6d4c7a9671a87e 100644
--- a/homeassistant/components/totalconnect/alarm_control_panel.py
+++ b/homeassistant/components/totalconnect/alarm_control_panel.py
@@ -9,9 +9,8 @@
from homeassistant.const import (
CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED,
- STATE_ALARM_ARMING, STATE_ALARM_DISARMING, CONF_NAME,
- STATE_ALARM_ARMED_CUSTOM_BYPASS)
-
+ STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_ALARM_TRIGGERED,
+ CONF_NAME, STATE_ALARM_ARMED_CUSTOM_BYPASS)
_LOGGER = logging.getLogger(__name__)
@@ -46,6 +45,7 @@ def __init__(self, name, username, password):
self._username = username
self._password = password
self._state = None
+ self._device_state_attributes = {}
self._client = TotalConnectClient.TotalConnectClient(
username, password)
@@ -59,9 +59,15 @@ def state(self):
"""Return the state of the device."""
return self._state
+ @property
+ def device_state_attributes(self):
+ """Return the state attributes of the device."""
+ return self._device_state_attributes
+
def update(self):
"""Return the state of the device."""
status = self._client.get_armed_status()
+ attr = {'triggered_source': None, 'triggered_zone': None}
if status == self._client.DISARMED:
state = STATE_ALARM_DISARMED
@@ -77,10 +83,22 @@ def update(self):
state = STATE_ALARM_ARMING
elif status == self._client.DISARMING:
state = STATE_ALARM_DISARMING
+ elif status == self._client.ALARMING:
+ state = STATE_ALARM_TRIGGERED
+ attr['triggered_source'] = 'Police/Medical'
+ elif status == self._client.ALARMING_FIRE_SMOKE:
+ state = STATE_ALARM_TRIGGERED
+ attr['triggered_source'] = 'Fire/Smoke'
+ elif status == self._client.ALARMING_CARBON_MONOXIDE:
+ state = STATE_ALARM_TRIGGERED
+ attr['triggered_source'] = 'Carbon Monoxide'
else:
+ logging.info("Total Connect Client returned unknown "
+ "status code: %s", status)
state = None
self._state = state
+ self._device_state_attributes = attr
def alarm_disarm(self, code=None):
"""Send disarm command."""
diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json
index adb60599ae533c..3ff3b5c5b46436 100644
--- a/homeassistant/components/totalconnect/manifest.json
+++ b/homeassistant/components/totalconnect/manifest.json
@@ -3,7 +3,7 @@
"name": "Totalconnect",
"documentation": "https://www.home-assistant.io/components/totalconnect",
"requirements": [
- "total_connect_client==0.25"
+ "total_connect_client==0.27"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py
index 4173c1aaa60432..794cc6867b96c4 100644
--- a/homeassistant/components/tplink/__init__.py
+++ b/homeassistant/components/tplink/__init__.py
@@ -6,28 +6,43 @@
from homeassistant.const import CONF_HOST
from homeassistant import config_entries
import homeassistant.helpers.config_validation as cv
-from .config_flow import async_get_devices
-from .const import DOMAIN
+from homeassistant.helpers.typing import ConfigType, HomeAssistantType
+
+from .common import (
+ async_discover_devices,
+ get_static_devices,
+ ATTR_CONFIG,
+ CONF_DIMMER,
+ CONF_DISCOVERY,
+ CONF_LIGHT,
+ CONF_SWITCH,
+ SmartDevices
+)
_LOGGER = logging.getLogger(__name__)
+DOMAIN = 'tplink'
+
TPLINK_HOST_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string
})
-CONF_LIGHT = 'light'
-CONF_SWITCH = 'switch'
-CONF_DISCOVERY = 'discovery'
-
-ATTR_CONFIG = 'config'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
- vol.Optional('light', default=[]): vol.All(cv.ensure_list,
- [TPLINK_HOST_SCHEMA]),
- vol.Optional('switch', default=[]): vol.All(cv.ensure_list,
- [TPLINK_HOST_SCHEMA]),
- vol.Optional('discovery', default=True): cv.boolean,
+ vol.Optional(CONF_LIGHT, default=[]): vol.All(
+ cv.ensure_list,
+ [TPLINK_HOST_SCHEMA]
+ ),
+ vol.Optional(CONF_SWITCH, default=[]): vol.All(
+ cv.ensure_list,
+ [TPLINK_HOST_SCHEMA]
+ ),
+ vol.Optional(CONF_DIMMER, default=[]): vol.All(
+ cv.ensure_list,
+ [TPLINK_HOST_SCHEMA]
+ ),
+ vol.Optional(CONF_DISCOVERY, default=True): cv.boolean,
}),
}, extra=vol.ALLOW_EXTRA)
@@ -46,76 +61,45 @@ async def async_setup(hass, config):
return True
-async def async_setup_entry(hass, config_entry):
+async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigType):
"""Set up TPLink from a config entry."""
- from pyHS100 import SmartBulb, SmartPlug, SmartDeviceException
-
- devices = {}
-
config_data = hass.data[DOMAIN].get(ATTR_CONFIG)
# These will contain the initialized devices
lights = hass.data[DOMAIN][CONF_LIGHT] = []
switches = hass.data[DOMAIN][CONF_SWITCH] = []
- # If discovery is defined and not disabled, discover devices
- # If initialized from configure integrations, there's no config
- # so we default here to True
- if config_data is None or config_data[CONF_DISCOVERY]:
- devs = await async_get_devices(hass)
- _LOGGER.info("Discovered %s TP-Link smart home device(s)", len(devs))
- devices.update(devs)
+ # Add static devices
+ static_devices = SmartDevices()
+ if config_data is not None:
+ static_devices = get_static_devices(
+ config_data,
+ )
- def _device_for_type(host, type_):
- dev = None
- if type_ == CONF_LIGHT:
- dev = SmartBulb(host)
- elif type_ == CONF_SWITCH:
- dev = SmartPlug(host)
+ lights.extend(static_devices.lights)
+ switches.extend(static_devices.switches)
- return dev
+ # Add discovered devices
+ if config_data is None or config_data[CONF_DISCOVERY]:
+ discovered_devices = await async_discover_devices(hass, static_devices)
- # When arriving from configure integrations, we have no config data.
- if config_data is not None:
- for type_ in [CONF_LIGHT, CONF_SWITCH]:
- for entry in config_data[type_]:
- try:
- host = entry['host']
- dev = _device_for_type(host, type_)
- devices[host] = dev
- _LOGGER.debug("Succesfully added %s %s: %s",
- type_, host, dev)
- except SmartDeviceException as ex:
- _LOGGER.error("Unable to initialize %s %s: %s",
- type_, host, ex)
-
- # This is necessary to avoid I/O blocking on is_dimmable
- def _fill_device_lists():
- for dev in devices.values():
- if isinstance(dev, SmartPlug):
- try:
- if dev.is_dimmable: # Dimmers act as lights
- lights.append(dev)
- else:
- switches.append(dev)
- except SmartDeviceException as ex:
- _LOGGER.error("Unable to connect to device %s: %s",
- dev.host, ex)
-
- elif isinstance(dev, SmartBulb):
- lights.append(dev)
- else:
- _LOGGER.error("Unknown smart device type: %s", type(dev))
-
- # Avoid blocking on is_dimmable
- await hass.async_add_executor_job(_fill_device_lists)
+ lights.extend(discovered_devices.lights)
+ switches.extend(discovered_devices.switches)
forward_setup = hass.config_entries.async_forward_entry_setup
if lights:
- _LOGGER.debug("Got %s lights: %s", len(lights), lights)
+ _LOGGER.debug(
+ "Got %s lights: %s",
+ len(lights),
+ ", ".join([d.host for d in lights])
+ )
hass.async_create_task(forward_setup(config_entry, 'light'))
if switches:
- _LOGGER.debug("Got %s switches: %s", len(switches), switches)
+ _LOGGER.debug(
+ "Got %s switches: %s",
+ len(switches),
+ ", ".join([d.host for d in switches])
+ )
hass.async_create_task(forward_setup(config_entry, 'switch'))
return True
diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py
new file mode 100644
index 00000000000000..d97ba36afb41b7
--- /dev/null
+++ b/homeassistant/components/tplink/common.py
@@ -0,0 +1,202 @@
+"""Common code for tplink."""
+import asyncio
+import logging
+from datetime import timedelta
+from typing import Any, Callable, List
+
+from pyHS100 import (
+ SmartBulb,
+ SmartDevice,
+ SmartPlug,
+ SmartDeviceException
+)
+
+from homeassistant.helpers.typing import HomeAssistantType
+
+_LOGGER = logging.getLogger(__name__)
+
+
+ATTR_CONFIG = 'config'
+CONF_DIMMER = 'dimmer'
+CONF_DISCOVERY = 'discovery'
+CONF_LIGHT = 'light'
+CONF_SWITCH = 'switch'
+
+
+class SmartDevices:
+ """Hold different kinds of devices."""
+
+ def __init__(
+ self,
+ lights: List[SmartDevice] = None,
+ switches: List[SmartDevice] = None
+ ):
+ """Constructor."""
+ self._lights = lights or []
+ self._switches = switches or []
+
+ @property
+ def lights(self):
+ """Get the lights."""
+ return self._lights
+
+ @property
+ def switches(self):
+ """Get the switches."""
+ return self._switches
+
+ def has_device_with_host(self, host):
+ """Check if a devices exists with a specific host."""
+ for device in self.lights + self.switches:
+ if device.host == host:
+ return True
+
+ return False
+
+
+async def async_get_discoverable_devices(hass):
+ """Return if there are devices that can be discovered."""
+ from pyHS100 import Discover
+
+ def discover():
+ devs = Discover.discover()
+ return devs
+ return await hass.async_add_executor_job(discover)
+
+
+async def async_discover_devices(
+ hass: HomeAssistantType,
+ existing_devices: SmartDevices
+) -> SmartDevices:
+ """Get devices through discovery."""
+ _LOGGER.debug("Discovering devices")
+ devices = await async_get_discoverable_devices(hass)
+ _LOGGER.info(
+ "Discovered %s TP-Link smart home device(s)",
+ len(devices)
+ )
+
+ lights = []
+ switches = []
+
+ def process_devices():
+ for dev in devices.values():
+ # If this device already exists, ignore dynamic setup.
+ if existing_devices.has_device_with_host(dev.host):
+ continue
+
+ if isinstance(dev, SmartPlug):
+ try:
+ if dev.is_dimmable: # Dimmers act as lights
+ lights.append(dev)
+ else:
+ switches.append(dev)
+ except SmartDeviceException as ex:
+ _LOGGER.error("Unable to connect to device %s: %s",
+ dev.host, ex)
+
+ elif isinstance(dev, SmartBulb):
+ lights.append(dev)
+ else:
+ _LOGGER.error("Unknown smart device type: %s", type(dev))
+
+ await hass.async_add_executor_job(process_devices)
+
+ return SmartDevices(lights, switches)
+
+
+def get_static_devices(config_data) -> SmartDevices:
+ """Get statically defined devices in the config."""
+ _LOGGER.debug("Getting static devices")
+ lights = []
+ switches = []
+
+ for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_DIMMER]:
+ for entry in config_data[type_]:
+ host = entry['host']
+
+ if type_ == CONF_LIGHT:
+ lights.append(SmartBulb(host))
+ elif type_ == CONF_SWITCH:
+ switches.append(SmartPlug(host))
+ # Dimmers need to be defined as smart plugs to work correctly.
+ elif type_ == CONF_DIMMER:
+ lights.append(SmartPlug(host))
+
+ return SmartDevices(
+ lights,
+ switches
+ )
+
+
+async def async_add_entities_retry(
+ hass: HomeAssistantType,
+ async_add_entities: Callable[[List[Any], bool], None],
+ objects: List[Any],
+ callback: Callable[[Any, Callable], None],
+ interval: timedelta = timedelta(seconds=60)
+):
+ """
+ Add entities now and retry later if issues are encountered.
+
+ If the callback throws an exception or returns false, that
+ object will try again a while later.
+ This is useful for devices that are not online when hass starts.
+ :param hass:
+ :param async_add_entities: The callback provided to a
+ platform's async_setup.
+ :param objects: The objects to create as entities.
+ :param callback: The callback that will perform the add.
+ :param interval: THe time between attempts to add.
+ :return: A callback to cancel the retries.
+ """
+ add_objects = objects.copy()
+
+ is_cancelled = False
+
+ def cancel_interval_callback():
+ nonlocal is_cancelled
+ is_cancelled = True
+
+ async def process_objects_loop(delay: int):
+ if is_cancelled:
+ return
+
+ await process_objects()
+
+ if not add_objects:
+ return
+
+ await asyncio.sleep(delay)
+
+ hass.async_create_task(process_objects_loop(delay))
+
+ async def process_objects(*args):
+ # Process each object.
+ for add_object in list(add_objects):
+ # Call the individual item callback.
+ try:
+ _LOGGER.debug(
+ "Attempting to add object of type %s",
+ type(add_object)
+ )
+ result = await hass.async_add_job(
+ callback,
+ add_object,
+ async_add_entities
+ )
+ except SmartDeviceException as ex:
+ _LOGGER.debug(
+ str(ex)
+ )
+ result = False
+
+ if result is True or result is None:
+ _LOGGER.debug("Added object.")
+ add_objects.remove(add_object)
+ else:
+ _LOGGER.debug("Failed to add object, will try again later")
+
+ await process_objects_loop(interval.seconds)
+
+ return cancel_interval_callback
diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py
index 86b1acf4ff119c..8a058be98ed5da 100644
--- a/homeassistant/components/tplink/config_flow.py
+++ b/homeassistant/components/tplink/config_flow.py
@@ -2,19 +2,10 @@
from homeassistant.helpers import config_entry_flow
from homeassistant import config_entries
from .const import DOMAIN
-
-
-async def async_get_devices(hass):
- """Return if there are devices that can be discovered."""
- from pyHS100 import Discover
-
- def discover():
- devs = Discover.discover()
- return devs
- return await hass.async_add_executor_job(discover)
+from .common import async_get_discoverable_devices
config_entry_flow.register_discovery_flow(DOMAIN,
'TP-Link Smart Home',
- async_get_devices,
+ async_get_discoverable_devices,
config_entries.CONN_CLASS_LOCAL_POLL)
diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py
index 7b665006a44cf7..b139aed4eea0eb 100644
--- a/homeassistant/components/tplink/device_tracker.py
+++ b/homeassistant/components/tplink/device_tracker.py
@@ -41,6 +41,12 @@ def get_scanner(hass, config):
should be gradually migrated in the pypi package
"""
+ _LOGGER.warning("TP-Link device tracker is unmaintained and will be "
+ "removed in the future releases if no maintainer is "
+ "found. If you have interest in this integration, "
+ "feel free to create a pull request to move this code "
+ "to a new 'tplink_router' integration and refactoring "
+ "the device-specific parts to the tplink library")
for cls in [
TplinkDeviceScanner, Tplink5DeviceScanner, Tplink4DeviceScanner,
Tplink3DeviceScanner, Tplink2DeviceScanner, Tplink1DeviceScanner
diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py
index dc2fcce949a5ad..99241e2e9f0e3b 100644
--- a/homeassistant/components/tplink/light.py
+++ b/homeassistant/components/tplink/light.py
@@ -2,15 +2,19 @@
import logging
import time
+from pyHS100 import SmartBulb, SmartDeviceException
+
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, SUPPORT_BRIGHTNESS,
SUPPORT_COLOR, SUPPORT_COLOR_TEMP, Light)
import homeassistant.helpers.device_registry as dr
+from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.util.color import (
color_temperature_kelvin_to_mired as kelvin_to_mired,
color_temperature_mired_to_kelvin as mired_to_kelvin)
from . import CONF_LIGHT, DOMAIN as TPLINK_DOMAIN
+from .common import async_add_entities_retry
PARALLEL_UPDATES = 0
@@ -31,17 +35,35 @@ async def async_setup_platform(hass, config, add_entities,
'convert to use the tplink component.')
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up discovered switches."""
- devs = []
- for dev in hass.data[TPLINK_DOMAIN][CONF_LIGHT]:
- devs.append(TPLinkSmartBulb(dev))
-
- async_add_entities(devs, True)
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry,
+ async_add_entities
+):
+ """Set up switches."""
+ await async_add_entities_retry(
+ hass,
+ async_add_entities,
+ hass.data[TPLINK_DOMAIN][CONF_LIGHT],
+ add_entity
+ )
return True
+def add_entity(device: SmartBulb, async_add_entities):
+ """Check if device is online and add the entity."""
+ # Attempt to get the sysinfo. If it fails, it will raise an
+ # exception that is caught by async_add_entities_retry which
+ # will try again later.
+ device.get_sysinfo()
+
+ async_add_entities(
+ [TPLinkSmartBulb(device)],
+ update_before_add=True
+ )
+
+
def brightness_to_percentage(byt):
"""Convert brightness from absolute 0..255 to percentage."""
return int((byt*100.0)/255.0)
@@ -55,7 +77,7 @@ def brightness_from_percentage(percent):
class TPLinkSmartBulb(Light):
"""Representation of a TPLink Smart Bulb."""
- def __init__(self, smartbulb) -> None:
+ def __init__(self, smartbulb: SmartBulb) -> None:
"""Initialize the bulb."""
self.smartbulb = smartbulb
self._sysinfo = None
@@ -69,25 +91,29 @@ def __init__(self, smartbulb) -> None:
self._max_mireds = None
self._emeter_params = {}
+ self._mac = None
+ self._alias = None
+ self._model = None
+
@property
def unique_id(self):
"""Return a unique ID."""
- return self._sysinfo["mac"]
+ return self._mac
@property
def name(self):
"""Return the name of the Smart Bulb."""
- return self._sysinfo["alias"]
+ return self._alias
@property
def device_info(self):
"""Return information about the device."""
return {
- "name": self.name,
- "model": self._sysinfo["model"],
+ "name": self._alias,
+ "model": self._model,
"manufacturer": 'TP-Link',
"connections": {
- (dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"])
+ (dr.CONNECTION_NETWORK_MAC, self._mac)
},
"sw_version": self._sysinfo["sw_ver"],
}
@@ -104,7 +130,6 @@ def device_state_attributes(self):
def turn_on(self, **kwargs):
"""Turn the light on."""
- from pyHS100 import SmartBulb
self.smartbulb.state = SmartBulb.BULB_STATE_ON
if ATTR_COLOR_TEMP in kwargs:
@@ -122,7 +147,6 @@ def turn_on(self, **kwargs):
def turn_off(self, **kwargs):
"""Turn the light off."""
- from pyHS100 import SmartBulb
self.smartbulb.state = SmartBulb.BULB_STATE_OFF
@property
@@ -157,7 +181,6 @@ def is_on(self):
def update(self):
"""Update the TP-Link Bulb's state."""
- from pyHS100 import SmartDeviceException, SmartBulb
try:
if self._supported_features is None:
self.get_features()
@@ -212,6 +235,9 @@ def get_features(self):
"""Determine all supported features in one go."""
self._sysinfo = self.smartbulb.sys_info
self._supported_features = 0
+ self._mac = self.smartbulb.mac
+ self._alias = self.smartbulb.alias
+ self._model = self.smartbulb.model
if self.smartbulb.is_dimmable:
self._supported_features += SUPPORT_BRIGHTNESS
diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py
index a3d680a0a50186..d09df73fe863e7 100644
--- a/homeassistant/components/tplink/switch.py
+++ b/homeassistant/components/tplink/switch.py
@@ -2,12 +2,16 @@
import logging
import time
+from pyHS100 import SmartDeviceException, SmartPlug
+
from homeassistant.components.switch import (
ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH, SwitchDevice)
from homeassistant.const import ATTR_VOLTAGE
import homeassistant.helpers.device_registry as dr
+from homeassistant.helpers.typing import HomeAssistantType
from . import CONF_SWITCH, DOMAIN as TPLINK_DOMAIN
+from .common import async_add_entities_retry
PARALLEL_UPDATES = 0
@@ -27,13 +31,31 @@ async def async_setup_platform(hass, config, add_entities,
'convert to use the tplink component.')
-async def async_setup_entry(hass, config_entry, async_add_entities):
- """Set up discovered switches."""
- devs = []
- for dev in hass.data[TPLINK_DOMAIN][CONF_SWITCH]:
- devs.append(SmartPlugSwitch(dev))
-
- async_add_entities(devs, True)
+def add_entity(device: SmartPlug, async_add_entities):
+ """Check if device is online and add the entity."""
+ # Attempt to get the sysinfo. If it fails, it will raise an
+ # exception that is caught by async_add_entities_retry which
+ # will try again later.
+ device.get_sysinfo()
+
+ async_add_entities(
+ [SmartPlugSwitch(device)],
+ update_before_add=True
+ )
+
+
+async def async_setup_entry(
+ hass: HomeAssistantType,
+ config_entry,
+ async_add_entities
+):
+ """Set up switches."""
+ await async_add_entities_retry(
+ hass,
+ async_add_entities,
+ hass.data[TPLINK_DOMAIN][CONF_SWITCH],
+ add_entity
+ )
return True
@@ -41,7 +63,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class SmartPlugSwitch(SwitchDevice):
"""Representation of a TPLink Smart Plug switch."""
- def __init__(self, smartplug):
+ def __init__(self, smartplug: SmartPlug):
"""Initialize the switch."""
self.smartplug = smartplug
self._sysinfo = None
@@ -50,25 +72,29 @@ def __init__(self, smartplug):
# Set up emeter cache
self._emeter_params = {}
+ self._mac = None
+ self._alias = None
+ self._model = None
+
@property
def unique_id(self):
"""Return a unique ID."""
- return self._sysinfo["mac"]
+ return self._mac
@property
def name(self):
"""Return the name of the Smart Plug."""
- return self._sysinfo["alias"]
+ return self._alias
@property
def device_info(self):
"""Return information about the device."""
return {
- "name": self.name,
- "model": self._sysinfo["model"],
+ "name": self._alias,
+ "model": self._model,
"manufacturer": 'TP-Link',
"connections": {
- (dr.CONNECTION_NETWORK_MAC, self._sysinfo["mac"])
+ (dr.CONNECTION_NETWORK_MAC, self._mac)
},
"sw_version": self._sysinfo["sw_ver"],
}
@@ -98,10 +124,12 @@ def device_state_attributes(self):
def update(self):
"""Update the TP-Link switch's state."""
- from pyHS100 import SmartDeviceException
try:
if not self._sysinfo:
self._sysinfo = self.smartplug.sys_info
+ self._mac = self.smartplug.mac
+ self._alias = self.smartplug.alias
+ self._model = self.smartplug.model
self._state = self.smartplug.state == \
self.smartplug.SWITCH_STATE_ON
diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py
index bbbe104f296910..76f6a8f5764c92 100644
--- a/homeassistant/components/tradfri/config_flow.py
+++ b/homeassistant/components/tradfri/config_flow.py
@@ -76,8 +76,8 @@ async def async_step_auth(self, user_input=None):
errors=errors,
)
- async def async_step_discovery(self, user_input):
- """Handle discovery."""
+ async def async_step_zeroconf(self, user_input):
+ """Handle zeroconf discovery."""
for entry in self._async_current_entries():
if entry.data[CONF_HOST] == user_input['host']:
return self.async_abort(
diff --git a/homeassistant/components/tradfri/manifest.json b/homeassistant/components/tradfri/manifest.json
index c9a4fca3dc92e8..aba3805a4aaf5c 100644
--- a/homeassistant/components/tradfri/manifest.json
+++ b/homeassistant/components/tradfri/manifest.json
@@ -7,6 +7,7 @@
"pytradfri[async]==6.0.1"
],
"dependencies": [],
+ "zeroconf": ["_coap._udp.local."],
"codeowners": [
"@ggravlingen"
]
diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json
index c061ab36e7bd55..f4d86300acaed2 100644
--- a/homeassistant/components/unifi/.translations/ru.json
+++ b/homeassistant/components/unifi/.translations/ru.json
@@ -15,7 +15,7 @@
"password": "\u041f\u0430\u0440\u043e\u043b\u044c",
"port": "\u041f\u043e\u0440\u0442",
"site": "ID \u0441\u0430\u0439\u0442\u0430",
- "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f",
+ "username": "\u041b\u043e\u0433\u0438\u043d",
"verify_ssl": "\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442"
},
"title": "UniFi Controller"
diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py
index b784aaa705ad9d..95af83767736b0 100644
--- a/homeassistant/components/unifi/config_flow.py
+++ b/homeassistant/components/unifi/config_flow.py
@@ -84,6 +84,7 @@ async def async_step_site(self, user_input=None):
try:
desc = user_input.get(CONF_SITE_ID, self.desc)
+ print(self.sites)
for site in self.sites.values():
if desc == site['desc']:
if site['role'] != 'admin':
diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py
index 2b9aa89fef24a7..5105e33f1d6f23 100644
--- a/homeassistant/components/unifi/controller.py
+++ b/homeassistant/components/unifi/controller.py
@@ -1,5 +1,6 @@
"""UniFi Controller abstraction."""
import asyncio
+import ssl
import async_timeout
from aiohttp import CookieJar
@@ -81,15 +82,19 @@ async def get_controller(
"""Create a controller object and verify authentication."""
import aiounifi
+ sslcontext = None
+
if verify_ssl:
session = aiohttp_client.async_get_clientsession(hass)
+ if isinstance(verify_ssl, str):
+ sslcontext = ssl.create_default_context(cafile=verify_ssl)
else:
session = aiohttp_client.async_create_clientsession(
hass, verify_ssl=verify_ssl, cookie_jar=CookieJar(unsafe=True))
controller = aiounifi.Controller(
host, username=username, password=password, port=port, site=site,
- websession=session
+ websession=session, sslcontext=sslcontext
)
try:
diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py
index 8bf384eef14f72..30754273254a46 100644
--- a/homeassistant/components/unifi/device_tracker.py
+++ b/homeassistant/components/unifi/device_tracker.py
@@ -1,8 +1,13 @@
"""Support for Unifi WAP controllers."""
+import asyncio
import logging
from datetime import timedelta
import voluptuous as vol
+import async_timeout
+
+import aiounifi
+
import homeassistant.helpers.config_validation as cv
from homeassistant.components.device_tracker import (
DOMAIN, PLATFORM_SCHEMA, DeviceScanner)
@@ -10,6 +15,9 @@
from homeassistant.const import CONF_VERIFY_SSL, CONF_MONITORED_CONDITIONS
import homeassistant.util.dt as dt_util
+from .controller import get_controller
+from .errors import AuthenticationRequired, CannotConnect
+
_LOGGER = logging.getLogger(__name__)
CONF_PORT = 'port'
CONF_SITE_ID = 'site_id'
@@ -54,10 +62,8 @@
})
-def get_scanner(hass, config):
+async def async_get_scanner(hass, config):
"""Set up the Unifi device_tracker."""
- from pyunifi.controller import Controller, APIError
-
host = config[DOMAIN].get(CONF_HOST)
username = config[DOMAIN].get(CONF_USERNAME)
password = config[DOMAIN].get(CONF_PASSWORD)
@@ -69,9 +75,11 @@ def get_scanner(hass, config):
ssid_filter = config[DOMAIN].get(CONF_SSID_FILTER)
try:
- ctrl = Controller(host, username, password, port, version='v4',
- site_id=site_id, ssl_verify=verify_ssl)
- except APIError as ex:
+ controller = await get_controller(
+ hass, host, username, password, port, site_id, verify_ssl)
+ await controller.initialize()
+
+ except (AuthenticationRequired, CannotConnect) as ex:
_LOGGER.error("Failed to connect to Unifi: %s", ex)
hass.components.persistent_notification.create(
'Failed to connect to Unifi. '
@@ -82,8 +90,8 @@ def get_scanner(hass, config):
notification_id=NOTIFICATION_ID)
return False
- return UnifiScanner(ctrl, detection_time, ssid_filter,
- monitored_conditions)
+ return UnifiScanner(
+ controller, detection_time, ssid_filter, monitored_conditions)
class UnifiScanner(DeviceScanner):
@@ -92,36 +100,45 @@ class UnifiScanner(DeviceScanner):
def __init__(self, controller, detection_time: timedelta,
ssid_filter, monitored_conditions) -> None:
"""Initialize the scanner."""
+ self.controller = controller
self._detection_time = detection_time
- self._controller = controller
self._ssid_filter = ssid_filter
self._monitored_conditions = monitored_conditions
- self._update()
+ self._clients = {}
- def _update(self):
+ async def async_update(self):
"""Get the clients from the device."""
- from pyunifi.controller import APIError
try:
- clients = self._controller.get_clients()
- except APIError as ex:
- _LOGGER.error("Failed to scan clients: %s", ex)
+ await self.controller.clients.update()
+ clients = self.controller.clients.values()
+
+ except aiounifi.LoginRequired:
+ try:
+ with async_timeout.timeout(5):
+ await self.controller.login()
+ except (asyncio.TimeoutError, aiounifi.AiounifiException):
+ clients = []
+
+ except aiounifi.AiounifiException:
clients = []
# Filter clients to provided SSID list
if self._ssid_filter:
- clients = [client for client in clients
- if 'essid' in client and
- client['essid'] in self._ssid_filter]
+ clients = [
+ client for client in clients
+ if client.essid in self._ssid_filter
+ ]
self._clients = {
- client['mac']: client
+ client.raw['mac']: client.raw
for client in clients
if (dt_util.utcnow() - dt_util.utc_from_timestamp(float(
- client['last_seen']))) < self._detection_time}
+ client.last_seen))) < self._detection_time
+ }
- def scan_devices(self):
+ async def async_scan_devices(self):
"""Scan for devices."""
- self._update()
+ await self.async_update()
return self._clients.keys()
def get_device_name(self, device):
diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json
index 22ece5addafb6a..64119bae2fecb5 100644
--- a/homeassistant/components/unifi/manifest.json
+++ b/homeassistant/components/unifi/manifest.json
@@ -4,8 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/unifi",
"requirements": [
- "aiounifi==4",
- "pyunifi==2.16"
+ "aiounifi==6"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json
index ff32000d5f0729..c432a2695ff568 100644
--- a/homeassistant/components/velbus/manifest.json
+++ b/homeassistant/components/velbus/manifest.json
@@ -3,7 +3,7 @@
"name": "Velbus",
"documentation": "https://www.home-assistant.io/components/velbus",
"requirements": [
- "python-velbus==2.0.24"
+ "python-velbus==2.0.26"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py
index 3c1b6ecb1eb0ff..68e25f7a61fe9c 100644
--- a/homeassistant/components/velux/cover.py
+++ b/homeassistant/components/velux/cover.py
@@ -60,7 +60,16 @@ def current_cover_position(self):
@property
def device_class(self):
- """Define this cover as a window."""
+ """Define this cover as either window/blind/awning/shutter."""
+ from pyvlx.opening_device import Blind, RollerShutter, Window, Awning
+ if isinstance(self.node, Window):
+ return 'window'
+ if isinstance(self.node, Blind):
+ return 'blind'
+ if isinstance(self.node, RollerShutter):
+ return 'shutter'
+ if isinstance(self.node, Awning):
+ return 'awning'
return 'window'
@property
diff --git a/homeassistant/components/vera/manifest.json b/homeassistant/components/vera/manifest.json
index 7b475c437c3cab..99492753edb960 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.2.45"
+ "pyvera==0.3.1"
],
"dependencies": [],
"codeowners": []
diff --git a/homeassistant/components/webostv/media_player.py b/homeassistant/components/webostv/media_player.py
index fa62e29f233be8..4e7f07af857627 100644
--- a/homeassistant/components/webostv/media_player.py
+++ b/homeassistant/components/webostv/media_player.py
@@ -167,6 +167,7 @@ def __init__(self, host, name, customize, config, timeout,
self._source_list = {}
self._app_list = {}
self._channel = None
+ self._last_icon = None
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update(self):
@@ -271,6 +272,13 @@ def media_image_url(self):
icon = self._app_list[self._current_source_id]['largeIcon']
if not icon.startswith('http'):
icon = self._app_list[self._current_source_id]['icon']
+
+ # 'icon' holds a URL with a transient key. Avoid unnecessary
+ # updates by returning the same URL until the image changes.
+ if self._last_icon and \
+ (icon.split('/')[-1] == self._last_icon.split('/')[-1]):
+ return self._last_icon
+ self._last_icon = icon
return icon
return None
diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py
index f1849fda53914d..887573f4abb523 100644
--- a/homeassistant/components/websocket_api/permissions.py
+++ b/homeassistant/components/websocket_api/permissions.py
@@ -10,14 +10,17 @@
EVENT_THEMES_UPDATED)
from homeassistant.components.persistent_notification import (
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED)
+from homeassistant.components.lovelace import EVENT_LOVELACE_UPDATED
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
+from homeassistant.components.frontend import EVENT_PANELS_UPDATED
# These are events that do not contain any sensitive data
# Except for state_changed, which is handled accordingly.
SUBSCRIBE_WHITELIST = {
EVENT_COMPONENT_LOADED,
+ EVENT_PANELS_UPDATED,
EVENT_PERSISTENT_NOTIFICATIONS_UPDATED,
EVENT_SERVICE_REGISTERED,
EVENT_SERVICE_REMOVED,
@@ -26,4 +29,5 @@
EVENT_AREA_REGISTRY_UPDATED,
EVENT_DEVICE_REGISTRY_UPDATED,
EVENT_ENTITY_REGISTRY_UPDATED,
+ EVENT_LOVELACE_UPDATED,
}
diff --git a/homeassistant/components/wemo/.translations/ca.json b/homeassistant/components/wemo/.translations/ca.json
new file mode 100644
index 00000000000000..62db7fa3eb83da
--- /dev/null
+++ b/homeassistant/components/wemo/.translations/ca.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No s'han trobat dispositius Wemo a la xarxa.",
+ "single_instance_allowed": "Nom\u00e9s \u00e9s possible una \u00fanica configuraci\u00f3 de Wemo."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vols configurar Wemo?",
+ "title": "Wemo"
+ }
+ },
+ "title": "Wemo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/.translations/en.json b/homeassistant/components/wemo/.translations/en.json
new file mode 100644
index 00000000000000..a3751b7f5d6345
--- /dev/null
+++ b/homeassistant/components/wemo/.translations/en.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "No Wemo devices found on the network.",
+ "single_instance_allowed": "Only a single configuration of Wemo is possible."
+ },
+ "step": {
+ "confirm": {
+ "description": "Do you want to set up Wemo?",
+ "title": "Wemo"
+ }
+ },
+ "title": "Wemo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/.translations/no.json b/homeassistant/components/wemo/.translations/no.json
new file mode 100644
index 00000000000000..917eb0ef3a9d76
--- /dev/null
+++ b/homeassistant/components/wemo/.translations/no.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Ingen Sonos enheter funnet p\u00e5 nettverket.",
+ "single_instance_allowed": "Kun en enkelt konfigurasjon av Wemo er mulig."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u00d8nsker du \u00e5 sette opp Wemo?",
+ "title": "Wemo"
+ }
+ },
+ "title": "Wemo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/.translations/pt-BR.json b/homeassistant/components/wemo/.translations/pt-BR.json
new file mode 100644
index 00000000000000..b64fab85f78fe5
--- /dev/null
+++ b/homeassistant/components/wemo/.translations/pt-BR.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Nenhum dispositivo Wemo encontrado na rede.",
+ "single_instance_allowed": "Somente uma \u00fanica configura\u00e7\u00e3o de Wemo \u00e9 poss\u00edvel."
+ },
+ "step": {
+ "confirm": {
+ "description": "Voc\u00ea quer configurar o Wemo?",
+ "title": "Wemo"
+ }
+ },
+ "title": "Wemo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/.translations/ru.json b/homeassistant/components/wemo/.translations/ru.json
new file mode 100644
index 00000000000000..c0572510925053
--- /dev/null
+++ b/homeassistant/components/wemo/.translations/ru.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Wemo \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.",
+ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430."
+ },
+ "step": {
+ "confirm": {
+ "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Wemo?",
+ "title": "Wemo"
+ }
+ },
+ "title": "Wemo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/.translations/sv.json b/homeassistant/components/wemo/.translations/sv.json
new file mode 100644
index 00000000000000..0773b0079bf68d
--- /dev/null
+++ b/homeassistant/components/wemo/.translations/sv.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "abort": {
+ "no_devices_found": "Inga Wemo-enheter finns p\u00e5 n\u00e4tverket.",
+ "single_instance_allowed": "Endast en enda konfiguration av Wemo \u00e4r m\u00f6jlig."
+ },
+ "step": {
+ "confirm": {
+ "description": "Vill du konfigurera Wemo?",
+ "title": "Wemo"
+ }
+ },
+ "title": "Wemo"
+ }
+}
\ No newline at end of file
diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py
index d921075bc1a6f9..8353b52b9f0d37 100644
--- a/homeassistant/components/wemo/__init__.py
+++ b/homeassistant/components/wemo/__init__.py
@@ -4,6 +4,7 @@
import requests
import voluptuous as vol
+from homeassistant import config_entries
from homeassistant.components.discovery import SERVICE_WEMO
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
@@ -68,22 +69,35 @@ def coerce_host_port(value):
def setup(hass, config):
"""Set up for WeMo devices."""
+ hass.data[DOMAIN] = config
+
+ if DOMAIN in config:
+ hass.async_create_task(hass.config_entries.flow.async_init(
+ DOMAIN, context={'source': config_entries.SOURCE_IMPORT}))
+
+ return True
+
+
+async def async_setup_entry(hass, entry):
+ """Set up a wemo config entry."""
import pywemo
+ config = hass.data[DOMAIN]
+
# Keep track of WeMo devices
devices = []
# Keep track of WeMo device subscriptions for push updates
global SUBSCRIPTION_REGISTRY
SUBSCRIPTION_REGISTRY = pywemo.SubscriptionRegistry()
- SUBSCRIPTION_REGISTRY.start()
+ await hass.async_add_executor_job(SUBSCRIPTION_REGISTRY.start)
def stop_wemo(event):
"""Shutdown Wemo subscriptions and subscription thread on exit."""
_LOGGER.debug("Shutting down WeMo event subscriptions")
SUBSCRIPTION_REGISTRY.stop()
- hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo)
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_wemo)
def setup_url_for_device(device):
"""Determine setup.xml url for given device."""
@@ -119,7 +133,7 @@ def discovery_dispatch(service, discovery_info):
discovery.load_platform(
hass, component, DOMAIN, discovery_info, config)
- discovery.listen(hass, SERVICE_WEMO, discovery_dispatch)
+ discovery.async_listen(hass, SERVICE_WEMO, discovery_dispatch)
def discover_wemo_devices(now):
"""Run discovery for WeMo devices."""
@@ -145,7 +159,7 @@ def discover_wemo_devices(now):
if d[1].serialnumber == device.serialnumber]:
devices.append((url, device))
- if config.get(DOMAIN, {}).get(CONF_DISCOVERY):
+ if config.get(DOMAIN, {}).get(CONF_DISCOVERY, DEFAULT_DISCOVERY):
_LOGGER.debug("Scanning network for WeMo devices...")
for device in pywemo.discover_devices():
if not [d[1] for d in devices
@@ -168,6 +182,7 @@ def discover_wemo_devices(now):
_LOGGER.debug("WeMo device discovery has finished")
- hass.bus.listen_once(EVENT_HOMEASSISTANT_START, discover_wemo_devices)
+ hass.bus.async_listen_once(
+ EVENT_HOMEASSISTANT_START, discover_wemo_devices)
return True
diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py
new file mode 100644
index 00000000000000..61094dbab3209b
--- /dev/null
+++ b/homeassistant/components/wemo/config_flow.py
@@ -0,0 +1,15 @@
+"""Config flow for Wemo."""
+from homeassistant.helpers import config_entry_flow
+from homeassistant import config_entries
+from . import DOMAIN
+
+
+async def _async_has_devices(hass):
+ """Return if there are devices that can be discovered."""
+ import pywemo
+
+ return bool(pywemo.discover_devices())
+
+
+config_entry_flow.register_discovery_flow(
+ DOMAIN, 'Wemo', _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH)
diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json
index 238be891886859..c610c28da394f0 100644
--- a/homeassistant/components/wemo/manifest.json
+++ b/homeassistant/components/wemo/manifest.json
@@ -1,10 +1,16 @@
{
"domain": "wemo",
"name": "Wemo",
+ "config_flow": true,
"documentation": "https://www.home-assistant.io/components/wemo",
"requirements": [
"pywemo==0.4.34"
],
+ "ssdp": {
+ "manufacturer": [
+ "Belkin International Inc."
+ ]
+ },
"dependencies": [],
"codeowners": [
"@sqldiablo"
diff --git a/homeassistant/components/wemo/strings.json b/homeassistant/components/wemo/strings.json
new file mode 100644
index 00000000000000..d4b40817cb395c
--- /dev/null
+++ b/homeassistant/components/wemo/strings.json
@@ -0,0 +1,15 @@
+{
+ "config": {
+ "title": "Wemo",
+ "step": {
+ "confirm": {
+ "title": "Wemo",
+ "description": "Do you want to set up Wemo?"
+ }
+ },
+ "abort": {
+ "single_instance_allowed": "Only a single configuration of Wemo is possible.",
+ "no_devices_found": "No Wemo devices found on the network."
+ }
+ }
+}
diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json
index c837a46e01107f..118f7a19733d5a 100644
--- a/homeassistant/components/wink/manifest.json
+++ b/homeassistant/components/wink/manifest.json
@@ -3,7 +3,7 @@
"name": "Wink",
"documentation": "https://www.home-assistant.io/components/wink",
"requirements": [
- "pubnubsub-handler==1.0.4",
+ "pubnubsub-handler==1.0.6",
"python-wink==1.10.5"
],
"dependencies": ["configurator"],
diff --git a/homeassistant/components/yr/manifest.json b/homeassistant/components/yr/manifest.json
index 88daadd35aa1c6..7f06ddddcb5700 100644
--- a/homeassistant/components/yr/manifest.json
+++ b/homeassistant/components/yr/manifest.json
@@ -6,5 +6,7 @@
"xmltodict==0.12.0"
],
"dependencies": [],
- "codeowners": []
+ "codeowners": [
+ "@danielhiversen"
+ ]
}
diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py
index 161321d1e88ae1..289aba6ef56292 100644
--- a/homeassistant/components/zeroconf/__init__.py
+++ b/homeassistant/components/zeroconf/__init__.py
@@ -1,14 +1,18 @@
"""Support for exposing Home Assistant via Zeroconf."""
+# PyLint bug confuses absolute/relative imports
+# https://github.com/PyCQA/pylint/issues/1931
+# pylint: disable=no-name-in-module
import logging
+import socket
import ipaddress
import voluptuous as vol
-from aiozeroconf import (
- ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf)
+from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf
+from homeassistant import util
from homeassistant.const import (EVENT_HOMEASSISTANT_STOP, __version__)
-from homeassistant.generated import zeroconf as zeroconf_manifest
+from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT
_LOGGER = logging.getLogger(__name__)
@@ -22,13 +26,14 @@
ATTR_PROPERTIES = 'properties'
ZEROCONF_TYPE = '_home-assistant._tcp.local.'
+HOMEKIT_TYPE = '_hap._tcp.local.'
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({}),
}, extra=vol.ALLOW_EXTRA)
-async def async_setup(hass, config):
+def setup(hass, config):
"""Set up Zeroconf and make Home Assistant discoverable."""
zeroconf_name = '{}.{}'.format(hass.config.location_name, ZEROCONF_TYPE)
@@ -39,44 +44,87 @@ async def async_setup(hass, config):
'requires_api_password': True,
}
- info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name,
- port=hass.http.server_port, properties=params)
+ host_ip = util.get_local_ip()
- zeroconf = Zeroconf(hass.loop)
+ try:
+ host_ip_pton = socket.inet_pton(socket.AF_INET, host_ip)
+ except socket.error:
+ host_ip_pton = socket.inet_pton(socket.AF_INET6, host_ip)
- await zeroconf.register_service(info)
+ info = ServiceInfo(ZEROCONF_TYPE, zeroconf_name, None,
+ addresses=[host_ip_pton], port=hass.http.server_port,
+ properties=params)
- async def new_service(service_type, name):
- """Signal new service discovered."""
- service_info = await zeroconf.get_service_info(service_type, name)
+ zeroconf = Zeroconf()
+
+ zeroconf.register_service(info)
+
+ def service_update(zeroconf, service_type, name, state_change):
+ """Service state changed."""
+ if state_change != ServiceStateChange.Added:
+ return
+
+ service_info = zeroconf.get_service_info(service_type, name)
info = info_from_service(service_info)
_LOGGER.debug("Discovered new device %s %s", name, info)
- for domain in zeroconf_manifest.SERVICE_TYPES[service_type]:
- hass.async_create_task(
+ # If we can handle it as a HomeKit discovery, we do that here.
+ if service_type == HOMEKIT_TYPE and handle_homekit(hass, info):
+ return
+
+ for domain in ZEROCONF[service_type]:
+ hass.add_job(
hass.config_entries.flow.async_init(
domain, context={'source': DOMAIN}, data=info
)
)
- def service_update(_, service_type, name, state_change):
- """Service state changed."""
- if state_change is ServiceStateChange.Added:
- hass.async_create_task(new_service(service_type, name))
-
- for service in zeroconf_manifest.SERVICE_TYPES:
+ for service in ZEROCONF:
ServiceBrowser(zeroconf, service, handlers=[service_update])
- async def stop_zeroconf(_):
+ if HOMEKIT_TYPE not in ZEROCONF:
+ ServiceBrowser(zeroconf, HOMEKIT_TYPE, handlers=[service_update])
+
+ def stop_zeroconf(_):
"""Stop Zeroconf."""
- await zeroconf.unregister_service(info)
- await zeroconf.close()
+ zeroconf.unregister_service(info)
+ zeroconf.close()
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)
+ hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_zeroconf)
return True
+def handle_homekit(hass, info) -> bool:
+ """Handle a HomeKit discovery.
+
+ Return if discovery was forwarded.
+ """
+ model = None
+ props = info.get('properties', {})
+
+ for key in props:
+ if key.lower() == 'md':
+ model = props[key]
+ break
+
+ if model is None:
+ return False
+
+ for test_model in HOMEKIT:
+ if not model.startswith(test_model):
+ continue
+
+ hass.add_job(
+ hass.config_entries.flow.async_init(
+ HOMEKIT[test_model], context={'source': 'homekit'}, data=info
+ )
+ )
+ return True
+
+ return False
+
+
def info_from_service(service):
"""Return prepared info from mDNS entries."""
properties = {}
@@ -89,7 +137,7 @@ def info_from_service(service):
except UnicodeDecodeError:
_LOGGER.warning("Unicode decode error on %s: %s", key, value)
- address = service.address or service.address6
+ address = service.addresses[0]
info = {
ATTR_HOST: str(ipaddress.ip_address(address)),
diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json
index 07e620381e4d7e..1461a54d147a7c 100644
--- a/homeassistant/components/zeroconf/manifest.json
+++ b/homeassistant/components/zeroconf/manifest.json
@@ -3,7 +3,7 @@
"name": "Zeroconf",
"documentation": "https://www.home-assistant.io/components/zeroconf",
"requirements": [
- "aiozeroconf==0.1.8"
+ "zeroconf==0.23.0"
],
"dependencies": [
"api"
diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py
index 1845ae8e999203..83ade5894652df 100644
--- a/homeassistant/components/zha/core/channels/__init__.py
+++ b/homeassistant/components/zha/core/channels/__init__.py
@@ -22,10 +22,6 @@
)
from ..registries import CLUSTER_REPORT_CONFIGS
-NODE_DESCRIPTOR_REQUEST = 0x0002
-MAINS_POWERED = 1
-BATTERY_OR_UNKNOWN = 0
-
ZIGBEE_CHANNEL_REGISTRY = {}
_LOGGER = logging.getLogger(__name__)
@@ -48,7 +44,6 @@ def decorate_command(channel, command):
"""Wrap a cluster command to make it safe."""
@wraps(command)
async def wrapper(*args, **kwds):
- from zigpy.zcl.foundation import Status
from zigpy.exceptions import DeliveryError
try:
result = await command(*args, **kwds)
@@ -58,9 +53,8 @@ async def wrapper(*args, **kwds):
"{}: {}".format("with args", args),
"{}: {}".format("with kwargs", kwds),
"{}: {}".format("and result", result))
- if isinstance(result, bool):
- return result
- return result[1] is Status.SUCCESS
+ return result
+
except (DeliveryError, Timeout) as ex:
_LOGGER.debug(
"%s: command failed: %s exception: %s",
@@ -68,7 +62,7 @@ async def wrapper(*args, **kwds):
command.__name__,
str(ex)
)
- return False
+ return ex
return wrapper
@@ -268,11 +262,6 @@ async def async_initialize(self, from_cache):
class ZDOChannel:
"""Channel for ZDO events."""
- POWER_SOURCES = {
- MAINS_POWERED: 'Mains',
- BATTERY_OR_UNKNOWN: 'Battery or Unknown'
- }
-
def __init__(self, cluster, device):
"""Initialize ZDOChannel."""
self.name = ZDO_CHANNEL
@@ -281,8 +270,6 @@ def __init__(self, cluster, device):
self._status = ChannelStatus.CREATED
self._unique_id = "{}_ZDO".format(device.name)
self._cluster.add_listener(self)
- self.power_source = None
- self.manufacturer_code = None
@property
def unique_id(self):
@@ -314,49 +301,10 @@ async def async_initialize(self, from_cache):
entry = self._zha_device.gateway.zha_storage.async_get_or_create(
self._zha_device)
_LOGGER.debug("entry loaded from storage: %s", entry)
- if entry is not None:
- self.power_source = entry.power_source
- self.manufacturer_code = entry.manufacturer_code
-
- if self.power_source is None:
- self.power_source = BATTERY_OR_UNKNOWN
-
- if self.manufacturer_code is None and not from_cache:
- # this should always be set. This is from us not doing
- # this previously so lets set it up so users don't have
- # to reconfigure every device.
- await self.async_get_node_descriptor(False)
- entry = self._zha_device.gateway.zha_storage.async_update(
- self._zha_device)
- _LOGGER.debug("entry after getting node desc in init: %s", entry)
self._status = ChannelStatus.INITIALIZED
- async def async_get_node_descriptor(self, from_cache):
- """Request the node descriptor from the device."""
- from zigpy.zdo.types import Status
-
- if from_cache:
- return
-
- node_descriptor = await self._cluster.request(
- NODE_DESCRIPTOR_REQUEST,
- self._cluster.device.nwk, tries=3, delay=2)
-
- def get_bit(byteval, idx):
- return int(((byteval & (1 << idx)) != 0))
-
- if node_descriptor is not None and\
- node_descriptor[0] == Status.SUCCESS:
- mac_capability_flags = node_descriptor[2].mac_capability_flags
-
- self.power_source = get_bit(mac_capability_flags, 2)
- self.manufacturer_code = node_descriptor[2].manufacturer_code
-
- _LOGGER.debug("node descriptor: %s", node_descriptor)
-
async def async_configure(self):
"""Configure channel."""
- await self.async_get_node_descriptor(False)
self._status = ChannelStatus.CONFIGURED
diff --git a/homeassistant/components/zha/core/channels/closures.py b/homeassistant/components/zha/core/channels/closures.py
index ba3b6b2e71617f..f2f8d07fde9299 100644
--- a/homeassistant/components/zha/core/channels/closures.py
+++ b/homeassistant/components/zha/core/channels/closures.py
@@ -5,5 +5,44 @@
https://home-assistant.io/components/zha/
"""
import logging
+from homeassistant.core import callback
+from homeassistant.helpers.dispatcher import async_dispatcher_send
+from . import ZigbeeChannel
+from ..const import SIGNAL_ATTR_UPDATED
_LOGGER = logging.getLogger(__name__)
+
+
+class DoorLockChannel(ZigbeeChannel):
+ """Door lock channel."""
+
+ _value_attribute = 0
+
+ async def async_update(self):
+ """Retrieve latest state."""
+ result = await self.get_attribute_value('lock_state', from_cache=True)
+
+ async_dispatcher_send(
+ self._zha_device.hass,
+ "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
+ result
+ )
+
+ @callback
+ def attribute_updated(self, attrid, value):
+ """Handle attribute update from lock cluster."""
+ attr_name = self.cluster.attributes.get(attrid, [attrid])[0]
+ _LOGGER.debug("%s: Attribute report '%s'[%s] = %s",
+ self.unique_id, self.cluster.name, attr_name, value)
+ if attrid == self._value_attribute:
+ async_dispatcher_send(
+ self._zha_device.hass,
+ "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
+ value
+ )
+
+ async def async_initialize(self, from_cache):
+ """Initialize channel."""
+ await self.get_attribute_value(
+ self._value_attribute, from_cache=from_cache)
+ await super().async_initialize(from_cache)
diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py
index 470cd6b38cffc6..3f08a738a13e97 100644
--- a/homeassistant/components/zha/core/channels/general.py
+++ b/homeassistant/components/zha/core/channels/general.py
@@ -8,7 +8,7 @@
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, MAINS_POWERED
+from . import ZigbeeChannel, parse_and_log_command
from ..helpers import get_attr_id_by_name
from ..const import (
SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL,
@@ -87,7 +87,7 @@ async def async_initialize(self, from_cache):
async def async_update(self):
"""Initialize channel."""
- from_cache = not self.device.power_source == MAINS_POWERED
+ from_cache = not self.device.is_mains_powered
_LOGGER.debug(
"%s is attempting to update onoff state - from cache: %s",
self._unique_id,
diff --git a/homeassistant/components/zha/core/channels/registry.py b/homeassistant/components/zha/core/channels/registry.py
index 8f7335d82a9cd8..8b50ff4149731c 100644
--- a/homeassistant/components/zha/core/channels/registry.py
+++ b/homeassistant/components/zha/core/channels/registry.py
@@ -5,6 +5,8 @@
https://home-assistant.io/components/zha/
"""
from . import ZigbeeChannel
+
+from .closures import DoorLockChannel
from .general import (
OnOffChannel, LevelControlChannel, PowerConfigurationChannel, BasicChannel
)
@@ -13,7 +15,6 @@
from .lighting import ColorChannel
from .security import IASZoneChannel
-
ZIGBEE_CHANNEL_REGISTRY = {}
@@ -44,4 +45,5 @@ def populate_channel_registry():
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/const.py b/homeassistant/components/zha/core/const.py
index 193780c9124728..97e2364619aa5c 100644
--- a/homeassistant/components/zha/core/const.py
+++ b/homeassistant/components/zha/core/const.py
@@ -5,6 +5,7 @@
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.fan import DOMAIN as FAN
from homeassistant.components.light import DOMAIN as LIGHT
+from homeassistant.components.lock import DOMAIN as LOCK
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
@@ -27,6 +28,7 @@
BINARY_SENSOR,
FAN,
LIGHT,
+ LOCK,
SENSOR,
SWITCH,
)
@@ -92,6 +94,7 @@
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"
@@ -104,6 +107,8 @@
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'
diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py
index 1a619dff981836..85373517aa213d 100644
--- a/homeassistant/components/zha/core/device.py
+++ b/homeassistant/components/zha/core/device.py
@@ -17,9 +17,10 @@
ATTR_CLUSTER_ID, ATTR_ATTRIBUTE, ATTR_VALUE, ATTR_COMMAND, SERVER,
ATTR_COMMAND_TYPE, ATTR_ARGS, CLIENT_COMMANDS, SERVER_COMMANDS,
ATTR_ENDPOINT_ID, IEEE, MODEL, NAME, UNKNOWN, QUIRK_APPLIED,
- QUIRK_CLASS, ZDO_CHANNEL, MANUFACTURER_CODE, POWER_SOURCE
+ QUIRK_CLASS, ZDO_CHANNEL, MANUFACTURER_CODE, POWER_SOURCE, MAINS_POWERED,
+ BATTERY_OR_UNKNOWN
)
-from .channels import EventRelayChannel, ZDOChannel
+from .channels import EventRelayChannel
_LOGGER = logging.getLogger(__name__)
@@ -68,7 +69,6 @@ def __init__(self, hass, zigpy_device, zha_gateway):
self._zigpy_device.__class__.__module__,
self._zigpy_device.__class__.__name__
)
- self._power_source = None
self.status = DeviceStatus.CREATED
@property
@@ -91,6 +91,13 @@ def model(self):
"""Return model for device."""
return self._model
+ @property
+ def manufacturer_code(self):
+ """Return the manufacturer code for the device."""
+ if self._zigpy_device.node_desc.is_valid:
+ return self._zigpy_device.node_desc.manufacturer_code
+ return None
+
@property
def nwk(self):
"""Return nwk for device."""
@@ -112,20 +119,29 @@ def last_seen(self):
return self._zigpy_device.last_seen
@property
- def manufacturer_code(self):
- """Return manufacturer code for device."""
- if ZDO_CHANNEL in self.cluster_channels:
- return self.cluster_channels.get(ZDO_CHANNEL).manufacturer_code
- return None
+ def is_mains_powered(self):
+ """Return true if device is mains powered."""
+ return self._zigpy_device.node_desc.is_mains_powered
@property
def power_source(self):
"""Return the power source for the device."""
- if self._power_source is not None:
- return self._power_source
- if ZDO_CHANNEL in self.cluster_channels:
- return self.cluster_channels.get(ZDO_CHANNEL).power_source
- return None
+ return MAINS_POWERED if self.is_mains_powered else BATTERY_OR_UNKNOWN
+
+ @property
+ def is_router(self):
+ """Return true if this is a routing capable device."""
+ return self._zigpy_device.node_desc.is_router
+
+ @property
+ def is_coordinator(self):
+ """Return true if this device represents the coordinator."""
+ return self._zigpy_device.node_desc.is_coordinator
+
+ @property
+ def is_end_device(self):
+ """Return true if this device is an end device."""
+ return self._zigpy_device.node_desc.is_end_device
@property
def gateway(self):
@@ -151,10 +167,6 @@ def set_available(self, available):
"""Set availability from restore and prevent signals."""
self._available = available
- def set_power_source(self, power_source):
- """Set the power source."""
- self._power_source = power_source
-
def update_available(self, available):
"""Set sensor availability."""
if self._available != available and available:
@@ -183,7 +195,7 @@ def device_info(self):
QUIRK_APPLIED: self.quirk_applied,
QUIRK_CLASS: self.quirk_class,
MANUFACTURER_CODE: self.manufacturer_code,
- POWER_SOURCE: ZDOChannel.POWER_SOURCES.get(self.power_source)
+ POWER_SOURCE: self.power_source
}
def add_cluster_channel(self, cluster_channel):
@@ -256,7 +268,7 @@ async def async_initialize(self, from_cache=False):
_LOGGER.debug(
'%s: power source: %s',
self.name,
- ZDOChannel.POWER_SOURCES.get(self.power_source)
+ self.power_source
)
self.status = DeviceStatus.INITIALIZED
_LOGGER.debug('%s: completed initialization', self.name)
diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py
index daf14297ec18c9..f8458848fc2f7c 100644
--- a/homeassistant/components/zha/core/gateway.py
+++ b/homeassistant/components/zha/core/gateway.py
@@ -18,7 +18,6 @@
from homeassistant.helpers.entity_component import EntityComponent
from ..api import async_get_device_info
-from .channels import MAINS_POWERED, ZDOChannel
from .const import (
ADD_DEVICE_RELAY_LOGGERS, ATTR_MANUFACTURER, BELLOWS, CONF_BAUDRATE,
CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, CURRENT,
@@ -116,6 +115,8 @@ def device_joined(self, device):
def raw_device_initialized(self, device):
"""Handle a device initialization without quirks loaded."""
+ if device.nwk == 0x0000:
+ return
endpoint_ids = device.endpoints.keys()
ept_id = next((ept_id for ept_id in endpoint_ids if ept_id != 0), None)
manufacturer = 'Unknown'
@@ -232,7 +233,6 @@ def _async_get_or_create_device(self, zigpy_device, is_new_join):
if not is_new_join:
entry = self.zha_storage.async_get_or_create(zha_device)
zha_device.async_update_last_seen(entry.last_seen)
- zha_device.set_power_source(entry.power_source)
return zha_device
@callback
@@ -259,6 +259,9 @@ async def async_update_device_storage(self):
async def async_device_initialized(self, device, is_new_join):
"""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)
is_rejoin = False
@@ -285,16 +288,13 @@ async def async_device_initialized(self, device, is_new_join):
# configure the device
await zha_device.async_configure()
zha_device.update_available(True)
- elif zha_device.power_source is not None\
- and zha_device.power_source == MAINS_POWERED:
+ elif 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",
zha_device.name,
- "with power source: {}".format(
- ZDOChannel.POWER_SOURCES.get(zha_device.power_source)
- )
+ "with power source: {}".format(zha_device.power_source)
)
await zha_device.async_initialize(from_cache=False)
else:
diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py
index b585ce5f48a679..0824426b8d7e50 100644
--- a/homeassistant/components/zha/core/registries.py
+++ b/homeassistant/components/zha/core/registries.py
@@ -8,6 +8,7 @@
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.fan import DOMAIN as FAN
from homeassistant.components.light import DOMAIN as LIGHT
+from homeassistant.components.lock import DOMAIN as LOCK
from homeassistant.components.sensor import DOMAIN as SENSOR
from homeassistant.components.switch import DOMAIN as SWITCH
@@ -154,7 +155,8 @@ def get_deconz_radio():
zcl.clusters.hvac.Fan: FAN,
SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR,
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
- zcl.clusters.general.AnalogInput.cluster_id: SENSOR
+ zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
+ zcl.clusters.closures.DoorLock: LOCK
})
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
@@ -282,6 +284,10 @@ def get_deconz_radio():
'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)
diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py
index f3547cea8a4154..c14345e89dd56a 100644
--- a/homeassistant/components/zha/core/store.py
+++ b/homeassistant/components/zha/core/store.py
@@ -26,8 +26,6 @@ class ZhaDeviceEntry:
name = attr.ib(type=str, default=None)
ieee = attr.ib(type=str, default=None)
- power_source = attr.ib(type=int, default=None)
- manufacturer_code = attr.ib(type=int, default=None)
last_seen = attr.ib(type=float, default=None)
@@ -46,8 +44,6 @@ def async_create(self, device) -> ZhaDeviceEntry:
device_entry = ZhaDeviceEntry(
name=device.name,
ieee=str(device.ieee),
- power_source=device.power_source,
- manufacturer_code=device.manufacturer_code,
last_seen=device.last_seen
)
@@ -85,13 +81,6 @@ def async_update(self, device) -> ZhaDeviceEntry:
old = self.devices[ieee_str]
changes = {}
-
- if device.power_source != old.power_source:
- changes['power_source'] = device.power_source
-
- if device.manufacturer_code != old.manufacturer_code:
- changes['manufacturer_code'] = device.manufacturer_code
-
changes['last_seen'] = device.last_seen
new = self.devices[ieee_str] = attr.evolve(old, **changes)
@@ -109,8 +98,6 @@ async def async_load(self) -> None:
devices[device['ieee']] = ZhaDeviceEntry(
name=device['name'],
ieee=device['ieee'],
- power_source=device['power_source'],
- manufacturer_code=device['manufacturer_code'],
last_seen=device['last_seen'] if 'last_seen' in device
else None
)
@@ -135,8 +122,6 @@ def _data_to_save(self) -> dict:
{
'name': entry.name,
'ieee': entry.ieee,
- 'power_source': entry.power_source,
- 'manufacturer_code': entry.manufacturer_code,
'last_seen': entry.last_seen
} for entry in self.devices.values()
]
diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py
index d894ef5d7a37c4..36df8aada2bc11 100644
--- a/homeassistant/components/zha/entity.py
+++ b/homeassistant/components/zha/entity.py
@@ -14,7 +14,6 @@
DOMAIN, ATTR_MANUFACTURER, DATA_ZHA, DATA_ZHA_BRIDGE_ID, MODEL, NAME,
SIGNAL_REMOVE
)
-from .core.channels import MAINS_POWERED
_LOGGER = logging.getLogger(__name__)
@@ -157,7 +156,7 @@ async def async_check_recently_seen(self):
time.time() - self._zha_device.last_seen <
RESTART_GRACE_PERIOD):
self.async_set_available(True)
- if self.zha_device.power_source != MAINS_POWERED:
+ if not self.zha_device.is_mains_powered:
# mains powered devices will get real time state
self.async_restore_last_state(last_state)
self._zha_device.set_available(True)
diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py
index 8395c2317e8ed1..64c515b06b0919 100644
--- a/homeassistant/components/zha/light.py
+++ b/homeassistant/components/zha/light.py
@@ -2,6 +2,7 @@
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
@@ -14,7 +15,6 @@
)
from .entity import ZhaEntity
-
_LOGGER = logging.getLogger(__name__)
DEFAULT_DURATION = 5
@@ -173,12 +173,12 @@ async def async_turn_on(self, **kwargs):
level = min(254, brightness)
else:
level = self._brightness or 254
- success = await self._level_channel.move_to_level_with_on_off(
+ result = await self._level_channel.move_to_level_with_on_off(
level,
duration
)
- t_log['move_to_level_with_on_off'] = success
- if not success:
+ t_log['move_to_level_with_on_off'] = result
+ if not isinstance(result, list) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._state = bool(level)
@@ -186,9 +186,9 @@ async def async_turn_on(self, **kwargs):
self._brightness = level
if brightness is None or brightness:
- success = await self._on_off_channel.on()
- t_log['on_off'] = success
- if not success:
+ result = await self._on_off_channel.on()
+ t_log['on_off'] = result
+ if not isinstance(result, list) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._state = True
@@ -196,10 +196,10 @@ async def async_turn_on(self, **kwargs):
if light.ATTR_COLOR_TEMP in kwargs and \
self.supported_features & light.SUPPORT_COLOR_TEMP:
temperature = kwargs[light.ATTR_COLOR_TEMP]
- success = await self._color_channel.move_to_color_temp(
+ result = await self._color_channel.move_to_color_temp(
temperature, duration)
- t_log['move_to_color_temp'] = success
- if not success:
+ t_log['move_to_color_temp'] = result
+ if not isinstance(result, list) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._color_temp = temperature
@@ -208,13 +208,13 @@ async def async_turn_on(self, **kwargs):
self.supported_features & light.SUPPORT_COLOR:
hs_color = kwargs[light.ATTR_HS_COLOR]
xy_color = color_util.color_hs_to_xy(*hs_color)
- success = await self._color_channel.move_to_color(
+ result = await self._color_channel.move_to_color(
int(xy_color[0] * 65535),
int(xy_color[1] * 65535),
duration,
)
- t_log['move_to_color'] = success
- if not success:
+ t_log['move_to_color'] = result
+ if not isinstance(result, list) or result[1] is not Status.SUCCESS:
self.debug("turned on: %s", t_log)
return
self._hs_color = hs_color
@@ -227,14 +227,14 @@ async def async_turn_off(self, **kwargs):
duration = kwargs.get(light.ATTR_TRANSITION)
supports_level = self.supported_features & light.SUPPORT_BRIGHTNESS
if duration and supports_level:
- success = await self._level_channel.move_to_level_with_on_off(
+ result = await self._level_channel.move_to_level_with_on_off(
0,
duration*10
)
else:
- success = await self._on_off_channel.off()
- self.debug("turned off: %s", success)
- if not success:
+ result = await self._on_off_channel.off()
+ self.debug("turned off: %s", result)
+ if not isinstance(result, list) or result[1] is not Status.SUCCESS:
return
self._state = False
self.async_schedule_update_ha_state()
@@ -246,6 +246,7 @@ async def async_update(self):
async def async_get_state(self, from_cache=True):
"""Attempt to retrieve on off state from the light."""
+ self.debug("polling current state")
if self._on_off_channel:
self._state = await self._on_off_channel.get_attribute_value(
'on_off', from_cache=from_cache)
diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py
new file mode 100644
index 00000000000000..5ac4a0c2e30825
--- /dev/null
+++ b/homeassistant/components/zha/lock.py
@@ -0,0 +1,134 @@
+"""Locks on Zigbee Home Automation networks."""
+import logging
+
+from zigpy.zcl.foundation import Status
+from homeassistant.core import callback
+from homeassistant.components.lock import (
+ DOMAIN, STATE_UNLOCKED, STATE_LOCKED, LockDevice)
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from .core.const import (
+ DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, DOORLOCK_CHANNEL,
+ SIGNAL_ATTR_UPDATED
+)
+from .entity import ZhaEntity
+
+_LOGGER = logging.getLogger(__name__)
+
+""" The first state is Zigbee 'Not fully locked' """
+
+STATE_LIST = [
+ STATE_UNLOCKED,
+ STATE_LOCKED,
+ STATE_UNLOCKED
+]
+
+VALUE_TO_STATE = {i: state for i, state in enumerate(STATE_LIST)}
+
+
+async def async_setup_platform(hass, config, async_add_entities,
+ discovery_info=None):
+ """Old way of setting up Zigbee Home Automation locks."""
+ pass
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+ """Set up the Zigbee Home Automation Door Lock from config entry."""
+ async def async_discover(discovery_info):
+ await _async_setup_entities(hass, config_entry, async_add_entities,
+ [discovery_info])
+
+ unsub = async_dispatcher_connect(
+ hass, ZHA_DISCOVERY_NEW.format(DOMAIN), async_discover)
+ hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub)
+
+ locks = hass.data.get(DATA_ZHA, {}).get(DOMAIN)
+ if locks is not None:
+ await _async_setup_entities(hass, config_entry, async_add_entities,
+ locks.values())
+ del hass.data[DATA_ZHA][DOMAIN]
+
+
+async def _async_setup_entities(hass, config_entry, async_add_entities,
+ discovery_infos):
+ """Set up the ZHA locks."""
+ entities = []
+ for discovery_info in discovery_infos:
+ entities.append(ZhaDoorLock(**discovery_info))
+
+ async_add_entities(entities, update_before_add=True)
+
+
+class ZhaDoorLock(ZhaEntity, LockDevice):
+ """Representation of a ZHA lock."""
+
+ _domain = DOMAIN
+
+ def __init__(self, unique_id, zha_device, channels, **kwargs):
+ """Init this sensor."""
+ super().__init__(unique_id, zha_device, channels, **kwargs)
+ self._doorlock_channel = self.cluster_channels.get(DOORLOCK_CHANNEL)
+
+ async def async_added_to_hass(self):
+ """Run when about to be added to hass."""
+ await super().async_added_to_hass()
+ await self.async_accept_signal(
+ self._doorlock_channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
+
+ @callback
+ def async_restore_last_state(self, last_state):
+ """Restore previous state."""
+ self._state = VALUE_TO_STATE.get(last_state.state, last_state.state)
+
+ @property
+ def is_locked(self) -> bool:
+ """Return true if entity is locked."""
+ if self._state is None:
+ return False
+ return self._state == STATE_LOCKED
+
+ @property
+ def device_state_attributes(self):
+ """Return state attributes."""
+ return self.state_attributes
+
+ async def async_lock(self, **kwargs):
+ """Lock the lock."""
+ result = await self._doorlock_channel.lock_door()
+ if not isinstance(result, list) or result[0] is not Status.SUCCESS:
+ _LOGGER.error("Error with lock_door: %s", result)
+ return
+ self.async_schedule_update_ha_state()
+
+ async def async_unlock(self, **kwargs):
+ """Unlock the lock."""
+ result = await self._doorlock_channel.unlock_door()
+ if not isinstance(result, list) or result[0] is not Status.SUCCESS:
+ _LOGGER.error("Error with unlock_door: %s", result)
+ return
+ self.async_schedule_update_ha_state()
+
+ async def async_update(self):
+ """Attempt to retrieve state from the lock."""
+ await super().async_update()
+ await self.async_get_state()
+
+ def async_set_state(self, state):
+ """Handle state update from channel."""
+ self._state = VALUE_TO_STATE.get(state, self._state)
+ self.async_schedule_update_ha_state()
+
+ async def async_get_state(self, from_cache=True):
+ """Attempt to retrieve state from the lock."""
+ if self._doorlock_channel:
+ state = await self._doorlock_channel.get_attribute_value(
+ 'lock_state', from_cache=from_cache)
+ if state is not None:
+ self._state = VALUE_TO_STATE.get(state, self._state)
+
+ async def refresh(self, time):
+ """Call async_get_state at an interval."""
+ await self.async_get_state(from_cache=False)
+
+ def debug(self, msg, *args):
+ """Log debug message."""
+ _LOGGER.debug('%s: ' + msg, self.entity_id, *args)
diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json
index 610498e62370c3..d9f17d3f41c241 100644
--- a/homeassistant/components/zha/manifest.json
+++ b/homeassistant/components/zha/manifest.json
@@ -4,11 +4,11 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/zha",
"requirements": [
- "bellows-homeassistant==0.7.3",
- "zha-quirks==0.0.13",
+ "bellows-homeassistant==0.8.0",
+ "zha-quirks==0.0.14",
"zigpy-deconz==0.1.4",
- "zigpy-homeassistant==0.3.3",
- "zigpy-xbee-homeassistant==0.2.1"
+ "zigpy-homeassistant==0.4.2",
+ "zigpy-xbee-homeassistant==0.3.0"
],
"dependencies": [],
"codeowners": [
diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py
index 7efcbabd74e1be..89452f00d9f2f7 100644
--- a/homeassistant/components/zha/switch.py
+++ b/homeassistant/components/zha/switch.py
@@ -1,6 +1,7 @@
"""Switches on Zigbee Home Automation networks."""
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
@@ -66,16 +67,16 @@ def is_on(self) -> bool:
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
- success = await self._on_off_channel.on()
- if not success:
+ result = await self._on_off_channel.on()
+ if not isinstance(result, list) or result[1] is not Status.SUCCESS:
return
self._state = True
self.async_schedule_update_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
- success = await self._on_off_channel.off()
- if not success:
+ result = await self._on_off_channel.off()
+ if not isinstance(result, list) or result[1] is not Status.SUCCESS:
return
self._state = False
self.async_schedule_update_ha_state()
diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py
index 0340964561c5b4..1ece0dbaaa1bd6 100644
--- a/homeassistant/components/zone/__init__.py
+++ b/homeassistant/components/zone/__init__.py
@@ -3,10 +3,12 @@
import voluptuous as vol
+from homeassistant.core import callback
from homeassistant.loader import bind_hass
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
- CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS)
+ CONF_NAME, CONF_LATITUDE, CONF_LONGITUDE, CONF_ICON, CONF_RADIUS,
+ EVENT_CORE_CONFIG_UPDATE)
from homeassistant.helpers import config_per_platform
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.util import slugify
@@ -90,12 +92,24 @@ async def async_setup(hass, config):
hass.async_create_task(zone.async_update_ha_state())
entities.add(zone.entity_id)
- if ENTITY_ID_HOME not in entities and HOME_ZONE not in zone_entries:
- zone = Zone(hass, hass.config.location_name,
- hass.config.latitude, hass.config.longitude,
- DEFAULT_RADIUS, ICON_HOME, False)
- zone.entity_id = ENTITY_ID_HOME
- hass.async_create_task(zone.async_update_ha_state())
+ if ENTITY_ID_HOME in entities or HOME_ZONE in zone_entries:
+ return True
+
+ zone = Zone(hass, hass.config.location_name,
+ hass.config.latitude, hass.config.longitude,
+ DEFAULT_RADIUS, ICON_HOME, False)
+ zone.entity_id = ENTITY_ID_HOME
+ hass.async_create_task(zone.async_update_ha_state())
+
+ @callback
+ def core_config_updated(_):
+ """Handle core config updated."""
+ zone.name = hass.config.location_name
+ zone.latitude = hass.config.latitude
+ zone.longitude = hass.config.longitude
+ zone.async_write_ha_state()
+
+ hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, core_config_updated)
return True
diff --git a/homeassistant/components/zone/zone.py b/homeassistant/components/zone/zone.py
index 20155e06311fa0..51e2a623def058 100644
--- a/homeassistant/components/zone/zone.py
+++ b/homeassistant/components/zone/zone.py
@@ -23,21 +23,18 @@ def in_zone(zone, latitude, longitude, radius=0) -> bool:
class Zone(Entity):
"""Representation of a Zone."""
+ name = None
+
def __init__(self, hass, name, latitude, longitude, radius, icon, passive):
"""Initialize the zone."""
self.hass = hass
- self._name = name
- self._latitude = latitude
- self._longitude = longitude
+ self.name = name
+ self.latitude = latitude
+ self.longitude = longitude
self._radius = radius
self._icon = icon
self._passive = passive
- @property
- def name(self):
- """Return the name of the zone."""
- return self._name
-
@property
def state(self):
"""Return the state property really does nothing for a zone."""
@@ -53,8 +50,8 @@ def state_attributes(self):
"""Return the state attributes of the zone."""
data = {
ATTR_HIDDEN: True,
- ATTR_LATITUDE: self._latitude,
- ATTR_LONGITUDE: self._longitude,
+ ATTR_LATITUDE: self.latitude,
+ ATTR_LONGITUDE: self.longitude,
ATTR_RADIUS: self._radius,
}
if self._passive:
diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py
index 51e956e33144ad..65dd551ebc1f36 100644
--- a/homeassistant/components/zwave/__init__.py
+++ b/homeassistant/components/zwave/__init__.py
@@ -11,7 +11,8 @@
from homeassistant.core import callback, CoreState
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import generate_entity_id
-from homeassistant.helpers.entity_component import EntityComponent
+from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL
+from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.entity_registry import async_get_registry
from homeassistant.const import (
ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
@@ -291,6 +292,8 @@ async def async_setup_entry(hass, config_entry):
hass.data[DATA_DEVICES] = {}
hass.data[DATA_ENTITY_VALUES] = []
+ registry = await async_get_registry(hass)
+
if use_debug: # pragma: no cover
def log_all(signal, value=None):
"""Log all the signals."""
@@ -332,14 +335,23 @@ def value_added(node, value):
new_values = hass.data[DATA_ENTITY_VALUES] + [values]
hass.data[DATA_ENTITY_VALUES] = new_values
- component = EntityComponent(_LOGGER, DOMAIN, hass)
- registry = await async_get_registry(hass)
+ platform = EntityPlatform(
+ hass=hass,
+ logger=_LOGGER,
+ domain=DOMAIN,
+ platform_name=DOMAIN,
+ platform=None,
+ scan_interval=DEFAULT_SCAN_INTERVAL,
+ entity_namespace=None,
+ async_entities_added_callback=lambda: None,
+ )
+ platform.config_entry = config_entry
def node_added(node):
"""Handle a new node on the network."""
entity = ZWaveNodeEntity(node, network)
- def _add_node_to_component():
+ async def _add_node_to_component():
if hass.data[DATA_DEVICES].get(entity.unique_id):
return
@@ -353,10 +365,10 @@ def _add_node_to_component():
return
hass.data[DATA_DEVICES][entity.unique_id] = entity
- component.add_entities([entity])
+ await platform.async_add_entities([entity])
if entity.unique_id:
- _add_node_to_component()
+ hass.async_add_job(_add_node_to_component())
return
@callback
@@ -1057,14 +1069,25 @@ def unique_id(self):
@property
def device_info(self):
"""Return device information."""
- return {
- 'identifiers': {
- (DOMAIN, self.node_id)
- },
+ info = {
'manufacturer': self.node.manufacturer_name,
'model': self.node.product_name,
- 'name': node_name(self.node),
}
+ if self.values.primary.instance > 1:
+ info['name'] = '{} ({})'.format(
+ node_name(self.node), self.values.primary.instance)
+ info['identifiers'] = {
+ (DOMAIN, self.node_id, self.values.primary.instance, ),
+ }
+ info['via_hub'] = (DOMAIN, self.node_id, )
+ else:
+ info['name'] = node_name(self.node)
+ info['identifiers'] = {
+ (DOMAIN, self.node_id),
+ }
+ if self.node_id > 1:
+ info['via_hub'] = (DOMAIN, 1, )
+ return info
@property
def name(self):
diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py
index 0a24f888c20e1d..86f5ae345203e1 100644
--- a/homeassistant/components/zwave/node_entity.py
+++ b/homeassistant/components/zwave/node_entity.py
@@ -124,7 +124,7 @@ def unique_id(self):
@property
def device_info(self):
"""Return device information."""
- return {
+ info = {
'identifiers': {
(DOMAIN, self.node_id)
},
@@ -132,6 +132,9 @@ def device_info(self):
'model': self.node.product_name,
'name': node_name(self.node)
}
+ if self.node_id > 1:
+ info['via_hub'] = (DOMAIN, 1)
+ return info
def network_node_changed(self, node=None, value=None, args=None):
"""Handle a changed node on the network."""
diff --git a/homeassistant/config.py b/homeassistant/config.py
index cffaffd89855a5..7e8bcec08a51a5 100644
--- a/homeassistant/config.py
+++ b/homeassistant/config.py
@@ -1,7 +1,7 @@
"""Module to help with parsing and generating configuration files."""
from collections import OrderedDict
# pylint: disable=no-name-in-module
-from distutils.version import LooseVersion # pylint: disable=import-error
+from distutils.version import StrictVersion # pylint: disable=import-error
import logging
import os
import re
@@ -31,6 +31,7 @@
Integration, async_get_integration, IntegrationNotFound
)
from homeassistant.util.yaml import load_yaml, SECRET_YAML
+from homeassistant.util.package import is_docker_env
import homeassistant.helpers.config_validation as cv
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
from homeassistant.helpers.entity_values import EntityValues
@@ -59,9 +60,6 @@
# http:
# base_url: example.duckdns.org:8123
-# Discover some devices automatically
-discovery:
-
# Sensors
sensor:
# Weather prediction
@@ -336,13 +334,15 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
_LOGGER.info("Upgrading configuration directory from %s to %s",
conf_version, __version__)
- if LooseVersion(conf_version) < LooseVersion('0.50'):
+ version_obj = StrictVersion(conf_version)
+
+ if version_obj < StrictVersion('0.50'):
# 0.50 introduced persistent deps dir.
lib_path = hass.config.path('deps')
if os.path.isdir(lib_path):
shutil.rmtree(lib_path)
- if LooseVersion(conf_version) < LooseVersion('0.92'):
+ if version_obj < StrictVersion('0.92'):
# 0.92 moved google/tts.py to google_translate/tts.py
config_path = find_config_file(hass.config.config_dir)
assert config_path is not None
@@ -360,6 +360,13 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None:
_LOGGER.exception("Migrating to google_translate tts failed")
pass
+ if version_obj < StrictVersion('0.94.0b6') and is_docker_env():
+ # In 0.94 we no longer install packages inside the deps folder when
+ # running inside a Docker container.
+ lib_path = hass.config.path('deps')
+ if os.path.isdir(lib_path):
+ shutil.rmtree(lib_path)
+
with open(version_path, 'wt') as outp:
outp.write(__version__)
diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py
index e96c10e17fa4bd..299bfe9b407453 100644
--- a/homeassistant/config_entries.py
+++ b/homeassistant/config_entries.py
@@ -169,6 +169,8 @@ async def async_step_discovery(info):
DISCOVERY_NOTIFICATION_ID = 'config_entry_discovery'
DISCOVERY_SOURCES = (
+ 'ssdp',
+ 'zeroconf',
SOURCE_DISCOVERY,
SOURCE_IMPORT,
)
diff --git a/homeassistant/core.py b/homeassistant/core.py
index b732eb0d4b3ee2..ef15a4b11a0e45 100644
--- a/homeassistant/core.py
+++ b/homeassistant/core.py
@@ -1288,10 +1288,7 @@ def _update(self, *,
unit_system: Optional[str] = None,
location_name: Optional[str] = None,
time_zone: Optional[str] = None) -> None:
- """Update the configuration from a dictionary.
-
- Async friendly.
- """
+ """Update the configuration from a dictionary."""
self.config_source = source
if latitude is not None:
self.latitude = latitude
@@ -1309,11 +1306,8 @@ def _update(self, *,
if time_zone is not None:
self.set_time_zone(time_zone)
- async def update(self, **kwargs: Any) -> None:
- """Update the configuration from a dictionary.
-
- Async friendly.
- """
+ async def async_update(self, **kwargs: Any) -> None:
+ """Update the configuration from a dictionary."""
self._update(source=SOURCE_STORAGE, **kwargs)
await self.async_store()
self.hass.bus.async_fire(
diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py
index aa1d21a66d39b9..389b84984214c0 100644
--- a/homeassistant/data_entry_flow.py
+++ b/homeassistant/data_entry_flow.py
@@ -58,6 +58,8 @@ async def async_init(self, handler: Hashable, *,
context: Optional[Dict] = None,
data: Any = None) -> Any:
"""Start a configuration flow."""
+ if context is None:
+ context = {}
flow = await self._async_create_flow(
handler, context=context, data=data)
flow.hass = self.hass
diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py
index c9a8c593b27795..955cdf3c8c4dcc 100644
--- a/homeassistant/generated/config_flows.py
+++ b/homeassistant/generated/config_flows.py
@@ -1,10 +1,11 @@
"""Automatically generated by hassfest.
-To update, run python3 -m hassfest
+To update, run python3 -m script.hassfest
"""
FLOWS = [
+ "adguard",
"ambiclimate",
"ambient_station",
"axis",
@@ -49,6 +50,7 @@
"twilio",
"unifi",
"upnp",
+ "wemo",
"zha",
"zone",
"zwave"
diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py
new file mode 100644
index 00000000000000..63dbe7616db370
--- /dev/null
+++ b/homeassistant/generated/ssdp.py
@@ -0,0 +1,19 @@
+"""Automatically generated by hassfest.
+
+To update, run python3 -m script.hassfest
+"""
+
+
+SSDP = {
+ "device_type": {},
+ "manufacturer": {
+ "Belkin International Inc.": [
+ "wemo"
+ ],
+ "Royal Philips Electronics": [
+ "deconz",
+ "hue"
+ ]
+ },
+ "st": {}
+}
diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py
index f009132228c14f..716b212e4c623e 100644
--- a/homeassistant/generated/zeroconf.py
+++ b/homeassistant/generated/zeroconf.py
@@ -1,14 +1,24 @@
"""Automatically generated by hassfest.
-To update, run python3 -m hassfest
+To update, run python3 -m script.hassfest
"""
-SERVICE_TYPES = {
+ZEROCONF = {
"_axis-video._tcp.local.": [
"axis"
],
+ "_coap._udp.local.": [
+ "tradfri"
+ ],
"_esphomelib._tcp.local.": [
"esphome"
+ ],
+ "_hap._tcp.local.": [
+ "homekit_controller"
]
}
+
+HOMEKIT = {
+ "LIFX ": "lifx"
+}
diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py
index 6d200a39c85948..c3e5195131b083 100644
--- a/homeassistant/helpers/config_entry_flow.py
+++ b/homeassistant/helpers/config_entry_flow.py
@@ -81,6 +81,10 @@ async def async_step_discovery(self, discovery_info):
return await self.async_step_confirm()
+ async_step_zeroconf = async_step_discovery
+ async_step_ssdp = async_step_discovery
+ async_step_homekit = async_step_discovery
+
async def async_step_import(self, _):
"""Handle a flow initialized by import."""
if self._async_in_progress() or self._async_current_entries():
diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py
index 9282770de1a85c..7ec6d177178245 100644
--- a/homeassistant/helpers/config_validation.py
+++ b/homeassistant/helpers/config_validation.py
@@ -1,6 +1,5 @@
"""Helpers for config validation using voluptuous."""
import inspect
-import json
import logging
import os
import re
@@ -17,7 +16,7 @@
import homeassistant.util.dt as dt_util
from homeassistant.const import (
CONF_ABOVE, CONF_ALIAS, CONF_BELOW, CONF_CONDITION, CONF_ENTITY_ID,
- CONF_ENTITY_NAMESPACE, CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL,
+ CONF_ENTITY_NAMESPACE, CONF_PLATFORM, CONF_SCAN_INTERVAL,
CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE,
CONF_TIMEOUT, ENTITY_MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET,
TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, __version__)
@@ -29,13 +28,6 @@
# pylint: disable=invalid-name
TIME_PERIOD_ERROR = "offset {} should be format 'HH:MM' or 'HH:MM:SS'"
-OLD_SLUG_VALIDATION = r'^[a-z0-9_]+$'
-OLD_ENTITY_ID_VALIDATION = r"^(\w+)\.(\w+)$"
-# Keep track of invalid slugs and entity ids found so we can create a
-# persistent notification. Rare temporary exception to use a global.
-INVALID_SLUGS_FOUND = {}
-INVALID_ENTITY_IDS_FOUND = {}
-INVALID_EXTRA_KEYS_FOUND = []
# Home Assistant types
@@ -176,17 +168,6 @@ def entity_id(value: Any) -> str:
value = string(value).lower()
if valid_entity_id(value):
return value
- if re.match(OLD_ENTITY_ID_VALIDATION, value):
- # To ease the breaking change, we allow old slugs for now
- # Remove after 0.94 or 1.0
- fixed = '.'.join(util_slugify(part) for part in value.split('.', 1))
- INVALID_ENTITY_IDS_FOUND[value] = fixed
- logging.getLogger(__name__).warning(
- "Found invalid entity_id %s, please update with %s. This "
- "will become a breaking change.",
- value, fixed
- )
- return value
raise vol.Invalid('Entity ID {} is an invalid entity id'.format(value))
@@ -377,21 +358,7 @@ def verify(value: Dict) -> Dict:
raise vol.Invalid('expected dictionary')
for key in value.keys():
- try:
- slug(key)
- except vol.Invalid:
- # To ease the breaking change, we allow old slugs for now
- # Remove after 0.94 or 1.0
- if re.match(OLD_SLUG_VALIDATION, key):
- fixed = util_slugify(key)
- INVALID_SLUGS_FOUND[key] = fixed
- logging.getLogger(__name__).warning(
- "Found invalid slug %s, please update with %s. This "
- "will be come a breaking change.",
- key, fixed
- )
- else:
- raise
+ slug(key)
return schema(value)
return verify
@@ -656,88 +623,7 @@ def validator(value):
# Schemas
-class HASchema(vol.Schema):
- """Schema class that allows us to mark PREVENT_EXTRA errors as warnings."""
-
- def __call__(self, data):
- """Override __call__ to mark PREVENT_EXTRA as warning."""
- try:
- return super().__call__(data)
- except vol.Invalid as orig_err:
- if self.extra != vol.PREVENT_EXTRA:
- raise
-
- # orig_error is of type vol.MultipleInvalid (see super __call__)
- assert isinstance(orig_err, vol.MultipleInvalid)
- # pylint: disable=no-member
- # If it fails with PREVENT_EXTRA, try with ALLOW_EXTRA
- self.extra = vol.ALLOW_EXTRA
- # In case it still fails the following will raise
- try:
- validated = super().__call__(data)
- finally:
- self.extra = vol.PREVENT_EXTRA
-
- # This is a legacy config, print warning
- extra_key_errs = [err.path[-1] for err in orig_err.errors
- if err.error_message == 'extra keys not allowed']
-
- if not extra_key_errs:
- # This should not happen (all errors should be extra key
- # errors). Let's raise the original error anyway.
- raise orig_err
-
- WHITELIST = [
- re.compile(CONF_NAME),
- re.compile(CONF_PLATFORM),
- re.compile('.*_topic'),
- ]
-
- msg = "Your configuration contains extra keys " \
- "that the platform does not support.\n" \
- "Please remove "
- submsg = ', '.join('[{}]'.format(err) for err in
- extra_key_errs)
- submsg += '. '
-
- # Add file+line information, if available
- if hasattr(data, '__config_file__'):
- submsg += " (See {}, line {}). ".format(
- data.__config_file__, data.__line__)
-
- # Add configuration source information, if available
- if hasattr(data, '__configuration_source__'):
- submsg += "\nConfiguration source: {}. ".format(
- data.__configuration_source__)
- redacted_data = {}
-
- # Print configuration causing the error, but filter any potentially
- # sensitive data
- for k, v in data.items():
- if (any(regex.match(k) for regex in WHITELIST) or
- k in extra_key_errs):
- redacted_data[k] = v
- else:
- redacted_data[k] = ''
- submsg += "\nOffending data: {}".format(
- json.dumps(redacted_data))
-
- msg += submsg
- logging.getLogger(__name__).warning(msg)
- INVALID_EXTRA_KEYS_FOUND.append(submsg)
-
- # Return legacy validated config
- return validated
-
- def extend(self, schema, required=None, extra=None):
- """Extend this schema and convert it to HASchema if necessary."""
- ret = super().extend(schema, required=required, extra=extra)
- if extra is not None:
- return ret
- return HASchema(ret.schema, required=required, extra=self.extra)
-
-
-PLATFORM_SCHEMA = HASchema({
+PLATFORM_SCHEMA = vol.Schema({
vol.Required(CONF_PLATFORM): string,
vol.Optional(CONF_ENTITY_NAMESPACE): string,
vol.Optional(CONF_SCAN_INTERVAL): time_period
diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py
index 7908440e92b1d4..30868c33f9df60 100644
--- a/homeassistant/helpers/entity_platform.py
+++ b/homeassistant/helpers/entity_platform.py
@@ -45,7 +45,7 @@ def __init__(self, *, hass, logger, domain, platform_name, platform,
self._async_unsub_polling = None
# Method to cancel the retry of setup
self._async_cancel_retry_setup = None
- self._process_updates = asyncio.Lock()
+ self._process_updates = None
# Platform is None for the EntityComponent "catch-all" EntityPlatform
# which powers entity_component.add_entities
@@ -404,6 +404,8 @@ async def _update_entity_states(self, now):
This method must be run in the event loop.
"""
+ if self._process_updates is None:
+ self._process_updates = asyncio.Lock()
if self._process_updates.locked():
self.logger.warning(
"Updating %s %s took longer than the scheduled update "
diff --git a/homeassistant/helpers/entityfilter.py b/homeassistant/helpers/entityfilter.py
index 7db577dfdc6a42..590aba02670fd3 100644
--- a/homeassistant/helpers/entityfilter.py
+++ b/homeassistant/helpers/entityfilter.py
@@ -1,5 +1,5 @@
"""Helper class to implement include/exclude of entities and domains."""
-from typing import Callable, Dict, Iterable
+from typing import Callable, Dict, List
import voluptuous as vol
@@ -12,7 +12,7 @@
CONF_EXCLUDE_ENTITIES = 'exclude_entities'
-def _convert_filter(config: Dict[str, Iterable[str]]) -> Callable[[str], bool]:
+def _convert_filter(config: Dict[str, List[str]]) -> Callable[[str], bool]:
filt = generate_filter(
config[CONF_INCLUDE_DOMAINS],
config[CONF_INCLUDE_ENTITIES],
@@ -20,6 +20,8 @@ def _convert_filter(config: Dict[str, Iterable[str]]) -> Callable[[str], bool]:
config[CONF_EXCLUDE_ENTITIES],
)
setattr(filt, 'config', config)
+ setattr(
+ filt, 'empty_filter', sum(len(val) for val in config.values()) == 0)
return filt
@@ -34,10 +36,10 @@ def _convert_filter(config: Dict[str, Iterable[str]]) -> Callable[[str], bool]:
}), _convert_filter)
-def generate_filter(include_domains: Iterable[str],
- include_entities: Iterable[str],
- exclude_domains: Iterable[str],
- exclude_entities: Iterable[str]) -> Callable[[str], bool]:
+def generate_filter(include_domains: List[str],
+ include_entities: List[str],
+ exclude_domains: List[str],
+ exclude_entities: List[str]) -> Callable[[str], bool]:
"""Return a function that will filter entities based on the args."""
include_d = set(include_domains)
include_e = set(include_entities)
diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt
index b81f7423f30a4b..8ae1023e1a9c5c 100644
--- a/homeassistant/package_constraints.txt
+++ b/homeassistant/package_constraints.txt
@@ -4,6 +4,7 @@ async_timeout==3.0.1
attrs==19.1.0
bcrypt==3.1.6
certifi>=2018.04.16
+importlib-metadata==0.15
jinja2>=2.10
PyJWT==1.7.1
cryptography==2.6.1
diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py
index 5039fbbd41e8f5..2ab4fe28bdcdb8 100644
--- a/homeassistant/requirements.py
+++ b/homeassistant/requirements.py
@@ -1,13 +1,9 @@
"""Module to handle installing requirements."""
import asyncio
-from functools import partial
+from pathlib import Path
import logging
import os
-import sys
from typing import Any, Dict, List, Optional
-from urllib.parse import urlparse
-
-import pkg_resources
import homeassistant.util.package as pkg_util
from homeassistant.core import HomeAssistant
@@ -15,6 +11,7 @@
DATA_PIP_LOCK = 'pip_lock'
DATA_PKG_CACHE = 'pkg_cache'
CONSTRAINT_FILE = 'package_constraints.txt'
+PROGRESS_FILE = '.pip_progress'
_LOGGER = logging.getLogger(__name__)
@@ -28,19 +25,16 @@ async def async_process_requirements(hass: HomeAssistant, name: str,
if pip_lock is None:
pip_lock = hass.data[DATA_PIP_LOCK] = asyncio.Lock()
- pkg_cache = hass.data.get(DATA_PKG_CACHE)
- if pkg_cache is None:
- pkg_cache = hass.data[DATA_PKG_CACHE] = PackageLoadable(hass)
-
- pip_install = partial(pkg_util.install_package,
- **pip_kwargs(hass.config.config_dir))
+ kwargs = pip_kwargs(hass.config.config_dir)
async with pip_lock:
for req in requirements:
- if await pkg_cache.loadable(req):
+ if pkg_util.is_installed(req):
continue
- ret = await hass.async_add_executor_job(pip_install, req)
+ ret = await hass.async_add_executor_job(
+ _install, hass, req, kwargs
+ )
if not ret:
_LOGGER.error("Not initializing %s because could not install "
@@ -50,58 +44,27 @@ async def async_process_requirements(hass: HomeAssistant, name: str,
return True
+def _install(hass: HomeAssistant, req: str, kwargs: Dict) -> bool:
+ """Install requirement."""
+ progress_path = Path(hass.config.path(PROGRESS_FILE))
+ progress_path.touch()
+ try:
+ return pkg_util.install_package(req, **kwargs)
+ finally:
+ progress_path.unlink()
+
+
def pip_kwargs(config_dir: Optional[str]) -> Dict[str, Any]:
"""Return keyword arguments for PIP install."""
+ is_docker = pkg_util.is_docker_env()
kwargs = {
- 'constraints': os.path.join(os.path.dirname(__file__), CONSTRAINT_FILE)
+ 'constraints': os.path.join(os.path.dirname(__file__),
+ CONSTRAINT_FILE),
+ 'no_cache_dir': is_docker,
}
- if not (config_dir is None or pkg_util.is_virtual_env()):
+ if 'WHEELS_LINKS' in os.environ:
+ kwargs['find_links'] = os.environ['WHEELS_LINKS']
+ if not (config_dir is None or pkg_util.is_virtual_env()) and \
+ not is_docker:
kwargs['target'] = os.path.join(config_dir, 'deps')
return kwargs
-
-
-class PackageLoadable:
- """Class to check if a package is loadable, with built-in cache."""
-
- def __init__(self, hass: HomeAssistant) -> None:
- """Initialize the PackageLoadable class."""
- self.dist_cache = {} # type: Dict[str, pkg_resources.Distribution]
- self.hass = hass
-
- async def loadable(self, package: str) -> bool:
- """Check if a package is what will be loaded when we import it.
-
- Returns True when the requirement is met.
- Returns False when the package is not installed or doesn't meet req.
- """
- dist_cache = self.dist_cache
-
- try:
- req = pkg_resources.Requirement.parse(package)
- except ValueError:
- # This is a zip file. We no longer use this in Home Assistant,
- # leaving it in for custom components.
- req = pkg_resources.Requirement.parse(urlparse(package).fragment)
-
- req_proj_name = req.project_name.lower()
- dist = dist_cache.get(req_proj_name)
-
- if dist is not None:
- return dist in req
-
- for path in sys.path:
- # We read the whole mount point as we're already here
- # Caching it on first call makes subsequent calls a lot faster.
- await self.hass.async_add_executor_job(self._fill_cache, path)
-
- dist = dist_cache.get(req_proj_name)
- if dist is not None:
- return dist in req
-
- return False
-
- def _fill_cache(self, path: str) -> None:
- """Add packages from a path to the cache."""
- dist_cache = self.dist_cache
- for dist in pkg_resources.find_distributions(path):
- dist_cache.setdefault(dist.project_name.lower(), dist)
diff --git a/homeassistant/scripts/__init__.py b/homeassistant/scripts/__init__.py
index 070d907a7d9ec5..961ce5a9d13164 100644
--- a/homeassistant/scripts/__init__.py
+++ b/homeassistant/scripts/__init__.py
@@ -9,9 +9,9 @@
from homeassistant.bootstrap import async_mount_local_lib_path
from homeassistant.config import get_default_config_dir
-from homeassistant.core import HomeAssistant
-from homeassistant.requirements import pip_kwargs, PackageLoadable
-from homeassistant.util.package import install_package, is_virtual_env
+from homeassistant.requirements import pip_kwargs
+from homeassistant.util.package import (
+ install_package, is_virtual_env, is_installed)
def run(args: List) -> int:
@@ -49,10 +49,8 @@ def run(args: List) -> int:
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
- hass = HomeAssistant(loop)
- pkgload = PackageLoadable(hass)
for req in getattr(script, 'REQUIREMENTS', []):
- if loop.run_until_complete(pkgload.loadable(req)):
+ if is_installed(req):
continue
if not install_package(req, **_pip_kwargs):
diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py
index 925755eb741d8a..6f6d03d67b6491 100644
--- a/homeassistant/util/package.py
+++ b/homeassistant/util/package.py
@@ -5,6 +5,12 @@
from subprocess import PIPE, Popen
import sys
from typing import Optional
+from urllib.parse import urlparse
+from pathlib import Path
+
+import pkg_resources
+from importlib_metadata import version, PackageNotFoundError
+
_LOGGER = logging.getLogger(__name__)
@@ -16,9 +22,35 @@ def is_virtual_env() -> bool:
hasattr(sys, 'real_prefix'))
+def is_docker_env() -> bool:
+ """Return True if we run in a docker env."""
+ return Path("/.dockerenv").exists()
+
+
+def is_installed(package: str) -> bool:
+ """Check if a package is installed and will be loaded when we import it.
+
+ Returns True when the requirement is met.
+ Returns False when the package is not installed or doesn't meet req.
+ """
+ try:
+ req = pkg_resources.Requirement.parse(package)
+ except ValueError:
+ # This is a zip file. We no longer use this in Home Assistant,
+ # leaving it in for custom components.
+ req = pkg_resources.Requirement.parse(urlparse(package).fragment)
+
+ try:
+ return version(req.project_name) in req
+ except PackageNotFoundError:
+ return False
+
+
def install_package(package: str, upgrade: bool = True,
target: Optional[str] = None,
- constraints: Optional[str] = None) -> bool:
+ constraints: Optional[str] = None,
+ find_links: Optional[str] = None,
+ no_cache_dir: Optional[bool] = False) -> bool:
"""Install a package on PyPi. Accepts pip compatible package strings.
Return boolean if install successful.
@@ -27,10 +59,14 @@ def install_package(package: str, upgrade: bool = True,
_LOGGER.info('Attempting install of %s', package)
env = os.environ.copy()
args = [sys.executable, '-m', 'pip', 'install', '--quiet', package]
+ if no_cache_dir:
+ args.append('--no-cache-dir')
if upgrade:
args.append('--upgrade')
if constraints is not None:
args += ['--constraint', constraints]
+ if find_links is not None:
+ args += ['--find-links', find_links]
if target:
assert not is_virtual_env()
# This only works if not running in venv
diff --git a/requirements_all.txt b/requirements_all.txt
index 81582926d95158..5136e040bd7aab 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -5,6 +5,7 @@ async_timeout==3.0.1
attrs==19.1.0
bcrypt==3.1.6
certifi>=2018.04.16
+importlib-metadata==0.15
jinja2>=2.10
PyJWT==1.7.1
cryptography==2.6.1
@@ -42,7 +43,7 @@ Mastodon.py==1.4.2
OPi.GPIO==0.3.6
# homeassistant.components.essent
-PyEssent==0.10
+PyEssent==0.12
# homeassistant.components.github
PyGithub==1.43.5
@@ -106,6 +107,9 @@ adafruit-blinka==1.2.1
# homeassistant.components.mcp23017
adafruit-circuitpython-mcp230xx==1.1.2
+# homeassistant.components.adguard
+adguardhome==0.2.0
+
# homeassistant.components.frontier_silicon
afsapi==0.0.4
@@ -125,7 +129,7 @@ aiobotocore==0.10.2
aiodns==2.0.0
# homeassistant.components.esphome
-aioesphomeapi==2.0.1
+aioesphomeapi==2.1.0
# homeassistant.components.freebox
aiofreepybox==0.0.8
@@ -159,10 +163,7 @@ aiopvapi==1.6.14
aioswitcher==2019.3.21
# homeassistant.components.unifi
-aiounifi==4
-
-# homeassistant.components.zeroconf
-aiozeroconf==0.1.8
+aiounifi==6
# homeassistant.components.aladdin_connect
aladdin_connect==0.3
@@ -214,7 +215,7 @@ av==6.1.2
# avion==0.10
# homeassistant.components.axis
-axis==23
+axis==24
# homeassistant.components.azure_event_hub
azure-eventhub==1.3.1
@@ -237,7 +238,7 @@ batinfo==0.4.2
beautifulsoup4==4.7.1
# homeassistant.components.zha
-bellows-homeassistant==0.7.3
+bellows-homeassistant==0.8.0
# homeassistant.components.bmw_connected_drive
bimmer_connected==0.5.3
@@ -274,7 +275,7 @@ boto3==1.9.16
braviarc-homeassistant==0.3.7.dev0
# homeassistant.components.broadlink
-broadlink==0.10.0
+broadlink==0.11.0
# homeassistant.components.brottsplatskartan
brottsplatskartan==0.0.1
@@ -354,7 +355,7 @@ defusedxml==0.6.0
deluge-client==1.4.0
# homeassistant.components.denonavr
-denonavr==0.7.8
+denonavr==0.7.9
# homeassistant.components.directv
directpy==0.5
@@ -414,7 +415,7 @@ enturclient==0.2.0
# envirophat==0.0.6
# homeassistant.components.enphase_envoy
-envoy_reader==0.3
+envoy_reader==0.4
# homeassistant.components.season
ephem==3.7.6.0
@@ -455,7 +456,7 @@ fiblary3==0.1.7
fints==1.0.1
# homeassistant.components.fitbit
-fitbit==0.3.0
+fitbit==0.3.1
# homeassistant.components.fixer
fixerio==1.0.0a0
@@ -549,7 +550,7 @@ habitipy==0.2.0
hangups==0.4.9
# homeassistant.components.cloud
-hass-nabucasa==0.12
+hass-nabucasa==0.13
# homeassistant.components.mqtt
hbmqtt==0.9.4
@@ -579,7 +580,7 @@ hole==0.3.0
holidays==0.9.10
# homeassistant.components.frontend
-home-assistant-frontend==20190523.0
+home-assistant-frontend==20190604.0
# homeassistant.components.zwave
homeassistant-pyozw==0.1.4
@@ -598,7 +599,7 @@ horimote==0.4.1
httplib2==0.10.3
# homeassistant.components.huawei_lte
-huawei-lte-api==1.1.5
+huawei-lte-api==1.2.0
# homeassistant.components.hydrawise
hydrawiser==0.1.1
@@ -729,10 +730,10 @@ mbddns==0.1.2
messagebird==1.2.0
# homeassistant.components.meteoalarm
-meteoalertapi==0.0.8
+meteoalertapi==0.1.3
# homeassistant.components.meteo_france
-meteofrance==0.3.4
+meteofrance==0.3.7
# homeassistant.components.mfi
mficlient==0.3.0
@@ -777,6 +778,7 @@ nessclient==0.9.15
netdata==0.1.2
# homeassistant.components.discovery
+# homeassistant.components.ssdp
netdisco==2.6.0
# homeassistant.components.neurio_energy
@@ -919,7 +921,7 @@ psutil==5.6.2
ptvsd==4.2.8
# homeassistant.components.wink
-pubnubsub-handler==1.0.4
+pubnubsub-handler==1.0.6
# homeassistant.components.pushbullet
pushbullet.py==0.11.0
@@ -965,7 +967,7 @@ pyRFXtrx==0.23
# pySwitchmate==0.4.5
# homeassistant.components.tibber
-pyTibber==0.10.3
+pyTibber==0.11.5
# homeassistant.components.dlink
pyW215==0.6.0
@@ -1022,7 +1024,7 @@ pycfdns==0.0.1
pychannels==1.0.0
# homeassistant.components.cast
-pychromecast==3.2.1
+pychromecast==3.2.2
# homeassistant.components.cmus
pycmus==0.1.1
@@ -1046,7 +1048,7 @@ pydaikin==1.4.6
pydanfossair==0.1.0
# homeassistant.components.deconz
-pydeconz==58
+pydeconz==59
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -1288,6 +1290,9 @@ pyrainbird==0.1.6
# homeassistant.components.recswitch
pyrecswitch==1.0.2
+# homeassistant.components.repetier
+pyrepetier==3.0.5
+
# homeassistant.components.ruter
pyruter==1.1.0
@@ -1325,7 +1330,7 @@ pysmartthings==0.6.8
pysnmp==4.4.9
# homeassistant.components.sonos
-pysonos==0.0.12
+pysonos==0.0.14
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -1442,7 +1447,7 @@ python-telegram-bot==11.1.0
python-twitch-client==0.6.0
# homeassistant.components.velbus
-python-velbus==2.0.24
+python-velbus==2.0.26
# homeassistant.components.vlc
python-vlc==1.1.2
@@ -1483,9 +1488,6 @@ pytrafikverket==0.1.5.9
# homeassistant.components.ubee
pyubee==0.6
-# homeassistant.components.unifi
-pyunifi==2.16
-
# homeassistant.components.uptimerobot
pyuptimerobot==0.0.5
@@ -1493,7 +1495,7 @@ pyuptimerobot==0.0.5
# pyuserinput==0.1.11
# homeassistant.components.vera
-pyvera==0.2.45
+pyvera==0.3.1
# homeassistant.components.vesync
pyvesync_v2==0.9.7
@@ -1553,7 +1555,7 @@ restrictedpython==4.0b8
rfk101py==0.0.1
# homeassistant.components.rflink
-rflink==0.0.37
+rflink==0.0.46
# homeassistant.components.ring
ring_doorbell==0.2.3
@@ -1616,7 +1618,7 @@ shodan==1.13.0
simplepush==1.1.4
# homeassistant.components.simplisafe
-simplisafe-python==3.4.1
+simplisafe-python==3.4.2
# homeassistant.components.sisyphus
sisyphus-control==2.1
@@ -1656,6 +1658,9 @@ snapcast==2.0.9
# homeassistant.components.socialblade
socialbladeclient==0.2
+# homeassistant.components.solaredge_local
+solaredge-local==0.1.4
+
# homeassistant.components.solaredge
solaredge==0.0.2
@@ -1708,7 +1713,7 @@ sucks==0.9.4
swisshydrodata==0.0.3
# homeassistant.components.synology_srm
-synology-srm==0.0.6
+synology-srm==0.0.7
# homeassistant.components.tahoma
tahoma-api==0.0.14
@@ -1753,7 +1758,7 @@ todoist-python==7.0.17
toonapilib==3.2.2
# homeassistant.components.totalconnect
-total_connect_client==0.25
+total_connect_client==0.27
# homeassistant.components.tplink_lte
tp-connected==0.0.4
@@ -1872,8 +1877,11 @@ youtube_dl==2019.05.11
# homeassistant.components.zengge
zengge==0.2
+# homeassistant.components.zeroconf
+zeroconf==0.23.0
+
# homeassistant.components.zha
-zha-quirks==0.0.13
+zha-quirks==0.0.14
# homeassistant.components.zhong_hong
zhong_hong_hvac==1.0.9
@@ -1885,10 +1893,10 @@ ziggo-mediabox-xl==1.1.0
zigpy-deconz==0.1.4
# homeassistant.components.zha
-zigpy-homeassistant==0.3.3
+zigpy-homeassistant==0.4.2
# homeassistant.components.zha
-zigpy-xbee-homeassistant==0.2.1
+zigpy-xbee-homeassistant==0.3.0
# homeassistant.components.zoneminder
zm-py==0.3.3
diff --git a/requirements_test.txt b/requirements_test.txt
index ff4d86436bb7ae..7de1ad9ab1d447 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -11,8 +11,8 @@ mypy==0.701
pydocstyle==3.0.0
pylint==2.3.1
pytest-aiohttp==0.3.0
-pytest-cov==2.6.1
+pytest-cov==2.7.1
pytest-sugar==0.9.2
pytest-timeout==1.3.3
-pytest==4.4.1
+pytest==4.6.1
requests_mock==1.5.2
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 14c074b46d8741..8729f50411b3e9 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -12,10 +12,10 @@ mypy==0.701
pydocstyle==3.0.0
pylint==2.3.1
pytest-aiohttp==0.3.0
-pytest-cov==2.6.1
+pytest-cov==2.7.1
pytest-sugar==0.9.2
pytest-timeout==1.3.3
-pytest==4.4.1
+pytest==4.6.1
requests_mock==1.5.2
@@ -35,6 +35,9 @@ PyTransportNSW==0.1.1
# homeassistant.components.yessssms
YesssSMS==0.2.3
+# homeassistant.components.adguard
+adguardhome==0.2.0
+
# homeassistant.components.ambient_station
aioambient==0.3.0
@@ -44,6 +47,9 @@ aioautomatic==0.6.5
# homeassistant.components.aws
aiobotocore==0.10.2
+# homeassistant.components.esphome
+aioesphomeapi==2.1.0
+
# homeassistant.components.emulated_hue
# homeassistant.components.http
aiohttp_cors==0.7.0
@@ -55,10 +61,7 @@ aiohue==1.9.1
aioswitcher==2019.3.21
# homeassistant.components.unifi
-aiounifi==4
-
-# homeassistant.components.zeroconf
-aiozeroconf==0.1.8
+aiounifi==6
# homeassistant.components.ambiclimate
ambiclimate==0.1.2
@@ -70,10 +73,10 @@ apns2==0.3.0
av==6.1.2
# homeassistant.components.axis
-axis==23
+axis==24
# homeassistant.components.zha
-bellows-homeassistant==0.7.3
+bellows-homeassistant==0.8.0
# homeassistant.components.caldav
caldav==0.6.1
@@ -136,7 +139,7 @@ ha-ffmpeg==2.0
hangups==0.4.9
# homeassistant.components.cloud
-hass-nabucasa==0.12
+hass-nabucasa==0.13
# homeassistant.components.mqtt
hbmqtt==0.9.4
@@ -148,7 +151,7 @@ hdate==0.8.7
holidays==0.9.10
# homeassistant.components.frontend
-home-assistant-frontend==20190523.0
+home-assistant-frontend==20190604.0
# homeassistant.components.homekit_controller
homekit[IP]==0.14.0
@@ -181,6 +184,10 @@ mbddns==0.1.2
# homeassistant.components.mfi
mficlient==0.3.0
+# homeassistant.components.discovery
+# homeassistant.components.ssdp
+netdisco==2.6.0
+
# homeassistant.components.iqvia
# homeassistant.components.opencv
# homeassistant.components.tensorflow
@@ -226,7 +233,7 @@ pyHS100==0.3.5
pyblackbird==0.5
# homeassistant.components.deconz
-pydeconz==58
+pydeconz==59
# homeassistant.components.zwave
pydispatcher==2.0.5
@@ -270,7 +277,7 @@ pysmartapp==0.3.2
pysmartthings==0.6.8
# homeassistant.components.sonos
-pysonos==0.0.12
+pysonos==0.0.14
# homeassistant.components.spc
pyspcwebgw==0.4.0
@@ -287,9 +294,6 @@ python_awair==0.0.4
# homeassistant.components.tradfri
pytradfri[async]==6.0.1
-# homeassistant.components.unifi
-pyunifi==2.16
-
# homeassistant.components.html5
pywebpush==1.9.2
@@ -300,7 +304,7 @@ regenmaschine==1.4.0
restrictedpython==4.0b8
# homeassistant.components.rflink
-rflink==0.0.37
+rflink==0.0.46
# homeassistant.components.ring
ring_doorbell==0.2.3
@@ -309,7 +313,7 @@ ring_doorbell==0.2.3
rxv==0.6.0
# homeassistant.components.simplisafe
-simplisafe-python==3.4.1
+simplisafe-python==3.4.2
# homeassistant.components.sleepiq
sleepyq==0.6
@@ -347,5 +351,8 @@ vultr==0.1.2
# homeassistant.components.wake_on_lan
wakeonlan==1.1.6
+# homeassistant.components.zeroconf
+zeroconf==0.23.0
+
# homeassistant.components.zha
-zigpy-homeassistant==0.3.3
+zigpy-homeassistant==0.4.2
diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py
index 108d0bcab0713c..33f27a6702188c 100755
--- a/script/gen_requirements_all.py
+++ b/script/gen_requirements_all.py
@@ -42,15 +42,16 @@
)
TEST_REQUIREMENTS = (
+ 'adguardhome',
'ambiclimate',
'aioambient',
'aioautomatic',
'aiobotocore',
+ 'aioesphomeapi',
'aiohttp_cors',
'aiohue',
'aiounifi',
'aioswitcher',
- 'aiozeroconf',
'apns2',
'av',
'axis',
@@ -89,6 +90,7 @@
'luftdaten',
'mbddns',
'mficlient',
+ 'netdisco',
'numpy',
'oauth2client',
'paho-mqtt',
@@ -148,6 +150,7 @@
'vultr',
'YesssSMS',
'ruamel.yaml',
+ 'zeroconf',
'zigpy-homeassistant',
'bellows-homeassistant',
)
diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py
index 6a6b19aada7345..5ee52e72f7acb9 100644
--- a/script/hassfest/__main__.py
+++ b/script/hassfest/__main__.py
@@ -4,15 +4,23 @@
from .model import Integration, Config
from . import (
- dependencies, manifest, codeowners, services, config_flow, zeroconf)
+ codeowners,
+ config_flow,
+ dependencies,
+ manifest,
+ services,
+ ssdp,
+ zeroconf,
+)
PLUGINS = [
- manifest,
- dependencies,
codeowners,
- services,
config_flow,
- zeroconf
+ dependencies,
+ manifest,
+ services,
+ ssdp,
+ zeroconf,
]
diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py
index 2f204227f25578..dd3c07fefd294b 100644
--- a/script/hassfest/config_flow.py
+++ b/script/hassfest/config_flow.py
@@ -7,7 +7,7 @@
BASE = """
\"\"\"Automatically generated by hassfest.
-To update, run python3 -m hassfest
+To update, run python3 -m script.hassfest
\"\"\"
diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py
index cfb2fdc006acdc..3e25ab31712c6c 100644
--- a/script/hassfest/manifest.py
+++ b/script/hassfest/manifest.py
@@ -12,6 +12,14 @@
vol.Required('name'): str,
vol.Optional('config_flow'): bool,
vol.Optional('zeroconf'): [str],
+ vol.Optional('ssdp'): vol.Schema({
+ vol.Optional('st'): [str],
+ vol.Optional('manufacturer'): [str],
+ vol.Optional('device_type'): [str],
+ }),
+ vol.Optional('homekit'): vol.Schema({
+ vol.Optional('models'): [str],
+ }),
vol.Required('documentation'): str,
vol.Required('requirements'): [str],
vol.Required('dependencies'): [str],
diff --git a/script/hassfest/ssdp.py b/script/hassfest/ssdp.py
new file mode 100644
index 00000000000000..9c745e5b033687
--- /dev/null
+++ b/script/hassfest/ssdp.py
@@ -0,0 +1,90 @@
+"""Generate ssdp file."""
+from collections import OrderedDict, defaultdict
+import json
+from typing import Dict
+
+from .model import Integration, Config
+
+BASE = """
+\"\"\"Automatically generated by hassfest.
+
+To update, run python3 -m script.hassfest
+\"\"\"
+
+
+SSDP = {}
+""".strip()
+
+
+def sort_dict(value):
+ """Sort a dictionary."""
+ return OrderedDict((key, value[key])
+ for key in sorted(value))
+
+
+def generate_and_validate(integrations: Dict[str, Integration]):
+ """Validate and generate ssdp data."""
+ data = {
+ 'st': defaultdict(list),
+ 'manufacturer': defaultdict(list),
+ 'device_type': defaultdict(list),
+ }
+
+ for domain in sorted(integrations):
+ integration = integrations[domain]
+
+ if not integration.manifest:
+ continue
+
+ ssdp = integration.manifest.get('ssdp')
+
+ if not ssdp:
+ continue
+
+ try:
+ with open(str(integration.path / "config_flow.py")) as fp:
+ content = fp.read()
+ if (' async_step_ssdp(' not in content and
+ 'register_discovery_flow' not in content):
+ integration.add_error(
+ 'ssdp', 'Config flow has no async_step_ssdp')
+ continue
+ except FileNotFoundError:
+ integration.add_error(
+ 'ssdp',
+ 'SSDP info in a manifest requires a config flow to exist'
+ )
+ continue
+
+ for key in 'st', 'manufacturer', 'device_type':
+ if key not in ssdp:
+ continue
+
+ for value in ssdp[key]:
+ data[key][value].append(domain)
+
+ data = sort_dict({key: sort_dict(value) for key, value in data.items()})
+ return BASE.format(json.dumps(data, indent=4))
+
+
+def validate(integrations: Dict[str, Integration], config: Config):
+ """Validate ssdp file."""
+ ssdp_path = config.root / 'homeassistant/generated/ssdp.py'
+ config.cache['ssdp'] = content = generate_and_validate(integrations)
+
+ with open(str(ssdp_path), 'r') as fp:
+ if fp.read().strip() != content:
+ config.add_error(
+ "ssdp",
+ "File ssdp.py is not up to date. "
+ "Run python3 -m script.hassfest",
+ fixable=True
+ )
+ return
+
+
+def generate(integrations: Dict[str, Integration], config: Config):
+ """Generate ssdp file."""
+ ssdp_path = config.root / 'homeassistant/generated/ssdp.py'
+ with open(str(ssdp_path), 'w') as fp:
+ fp.write(config.cache['ssdp'] + '\n')
diff --git a/script/hassfest/zeroconf.py b/script/hassfest/zeroconf.py
index 26e302c864c471..895ae4ab790d6c 100644
--- a/script/hassfest/zeroconf.py
+++ b/script/hassfest/zeroconf.py
@@ -1,5 +1,5 @@
"""Generate zeroconf file."""
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
import json
from typing import Dict
@@ -8,17 +8,20 @@
BASE = """
\"\"\"Automatically generated by hassfest.
-To update, run python3 -m hassfest
+To update, run python3 -m script.hassfest
\"\"\"
-SERVICE_TYPES = {}
+ZEROCONF = {}
+
+HOMEKIT = {}
""".strip()
def generate_and_validate(integrations: Dict[str, Integration]):
"""Validate and generate zeroconf data."""
- service_type_dict = {}
+ service_type_dict = defaultdict(list)
+ homekit_dict = {}
for domain in sorted(integrations):
integration = integrations[domain]
@@ -26,22 +29,82 @@ def generate_and_validate(integrations: Dict[str, Integration]):
if not integration.manifest:
continue
- service_types = integration.manifest.get('zeroconf')
+ service_types = integration.manifest.get('zeroconf', [])
+ homekit = integration.manifest.get('homekit', {})
+ homekit_models = homekit.get('models', [])
+
+ if not service_types and not homekit_models:
+ continue
- if not service_types:
+ try:
+ with open(str(integration.path / "config_flow.py")) as fp:
+ content = fp.read()
+ uses_discovery_flow = 'register_discovery_flow' in content
+
+ if (service_types and not uses_discovery_flow and
+ ' async_step_zeroconf(' not in content):
+ integration.add_error(
+ 'zeroconf', 'Config flow has no async_step_zeroconf')
+ continue
+
+ if (homekit_models and not uses_discovery_flow and
+ ' async_step_homekit(' not in content):
+ integration.add_error(
+ 'zeroconf', 'Config flow has no async_step_homekit')
+ continue
+
+ except FileNotFoundError:
+ integration.add_error(
+ 'zeroconf',
+ 'Zeroconf info in a manifest requires a config flow to exist'
+ )
continue
for service_type in service_types:
+ service_type_dict[service_type].append(domain)
- if service_type not in service_type_dict:
- service_type_dict[service_type] = []
+ for model in homekit_models:
+ # We add a space, as we want to test for it to be model + space.
+ model += " "
- service_type_dict[service_type].append(domain)
+ if model in homekit_dict:
+ integration.add_error(
+ 'zeroconf',
+ 'Integrations {} and {} have overlapping HomeKit '
+ 'models'.format(domain, homekit_dict[model]))
+ break
+
+ homekit_dict[model] = domain
+
+ # HomeKit models are matched on starting string, make sure none overlap.
+ warned = set()
+ for key in homekit_dict:
+ if key in warned:
+ continue
+
+ # n^2 yoooo
+ for key_2 in homekit_dict:
+ if key == key_2 or key_2 in warned:
+ continue
+
+ if key.startswith(key_2) or key_2.startswith(key):
+ integration.add_error(
+ 'zeroconf',
+ 'Integrations {} and {} have overlapping HomeKit '
+ 'models'.format(homekit_dict[key], homekit_dict[key_2]))
+ warned.add(key)
+ warned.add(key_2)
+ break
- data = OrderedDict((key, service_type_dict[key])
- for key in sorted(service_type_dict))
+ zeroconf = OrderedDict((key, service_type_dict[key])
+ for key in sorted(service_type_dict))
+ homekit = OrderedDict((key, homekit_dict[key])
+ for key in sorted(homekit_dict))
- return BASE.format(json.dumps(data, indent=4))
+ return BASE.format(
+ json.dumps(zeroconf, indent=4),
+ json.dumps(homekit, indent=4),
+ )
def validate(integrations: Dict[str, Integration], config: Config):
@@ -50,7 +113,8 @@ def validate(integrations: Dict[str, Integration], config: Config):
config.cache['zeroconf'] = content = generate_and_validate(integrations)
with open(str(zeroconf_path), 'r') as fp:
- if fp.read().strip() != content:
+ current = fp.read().strip()
+ if current != content:
config.add_error(
"zeroconf",
"File zeroconf.py is not up to date. "
diff --git a/setup.py b/setup.py
index b1b66e0ca01b79..2ae5d8e8c3b553 100755
--- a/setup.py
+++ b/setup.py
@@ -38,6 +38,7 @@
'attrs==19.1.0',
'bcrypt==3.1.6',
'certifi>=2018.04.16',
+ 'importlib-metadata==0.15',
'jinja2>=2.10',
'PyJWT==1.7.1',
# PyJWT has loose dependency. We want the latest one.
diff --git a/tests/components/adguard/__init__.py b/tests/components/adguard/__init__.py
new file mode 100644
index 00000000000000..318e881ef2f90b
--- /dev/null
+++ b/tests/components/adguard/__init__.py
@@ -0,0 +1 @@
+"""Tests for the AdGuard Home component."""
diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py
new file mode 100644
index 00000000000000..451fd1436d411d
--- /dev/null
+++ b/tests/components/adguard/test_config_flow.py
@@ -0,0 +1,167 @@
+"""Tests for the AdGuard Home config flow."""
+import aiohttp
+
+from homeassistant import data_entry_flow
+from homeassistant.components.adguard import config_flow
+from homeassistant.components.adguard.const import DOMAIN
+from homeassistant.const import (
+ CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME,
+ CONF_VERIFY_SSL)
+
+from tests.common import MockConfigEntry
+
+FIXTURE_USER_INPUT = {
+ CONF_HOST: '127.0.0.1',
+ CONF_PORT: 3000,
+ CONF_USERNAME: 'user',
+ CONF_PASSWORD: 'pass',
+ CONF_SSL: True,
+ CONF_VERIFY_SSL: True,
+}
+
+
+async def test_show_authenticate_form(hass):
+ """Test that the setup form is served."""
+ flow = config_flow.AdGuardHomeFlowHandler()
+ 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_connection_error(hass, aioclient_mock):
+ """Test we show user form on AdGuard Home connection error."""
+ aioclient_mock.get(
+ "{}://{}:{}/control/status".format(
+ 'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http',
+ FIXTURE_USER_INPUT[CONF_HOST],
+ FIXTURE_USER_INPUT[CONF_PORT],
+ ),
+ exc=aiohttp.ClientError,
+ )
+
+ flow = config_flow.AdGuardHomeFlowHandler()
+ flow.hass = hass
+ result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'user'
+ assert result['errors'] == {'base': 'connection_error'}
+
+
+async def test_full_flow_implementation(hass, aioclient_mock):
+ """Test registering an integration and finishing flow works."""
+ aioclient_mock.get(
+ "{}://{}:{}/control/status".format(
+ 'https' if FIXTURE_USER_INPUT[CONF_SSL] else 'http',
+ FIXTURE_USER_INPUT[CONF_HOST],
+ FIXTURE_USER_INPUT[CONF_PORT],
+ ),
+ json={'version': '1.0'},
+ headers={'Content-Type': 'application/json'},
+ )
+
+ flow = config_flow.AdGuardHomeFlowHandler()
+ 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'
+
+ result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT)
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['title'] == FIXTURE_USER_INPUT[CONF_HOST]
+ assert result['data'][CONF_HOST] == FIXTURE_USER_INPUT[CONF_HOST]
+ assert result['data'][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD]
+ assert result['data'][CONF_PORT] == FIXTURE_USER_INPUT[CONF_PORT]
+ assert result['data'][CONF_SSL] == FIXTURE_USER_INPUT[CONF_SSL]
+ assert result['data'][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME]
+ assert (
+ result['data'][CONF_VERIFY_SSL] == FIXTURE_USER_INPUT[CONF_VERIFY_SSL]
+ )
+
+
+async def test_integration_already_exists(hass):
+ """Test we only allow a single config flow."""
+ MockConfigEntry(domain=DOMAIN).add_to_hass(hass)
+
+ result = await hass.config_entries.flow.async_init(
+ DOMAIN, context={'source': 'user'}
+ )
+ assert result['type'] == 'abort'
+ assert result['reason'] == 'single_instance_allowed'
+
+
+async def test_hassio_single_instance(hass):
+ """Test we only allow a single config flow."""
+ MockConfigEntry(domain='adguard', data={'host': '1.2.3.4'}).add_to_hass(
+ hass
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ 'adguard', context={'source': 'hassio'}
+ )
+ assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
+ assert result['reason'] == 'single_instance_allowed'
+
+
+async def test_hassio_confirm(hass, aioclient_mock):
+ """Test we can finish a config flow."""
+ aioclient_mock.get(
+ "http://mock-adguard:3000/control/status",
+ json={'version': '1.0'},
+ headers={'Content-Type': 'application/json'},
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ 'adguard',
+ data={
+ 'addon': 'AdGuard Home Addon',
+ 'host': 'mock-adguard',
+ 'port': 3000,
+ },
+ context={'source': 'hassio'},
+ )
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'hassio_confirm'
+ assert result['description_placeholders'] == {
+ 'addon': 'AdGuard Home Addon'
+ }
+
+ result = await hass.config_entries.flow.async_configure(
+ result['flow_id'], {}
+ )
+ assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
+ assert result['title'] == 'AdGuard Home Addon'
+ assert result['data'][CONF_HOST] == 'mock-adguard'
+ assert result['data'][CONF_PASSWORD] is None
+ assert result['data'][CONF_PORT] == 3000
+ assert result['data'][CONF_SSL] is False
+ assert result['data'][CONF_USERNAME] is None
+ assert result['data'][CONF_VERIFY_SSL]
+
+
+async def test_hassio_connection_error(hass, aioclient_mock):
+ """Test we show hassio confirm form on AdGuard Home connection error."""
+ aioclient_mock.get(
+ "http://mock-adguard:3000/control/status",
+ exc=aiohttp.ClientError,
+ )
+
+ result = await hass.config_entries.flow.async_init(
+ 'adguard',
+ data={
+ 'addon': 'AdGuard Home Addon',
+ 'host': 'mock-adguard',
+ 'port': 3000,
+ },
+ context={'source': 'hassio'},
+ )
+
+ result = await hass.config_entries.flow.async_configure(
+ result['flow_id'], {}
+ )
+
+ assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
+ assert result['step_id'] == 'hassio_confirm'
+ assert result['errors'] == {'base': 'connection_error'}
diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py
index d251e8fdce8640..d5bb8236a1e1e0 100644
--- a/tests/components/awair/test_sensor.py
+++ b/tests/components/awair/test_sensor.py
@@ -178,7 +178,7 @@ async def test_awair_humid(hass):
await setup_awair(hass)
sensor = hass.states.get("sensor.awair_humidity")
- assert sensor.state == "32.73"
+ assert sensor.state == "32.7"
assert sensor.attributes["device_class"] == DEVICE_CLASS_HUMIDITY
assert sensor.attributes["unit_of_measurement"] == "%"
@@ -291,7 +291,7 @@ async def test_async_update(hass):
assert score_sensor.state == "79"
assert hass.states.get("sensor.awair_temperature").state == "23.4"
- assert hass.states.get("sensor.awair_humidity").state == "33.73"
+ assert hass.states.get("sensor.awair_humidity").state == "33.7"
assert hass.states.get("sensor.awair_co2").state == "613"
assert hass.states.get("sensor.awair_voc").state == "1013"
assert hass.states.get("sensor.awair_pm2_5").state == "7.2"
diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py
index 4440651d089e18..fa1d8cf8b9b5e7 100644
--- a/tests/components/cloud/test_client.py
+++ b/tests/components/cloud/test_client.py
@@ -2,9 +2,12 @@
from unittest.mock import patch, MagicMock
from aiohttp import web
+import jwt
import pytest
+from homeassistant.core import State
from homeassistant.setup import async_setup_component
+from homeassistant.components.cloud import DOMAIN
from homeassistant.components.cloud.const import (
PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE)
from tests.components.alexa import test_smart_home as test_alexa
@@ -19,6 +22,25 @@ def mock_cloud():
return MagicMock(subscription_expired=False)
+@pytest.fixture
+async def mock_cloud_setup(hass):
+ """Set up the cloud."""
+ with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()):
+ assert await async_setup_component(hass, 'cloud', {
+ 'cloud': {}
+ })
+
+
+@pytest.fixture
+def mock_cloud_login(hass, mock_cloud_setup):
+ """Mock cloud is logged in."""
+ hass.data[DOMAIN].id_token = jwt.encode({
+ 'email': 'hello@home-assistant.io',
+ 'custom:sub-exp': '2018-01-03',
+ 'cognito:username': 'abcdefghjkl',
+ }, 'test')
+
+
async def test_handler_alexa(hass):
"""Test handler Alexa."""
hass.states.async_set(
@@ -197,3 +219,35 @@ async def handler(hass, webhook_id, request):
assert await received[0].json() == {
'hello': 'world'
}
+
+
+async def test_google_config_expose_entity(
+ hass, mock_cloud_setup, mock_cloud_login):
+ """Test Google config exposing entity method uses latest config."""
+ cloud_client = hass.data[DOMAIN].client
+ state = State('light.kitchen', 'on')
+
+ assert cloud_client.google_config.should_expose(state)
+
+ await cloud_client.prefs.async_update_google_entity_config(
+ entity_id='light.kitchen',
+ should_expose=False,
+ )
+
+ assert not cloud_client.google_config.should_expose(state)
+
+
+async def test_google_config_should_2fa(
+ hass, mock_cloud_setup, mock_cloud_login):
+ """Test Google config disabling 2FA method uses latest config."""
+ cloud_client = hass.data[DOMAIN].client
+ state = State('light.kitchen', 'on')
+
+ assert cloud_client.google_config.should_2fa(state)
+
+ await cloud_client.prefs.async_update_google_entity_config(
+ entity_id='light.kitchen',
+ disable_2fa=True,
+ )
+
+ assert not cloud_client.google_config.should_2fa(state)
diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py
index 4aebc5679a0fd8..5ccaba14be639f 100644
--- a/tests/components/cloud/test_http_api.py
+++ b/tests/components/cloud/test_http_api.py
@@ -7,10 +7,13 @@
from hass_nabucasa.auth import Unauthenticated, UnknownError
from hass_nabucasa.const import STATE_CONNECTED
+from homeassistant.core import State
from homeassistant.auth.providers import trusted_networks as tn_auth
from homeassistant.components.cloud.const import (
PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN,
DOMAIN)
+from homeassistant.components.google_assistant.helpers import (
+ GoogleEntity, Config)
from tests.common import mock_coro
@@ -32,7 +35,8 @@ def mock_cloud_login(hass, setup_api):
"""Mock cloud is logged in."""
hass.data[DOMAIN].id_token = jwt.encode({
'email': 'hello@home-assistant.io',
- 'custom:sub-exp': '2018-01-03'
+ 'custom:sub-exp': '2018-01-03',
+ 'cognito:username': 'abcdefghjkl',
}, 'test')
@@ -349,7 +353,15 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
'logged_in': True,
'email': 'hello@home-assistant.io',
'cloud': 'connected',
- 'prefs': mock_cloud_fixture,
+ 'prefs': {
+ 'alexa_enabled': True,
+ 'cloud_user': None,
+ 'cloudhooks': {},
+ 'google_enabled': True,
+ 'google_entity_configs': {},
+ 'google_secure_devices_pin': None,
+ 'remote_enabled': False,
+ },
'alexa_entities': {
'include_domains': [],
'include_entities': ['light.kitchen', 'switch.ac'],
@@ -363,7 +375,6 @@ async def test_websocket_status(hass, hass_ws_client, mock_cloud_fixture,
'exclude_domains': [],
'exclude_entities': [],
},
- 'google_domains': ['light'],
'remote_domain': None,
'remote_connected': False,
'remote_certificate': None,
@@ -689,3 +700,52 @@ async def test_enabling_remote_trusted_networks_other(
assert cloud.client.remote_autostart
assert len(mock_connect.mock_calls) == 1
+
+
+async def test_list_google_entities(
+ hass, hass_ws_client, setup_api, mock_cloud_login):
+ """Test that we can list Google entities."""
+ client = await hass_ws_client(hass)
+ entity = GoogleEntity(hass, Config(lambda *_: False), State(
+ 'light.kitchen', 'on'
+ ))
+ with patch('homeassistant.components.google_assistant.helpers'
+ '.async_get_entities', return_value=[entity]):
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/google_assistant/entities',
+ })
+ response = await client.receive_json()
+
+ assert response['success']
+ assert len(response['result']) == 1
+ assert response['result'][0] == {
+ 'entity_id': 'light.kitchen',
+ 'might_2fa': False,
+ 'traits': ['action.devices.traits.OnOff'],
+ }
+
+
+async def test_update_google_entity(
+ hass, hass_ws_client, setup_api, mock_cloud_login):
+ """Test that we can update config of a Google entity."""
+ client = await hass_ws_client(hass)
+ await client.send_json({
+ 'id': 5,
+ 'type': 'cloud/google_assistant/entities/update',
+ 'entity_id': 'light.kitchen',
+ 'should_expose': False,
+ 'override_name': 'updated name',
+ 'aliases': ['lefty', 'righty'],
+ 'disable_2fa': False,
+ })
+ response = await client.receive_json()
+
+ assert response['success']
+ prefs = hass.data[DOMAIN].client.prefs
+ assert prefs.google_entity_configs['light.kitchen'] == {
+ 'should_expose': False,
+ 'override_name': 'updated name',
+ 'aliases': ['lefty', 'righty'],
+ 'disable_2fa': False,
+ }
diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py
index 1aee53f43c2746..89629a07cfa47a 100644
--- a/tests/components/deconz/test_binary_sensor.py
+++ b/tests/components/deconz/test_binary_sensor.py
@@ -1,6 +1,8 @@
"""deCONZ binary sensor platform tests."""
from unittest.mock import Mock, patch
+from tests.common import mock_coro
+
from homeassistant import config_entries
from homeassistant.components import deconz
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -8,8 +10,6 @@
import homeassistant.components.binary_sensor as binary_sensor
-from tests.common import mock_coro
-
SENSOR = {
"1": {
@@ -104,6 +104,7 @@ async def test_add_new_sensor(hass):
sensor = Mock()
sensor.name = 'name'
sensor.type = 'ZHAPresence'
+ sensor.BINARY = True
sensor.register_async_callback = Mock()
async_dispatcher_send(
hass, gateway.async_event_new_device('sensor'), [sensor])
diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py
index a5f4d2bb79b6e2..407f5d92871008 100644
--- a/tests/components/deconz/test_climate.py
+++ b/tests/components/deconz/test_climate.py
@@ -1,4 +1,5 @@
"""deCONZ climate platform tests."""
+from copy import deepcopy
from unittest.mock import Mock, patch
import asynctest
@@ -18,9 +19,9 @@
"id": "Climate 1 id",
"name": "Climate 1 name",
"type": "ZHAThermostat",
- "state": {"on": True, "temperature": 2260},
+ "state": {"on": True, "temperature": 2260, "valve": 30},
"config": {"battery": 100, "heatsetpoint": 2200, "mode": "auto",
- "offset": 10, "reachable": True, "valve": 30},
+ "offset": 10, "reachable": True},
"uniqueid": "00:00:00:00:00:00:00:00-00"
},
"2": {
@@ -97,7 +98,7 @@ async def test_no_sensors(hass):
async def test_climate_devices(hass):
"""Test successful creation of sensor entities."""
- gateway = await setup_gateway(hass, {"sensors": SENSOR})
+ gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)})
assert "climate.climate_1_name" in gateway.deconz_ids
assert "sensor.sensor_2_name" not in gateway.deconz_ids
assert len(hass.states.async_all()) == 1
@@ -138,7 +139,7 @@ async def test_climate_devices(hass):
async def test_verify_state_update(hass):
"""Test that state update properly."""
- gateway = await setup_gateway(hass, {"sensors": SENSOR})
+ gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)})
assert "climate.climate_1_name" in gateway.deconz_ids
thermostat = hass.states.get('climate.climate_1_name')
@@ -149,7 +150,7 @@ async def test_verify_state_update(hass):
"e": "changed",
"r": "sensors",
"id": "1",
- "config": {"on": False}
+ "state": {"on": False}
}
gateway.api.async_event_handler(state_update)
@@ -158,6 +159,8 @@ async def test_verify_state_update(hass):
thermostat = hass.states.get('climate.climate_1_name')
assert thermostat.state == 'off'
+ assert gateway.api.sensors['1'].changed_keys == \
+ {'state', 'r', 't', 'on', 'e', 'id'}
async def test_add_new_climate_device(hass):
diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py
index ada506be4285f5..2b9f2c013b0e1b 100644
--- a/tests/components/deconz/test_config_flow.py
+++ b/tests/components/deconz/test_config_flow.py
@@ -43,7 +43,7 @@ async def test_flow_works(hass, aioclient_mock):
async def test_user_step_bridge_discovery_fails(hass, aioclient_mock):
"""Test config flow works when discovery fails."""
- with patch('pydeconz.utils.async_discovery',
+ with patch('homeassistant.components.deconz.config_flow.async_discovery',
side_effect=asyncio.TimeoutError):
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
@@ -158,8 +158,9 @@ async def test_link_no_api_key(hass):
config_flow.CONF_PORT: 80
}
- with patch('pydeconz.utils.async_get_api_key',
- side_effect=pydeconz.errors.ResponseError):
+ with patch(
+ 'homeassistant.components.deconz.config_flow.async_get_api_key',
+ side_effect=pydeconz.errors.ResponseError):
result = await flow.async_step_link(user_input={})
assert result['type'] == 'form'
@@ -167,22 +168,38 @@ async def test_link_no_api_key(hass):
assert result['errors'] == {'base': 'no_key'}
-async def test_bridge_discovery(hass):
- """Test a bridge being discovered."""
+async def test_bridge_ssdp_discovery(hass):
+ """Test a bridge being discovered over ssdp."""
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
data={
config_flow.CONF_HOST: '1.2.3.4',
config_flow.CONF_PORT: 80,
- config_flow.CONF_SERIAL: 'id',
+ config_flow.ATTR_SERIAL: 'id',
+ config_flow.ATTR_MANUFACTURERURL:
+ config_flow.DECONZ_MANUFACTURERURL
},
- context={'source': 'discovery'}
+ context={'source': 'ssdp'}
)
assert result['type'] == 'form'
assert result['step_id'] == 'link'
+async def test_bridge_ssdp_discovery_not_deconz_bridge(hass):
+ """Test a non deconz bridge being discovered over ssdp."""
+ result = await hass.config_entries.flow.async_init(
+ config_flow.DOMAIN,
+ data={
+ config_flow.ATTR_MANUFACTURERURL: 'not deconz bridge'
+ },
+ context={'source': 'ssdp'}
+ )
+
+ assert result['type'] == 'abort'
+ assert result['reason'] == 'not_deconz_bridge'
+
+
async def test_bridge_discovery_update_existing_entry(hass):
"""Test if a discovered bridge has already been configured."""
entry = MockConfigEntry(domain=config_flow.DOMAIN, data={
@@ -194,9 +211,11 @@ async def test_bridge_discovery_update_existing_entry(hass):
config_flow.DOMAIN,
data={
config_flow.CONF_HOST: 'mock-deconz',
- config_flow.CONF_SERIAL: 'id',
+ config_flow.ATTR_SERIAL: 'id',
+ config_flow.ATTR_MANUFACTURERURL:
+ config_flow.DECONZ_MANUFACTURERURL
},
- context={'source': 'discovery'}
+ context={'source': 'ssdp'}
)
assert result['type'] == 'abort'
@@ -275,8 +294,9 @@ async def test_create_entry_timeout(hass, aioclient_mock):
config_flow.CONF_API_KEY: '1234567890ABCDEF'
}
- with patch('pydeconz.utils.async_get_bridgeid',
- side_effect=asyncio.TimeoutError):
+ with patch(
+ 'homeassistant.components.deconz.config_flow.async_get_bridgeid',
+ side_effect=asyncio.TimeoutError):
result = await flow._create_entry()
assert result['type'] == 'abort'
diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py
index 6006ff668986ab..46107e1dd6cd99 100644
--- a/tests/components/deconz/test_gateway.py
+++ b/tests/components/deconz/test_gateway.py
@@ -223,7 +223,8 @@ async def test_update_event():
remote.name = 'Name'
event = gateway.DeconzEvent(hass, remote)
- event.async_update_callback({'state': True})
+ remote.changed_keys = {'state': True}
+ event.async_update_callback()
assert len(hass.bus.async_fire.mock_calls) == 1
diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py
index 41bb7b362f5f86..7ed8bef093e496 100644
--- a/tests/components/deconz/test_sensor.py
+++ b/tests/components/deconz/test_sensor.py
@@ -1,6 +1,8 @@
"""deCONZ sensor platform tests."""
from unittest.mock import Mock, patch
+from tests.common import mock_coro
+
from homeassistant import config_entries
from homeassistant.components import deconz
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -8,8 +10,6 @@
import homeassistant.components.sensor as sensor
-from tests.common import mock_coro
-
SENSOR = {
"1": {
@@ -142,6 +142,7 @@ async def test_add_new_sensor(hass):
sensor = Mock()
sensor.name = 'name'
sensor.type = 'ZHATemperature'
+ sensor.BINARY = False
sensor.register_async_callback = Mock()
async_dispatcher_send(
hass, gateway.async_event_new_device('sensor'), [sensor])
diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py
index 8e2766a857b2bb..5aacf06aa66a2a 100644
--- a/tests/components/default_config/test_init.py
+++ b/tests/components/default_config/test_init.py
@@ -9,9 +9,9 @@
@pytest.fixture(autouse=True)
-def aiozeroconf_mock():
- """Mock aiozeroconf."""
- with MockDependency('aiozeroconf') as mocked_zeroconf:
+def zeroconf_mock():
+ """Mock zeroconf."""
+ with MockDependency('zeroconf') as mocked_zeroconf:
mocked_zeroconf.Zeroconf.return_value.register_service \
.return_value = mock_coro(True)
yield
diff --git a/tests/components/demo/test_remote.py b/tests/components/demo/test_remote.py
index c68e34ddc18e5d..e5db98c381b6c0 100644
--- a/tests/components/demo/test_remote.py
+++ b/tests/components/demo/test_remote.py
@@ -48,5 +48,8 @@ def test_methods(self):
common.send_command(self.hass, 'test', entity_id=ENTITY_ID)
self.hass.block_till_done()
state = self.hass.states.get(ENTITY_ID)
- assert state.attributes == \
- {'friendly_name': 'Remote One', 'last_command_sent': 'test'}
+ assert state.attributes == {
+ 'friendly_name': 'Remote One',
+ 'last_command_sent': 'test',
+ 'supported_features': 0
+ }
diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py
index 5aeb9d1c045c81..f991c36c4f000e 100644
--- a/tests/components/esphome/test_config_flow.py
+++ b/tests/components/esphome/test_config_flow.py
@@ -4,7 +4,7 @@
import pytest
-from homeassistant.components.esphome import config_flow
+from homeassistant.components.esphome import config_flow, DATA_KEY
from tests.common import mock_coro, MockConfigEntry
MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"])
@@ -254,3 +254,30 @@ async def test_discovery_already_configured_ip(hass, mock_client):
result = await flow.async_step_zeroconf(user_input=service_info)
assert result['type'] == 'abort'
assert result['reason'] == 'already_configured'
+
+
+async def test_discovery_already_configured_name(hass, mock_client):
+ """Test discovery aborts if already configured via name."""
+ entry = MockConfigEntry(
+ domain='esphome',
+ data={'host': '192.168.43.183', 'port': 6053, 'password': ''}
+ )
+ entry.add_to_hass(hass)
+ mock_entry_data = MagicMock()
+ mock_entry_data.device_info.name = 'test8266'
+ hass.data[DATA_KEY] = {
+ entry.entry_id: mock_entry_data,
+ }
+
+ flow = _setup_flow_handler(hass)
+ service_info = {
+ 'host': '192.168.43.183',
+ 'port': 6053,
+ 'hostname': 'test8266.local.',
+ 'properties': {
+ "address": "test8266.local"
+ }
+ }
+ result = await flow.async_step_zeroconf(user_input=service_info)
+ assert result['type'] == 'abort'
+ assert result['reason'] == 'already_configured'
diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py
index ee10b986697f99..c362499db152a4 100644
--- a/tests/components/frontend/test_init.py
+++ b/tests/components/frontend/test_init.py
@@ -8,10 +8,10 @@
from homeassistant.setup import async_setup_component
from homeassistant.components.frontend import (
DOMAIN, CONF_JS_VERSION, CONF_THEMES, CONF_EXTRA_HTML_URL,
- CONF_EXTRA_HTML_URL_ES5)
+ CONF_EXTRA_HTML_URL_ES5, EVENT_PANELS_UPDATED)
from homeassistant.components.websocket_api.const import TYPE_RESULT
-from tests.common import mock_coro
+from tests.common import mock_coro, async_capture_events
CONFIG_THEMES = {
@@ -232,12 +232,21 @@ def test_extra_urls(mock_http_client_with_urls, mock_onboarded):
assert text.find('href="https://domain.com/my_extra_url.html"') >= 0
-async def test_get_panels(hass, hass_ws_client):
+async def test_get_panels(hass, hass_ws_client, mock_http_client):
"""Test get_panels command."""
- await async_setup_component(hass, 'frontend', {})
- await hass.components.frontend.async_register_built_in_panel(
+ events = async_capture_events(hass, EVENT_PANELS_UPDATED)
+
+ resp = await mock_http_client.get('/map')
+ assert resp.status == 404
+
+ hass.components.frontend.async_register_built_in_panel(
'map', 'Map', 'mdi:tooltip-account', require_admin=True)
+ resp = await mock_http_client.get('/map')
+ assert resp.status == 200
+
+ assert len(events) == 1
+
client = await hass_ws_client(hass)
await client.send_json({
'id': 5,
@@ -255,14 +264,21 @@ async def test_get_panels(hass, hass_ws_client):
assert msg['result']['map']['title'] == 'Map'
assert msg['result']['map']['require_admin'] is True
+ hass.components.frontend.async_remove_panel('map')
+
+ resp = await mock_http_client.get('/map')
+ assert resp.status == 404
+
+ assert len(events) == 2
+
async def test_get_panels_non_admin(hass, hass_ws_client, hass_admin_user):
"""Test get_panels command."""
hass_admin_user.groups = []
await async_setup_component(hass, 'frontend', {})
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'map', 'Map', 'mdi:tooltip-account', require_admin=True)
- await hass.components.frontend.async_register_built_in_panel(
+ hass.components.frontend.async_register_built_in_panel(
'history', 'History', 'mdi:history')
client = await hass_ws_client(hass)
diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py
index 718eb259db5596..884ef125eabc30 100644
--- a/tests/components/geofency/test_init.py
+++ b/tests/components/geofency/test_init.py
@@ -5,13 +5,12 @@
import pytest
from homeassistant import data_entry_flow
-from homeassistant.components import zone, geofency
+from homeassistant.components import zone
from homeassistant.components.geofency import (
- CONF_MOBILE_BEACONS, DOMAIN, TRACKER_UPDATE)
+ CONF_MOBILE_BEACONS, DOMAIN)
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
from homeassistant.util import slugify
@@ -217,6 +216,12 @@ async def test_gps_enter_and_exit_home(hass, geofency_client, webhook_id):
'device_tracker', device_name)).attributes['longitude']
assert NOT_HOME_LONGITUDE == current_longitude
+ 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_beacon_enter_and_exit_home(hass, geofency_client, webhook_id):
"""Test iBeacon based zone enter and exit - a.k.a stationary iBeacon."""
@@ -285,9 +290,6 @@ async def test_beacon_enter_and_exit_car(hass, geofency_client, webhook_id):
assert STATE_HOME == state_name
-@pytest.mark.xfail(
- reason='The device_tracker component does not support unloading yet.'
-)
async def test_load_unload_entry(hass, geofency_client, webhook_id):
"""Test that the appropriate dispatch signals are added and removed."""
url = '/api/webhook/{}'.format(webhook_id)
@@ -297,13 +299,23 @@ async def test_load_unload_entry(hass, geofency_client, webhook_id):
await hass.async_block_till_done()
assert req.status == HTTP_OK
device_name = slugify(GPS_ENTER_HOME['device'])
- state_name = hass.states.get('{}.{}'.format(
- 'device_tracker', device_name)).state
- assert STATE_HOME == state_name
- assert len(hass.data[DATA_DISPATCHER][TRACKER_UPDATE]) == 1
+ state_1 = hass.states.get('{}.{}'.format('device_tracker', device_name))
+ assert STATE_HOME == state_1.state
+ assert len(hass.data[DOMAIN]['devices']) == 1
entry = hass.config_entries.async_entries(DOMAIN)[0]
- assert await geofency.async_unload_entry(hass, entry)
+ assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
- assert not hass.data[DATA_DISPATCHER][TRACKER_UPDATE]
+ assert len(hass.data[DOMAIN]['devices']) == 0
+
+ assert await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ state_2 = hass.states.get('{}.{}'.format('device_tracker', device_name))
+ assert state_2 is not None
+ assert state_1 is not state_2
+
+ assert STATE_HOME == state_2.state
+ assert state_2.attributes['latitude'] == HOME_LATITUDE
+ assert state_2.attributes['longitude'] == HOME_LONGITUDE
diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py
index 519a55fbc00e1f..a65387d48a2026 100644
--- a/tests/components/google_assistant/test_smart_home.py
+++ b/tests/components/google_assistant/test_smart_home.py
@@ -530,34 +530,6 @@ async def test_unavailable_state_doesnt_sync(hass):
}
-async def test_empty_name_doesnt_sync(hass):
- """Test that an entity with empty name does not sync over."""
- light = DemoLight(
- None, ' ',
- state=False,
- )
- light.hass = hass
- light.entity_id = 'light.demo_light'
- await light.async_update_ha_state()
-
- result = await sh.async_handle_message(
- hass, BASIC_CONFIG, 'test-agent',
- {
- "requestId": REQ_ID,
- "inputs": [{
- "intent": "action.devices.SYNC"
- }]
- })
-
- assert result == {
- 'requestId': REQ_ID,
- 'payload': {
- 'agentUserId': 'test-agent',
- 'devices': []
- }
- }
-
-
@pytest.mark.parametrize("device_class,google_type", [
('non_existing_class', 'action.devices.types.SWITCH'),
('switch', 'action.devices.types.SWITCH'),
diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py
index 5e6dadf14f4c02..6b1b6a7c9f401b 100644
--- a/tests/components/google_assistant/test_trait.py
+++ b/tests/components/google_assistant/test_trait.py
@@ -14,6 +14,7 @@
media_player,
scene,
script,
+ sensor,
switch,
vacuum,
group,
@@ -843,6 +844,8 @@ async def test_lock_unlock_lock(hass):
assert helpers.get_google_type(lock.DOMAIN, None) is not None
assert trait.LockUnlockTrait.supported(lock.DOMAIN, lock.SUPPORT_OPEN,
None)
+ assert trait.LockUnlockTrait.might_2fa(lock.DOMAIN, lock.SUPPORT_OPEN,
+ None)
trt = trait.LockUnlockTrait(hass,
State('lock.front_door', lock.STATE_LOCKED),
@@ -922,6 +925,13 @@ async def test_lock_unlock_unlock(hass):
assert len(calls) == 1
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
+ # Test with 2FA override
+ with patch('homeassistant.components.google_assistant.helpers'
+ '.Config.should_2fa', return_value=False):
+ await trt.execute(
+ trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False}, {})
+ assert len(calls) == 2
+
async def test_fan_speed(hass):
"""Test FanSpeed trait speed control support for fan domain."""
@@ -1216,6 +1226,8 @@ async def test_openclose_cover_secure(hass, device_class):
assert helpers.get_google_type(cover.DOMAIN, device_class) is not None
assert trait.OpenCloseTrait.supported(
cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class)
+ assert trait.OpenCloseTrait.might_2fa(
+ cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class)
trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, {
ATTR_DEVICE_CLASS: device_class,
@@ -1369,3 +1381,35 @@ async def test_volume_media_player_relative(hass):
ATTR_ENTITY_ID: 'media_player.bla',
media_player.ATTR_MEDIA_VOLUME_LEVEL: .5
}
+
+
+async def test_temperature_setting_sensor(hass):
+ """Test TemperatureSetting trait support for temperature sensor."""
+ assert helpers.get_google_type(sensor.DOMAIN,
+ sensor.DEVICE_CLASS_TEMPERATURE) is not None
+ assert not trait.TemperatureSettingTrait.supported(
+ sensor.DOMAIN,
+ 0,
+ sensor.DEVICE_CLASS_HUMIDITY
+ )
+ assert trait.TemperatureSettingTrait.supported(
+ sensor.DOMAIN,
+ 0,
+ sensor.DEVICE_CLASS_TEMPERATURE
+ )
+
+ hass.config.units.temperature_unit = TEMP_FAHRENHEIT
+
+ trt = trait.TemperatureSettingTrait(hass, State('sensor.test', "70", {
+ ATTR_DEVICE_CLASS: sensor.DEVICE_CLASS_TEMPERATURE,
+ }), BASIC_CONFIG)
+
+ assert trt.sync_attributes() == {
+ 'queryOnlyTemperatureSetting': True,
+ 'thermostatTemperatureUnit': 'F',
+ }
+
+ assert trt.query_attributes() == {
+ 'thermostatTemperatureAmbient': 21.1
+ }
+ hass.config.units.temperature_unit = TEMP_CELSIUS
diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py
index 2cffa86f39358c..dbc283895fcb0c 100644
--- a/tests/components/gpslogger/test_init.py
+++ b/tests/components/gpslogger/test_init.py
@@ -140,6 +140,12 @@ async def test_enter_and_exit(hass, gpslogger_client, webhook_id):
data['device'])).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, gpslogger_client, webhook_id):
"""Test when additional attributes are present."""
@@ -172,6 +178,33 @@ async def test_enter_with_attrs(hass, gpslogger_client, webhook_id):
assert state.attributes['provider'] == 'gps'
assert state.attributes['activity'] == 'running'
+ data = {
+ 'latitude': HOME_LATITUDE,
+ 'longitude': HOME_LONGITUDE,
+ 'device': '123',
+ 'accuracy': 123,
+ 'battery': 23,
+ 'speed': 23,
+ 'direction': 123,
+ 'altitude': 123,
+ 'provider': 'gps',
+ 'activity': 'idle'
+ }
+
+ req = await gpslogger_client.post(url, data=data)
+ await hass.async_block_till_done()
+ assert req.status == HTTP_OK
+ state = hass.states.get('{}.{}'.format(DEVICE_TRACKER_DOMAIN,
+ data['device']))
+ 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['direction'] == 123
+ assert state.attributes['altitude'] == 123
+ assert state.attributes['provider'] == 'gps'
+ assert state.attributes['activity'] == 'idle'
+
@pytest.mark.xfail(
reason='The device_tracker component does not support unloading yet.'
diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py
index 87482f8e92c35f..34b6474c6e9e18 100644
--- a/tests/components/homekit_controller/common.py
+++ b/tests/components/homekit_controller/common.py
@@ -277,7 +277,7 @@ async def device_config_changed(hass, accessories):
flow = config_flow.HomekitControllerFlowHandler()
flow.hass = hass
flow.context = {}
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'abort'
assert result['reason'] == 'already_configured'
diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py
index 9c869809544a8a..b5f923dd55ee1b 100644
--- a/tests/components/homekit_controller/test_config_flow.py
+++ b/tests/components/homekit_controller/test_config_flow.py
@@ -47,6 +47,15 @@ def _setup_flow_handler(hass):
return flow
+async def _setup_flow_zeroconf(hass, discovery_info):
+ result = await hass.config_entries.flow.async_init(
+ 'homekit_controller',
+ context={'source': 'zeroconf'},
+ data=discovery_info,
+ )
+ return result
+
+
async def test_discovery_works(hass):
"""Test a device being discovered."""
discovery_info = {
@@ -64,10 +73,13 @@ async def test_discovery_works(hass):
flow = _setup_flow_handler(hass)
# Device is discovered
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# User initiates pairing - device enters pairing mode and displays code
result = await flow.async_step_pair({})
@@ -119,10 +131,13 @@ async def test_discovery_works_upper_case(hass):
flow = _setup_flow_handler(hass)
# Device is discovered
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# User initiates pairing - device enters pairing mode and displays code
result = await flow.async_step_pair({})
@@ -172,10 +187,13 @@ async def test_discovery_works_missing_csharp(hass):
flow = _setup_flow_handler(hass)
# Device is discovered
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# User initiates pairing - device enters pairing mode and displays code
result = await flow.async_step_pair({})
@@ -210,6 +228,29 @@ async def test_discovery_works_missing_csharp(hass):
assert result['data'] == pairing.pairing_data
+async def test_abort_duplicate_flow(hass):
+ """Already paired."""
+ discovery_info = {
+ 'name': 'TestDevice',
+ 'host': '127.0.0.1',
+ 'port': 8080,
+ 'properties': {
+ 'md': 'TestDevice',
+ 'id': '00:00:00:00:00:00',
+ 'c#': 1,
+ 'sf': 1,
+ }
+ }
+
+ result = await _setup_flow_zeroconf(hass, discovery_info)
+ assert result['type'] == 'form'
+ assert result['step_id'] == 'pair'
+
+ result = await _setup_flow_zeroconf(hass, discovery_info)
+ assert result['type'] == 'abort'
+ assert result['reason'] == 'already_in_progress'
+
+
async def test_pair_already_paired_1(hass):
"""Already paired."""
discovery_info = {
@@ -226,10 +267,13 @@ async def test_pair_already_paired_1(hass):
flow = _setup_flow_handler(hass)
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'abort'
assert result['reason'] == 'already_paired'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
async def test_discovery_ignored_model(hass):
@@ -248,10 +292,13 @@ async def test_discovery_ignored_model(hass):
flow = _setup_flow_handler(hass)
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'abort'
assert result['reason'] == 'ignored_model'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
async def test_discovery_invalid_config_entry(hass):
@@ -277,10 +324,13 @@ async def test_discovery_invalid_config_entry(hass):
flow = _setup_flow_handler(hass)
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# Discovery of a HKID that is in a pairable state but for which there is
# already a config entry - in that case the stale config entry is
@@ -311,10 +361,13 @@ async def test_discovery_already_configured(hass):
flow = _setup_flow_handler(hass)
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'abort'
assert result['reason'] == 'already_configured'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
assert conn.async_config_num_changed.call_count == 0
@@ -341,10 +394,13 @@ async def test_discovery_already_configured_config_change(hass):
flow = _setup_flow_handler(hass)
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'abort'
assert result['reason'] == 'already_configured'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
assert conn.async_refresh_entity_map.call_args == mock.call(2)
@@ -366,10 +422,13 @@ async def test_pair_unable_to_pair(hass):
flow = _setup_flow_handler(hass)
# Device is discovered
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# User initiates pairing - device enters pairing mode and displays code
result = await flow.async_step_pair({})
@@ -403,10 +462,13 @@ async def test_pair_abort_errors_on_start(hass, exception, expected):
flow = _setup_flow_handler(hass)
# Device is discovered
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# User initiates pairing - device refuses to enter pairing mode
with mock.patch.object(flow.controller, 'start_pairing') as start_pairing:
@@ -415,7 +477,10 @@ async def test_pair_abort_errors_on_start(hass, exception, expected):
assert result['type'] == 'abort'
assert result['reason'] == expected
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
@pytest.mark.parametrize("exception,expected", PAIRING_START_FORM_ERRORS)
@@ -436,10 +501,13 @@ async def test_pair_form_errors_on_start(hass, exception, expected):
flow = _setup_flow_handler(hass)
# Device is discovered
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# User initiates pairing - device refuses to enter pairing mode
with mock.patch.object(flow.controller, 'start_pairing') as start_pairing:
@@ -448,7 +516,10 @@ async def test_pair_form_errors_on_start(hass, exception, expected):
assert result['type'] == 'form'
assert result['errors']['pairing_code'] == expected
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
@pytest.mark.parametrize("exception,expected", PAIRING_FINISH_ABORT_ERRORS)
@@ -469,10 +540,13 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected):
flow = _setup_flow_handler(hass)
# Device is discovered
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# User initiates pairing - device enters pairing mode and displays code
result = await flow.async_step_pair({})
@@ -487,7 +561,10 @@ async def test_pair_abort_errors_on_finish(hass, exception, expected):
})
assert result['type'] == 'abort'
assert result['reason'] == expected
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
@pytest.mark.parametrize("exception,expected", PAIRING_FINISH_FORM_ERRORS)
@@ -508,10 +585,13 @@ async def test_pair_form_errors_on_finish(hass, exception, expected):
flow = _setup_flow_handler(hass)
# Device is discovered
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'form'
assert result['step_id'] == 'pair'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
# User initiates pairing - device enters pairing mode and displays code
result = await flow.async_step_pair({})
@@ -526,7 +606,10 @@ async def test_pair_form_errors_on_finish(hass, exception, expected):
})
assert result['type'] == 'form'
assert result['errors']['pairing_code'] == expected
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
async def test_import_works(hass):
@@ -738,12 +821,15 @@ async def test_parse_new_homekit_json(hass):
pairing_cls.return_value = pairing
with mock.patch('builtins.open', mock_open):
with mock.patch('os.path', mock_path):
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'create_entry'
assert result['title'] == 'TestDevice'
assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
async def test_parse_old_homekit_json(hass):
@@ -796,12 +882,15 @@ async def test_parse_old_homekit_json(hass):
with mock.patch('builtins.open', mock_open):
with mock.patch('os.path', mock_path):
with mock.patch('os.listdir', mock_listdir):
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
assert result['type'] == 'create_entry'
assert result['title'] == 'TestDevice'
assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
async def test_parse_overlapping_homekit_json(hass):
@@ -865,11 +954,14 @@ async def test_parse_overlapping_homekit_json(hass):
with mock.patch('builtins.open', side_effect=side_effects):
with mock.patch('os.path', mock_path):
with mock.patch('os.listdir', mock_listdir):
- result = await flow.async_step_discovery(discovery_info)
+ result = await flow.async_step_zeroconf(discovery_info)
await hass.async_block_till_done()
assert result['type'] == 'create_entry'
assert result['title'] == 'TestDevice'
assert result['data']['AccessoryPairingID'] == '00:00:00:00:00:00'
- assert flow.context == {'title_placeholders': {'name': 'TestDevice'}}
+ assert flow.context == {
+ 'hkid': '00:00:00:00:00:00',
+ 'title_placeholders': {'name': 'TestDevice'}
+ }
diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py
index e17fb105efe057..d9fa6c11309017 100644
--- a/tests/components/http/test_cors.py
+++ b/tests/components/http/test_cors.py
@@ -140,3 +140,15 @@ async def get(self, request):
hass.http.app._on_startup.freeze()
await hass.http.app.startup()
+
+
+async def test_cors_works_with_frontend(hass, hass_client):
+ """Test CORS works with the frontend."""
+ assert await async_setup_component(hass, 'frontend', {
+ 'http': {
+ 'cors_allowed_origins': ['http://home-assistant.io']
+ }
+ })
+ client = await hass_client()
+ resp = await client.get('/')
+ assert resp.status == 200
diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py
index 78b7ba0269c0fc..b7736e62390ea3 100644
--- a/tests/components/hue/test_config_flow.py
+++ b/tests/components/hue/test_config_flow.py
@@ -185,37 +185,53 @@ async def test_flow_link_unknown_host(hass):
}
-async def test_bridge_discovery(hass):
+async def test_bridge_ssdp(hass):
"""Test a bridge being discovered."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
+ flow.context = {}
with patch.object(config_flow, 'get_bridge',
side_effect=errors.AuthenticationRequired):
- result = await flow.async_step_discovery({
+ result = await flow.async_step_ssdp({
'host': '0.0.0.0',
- 'serial': '1234'
+ 'serial': '1234',
+ 'manufacturerURL': config_flow.HUE_MANUFACTURERURL
})
assert result['type'] == 'form'
assert result['step_id'] == 'link'
-async def test_bridge_discovery_emulated_hue(hass):
+async def test_bridge_ssdp_discover_other_bridge(hass):
+ """Test that discovery ignores other bridges."""
+ flow = config_flow.HueFlowHandler()
+ flow.hass = hass
+
+ result = await flow.async_step_ssdp({
+ 'manufacturerURL': 'http://www.notphilips.com'
+ })
+
+ assert result['type'] == 'abort'
+
+
+async def test_bridge_ssdp_emulated_hue(hass):
"""Test if discovery info is from an emulated hue instance."""
flow = config_flow.HueFlowHandler()
flow.hass = hass
+ flow.context = {}
- result = await flow.async_step_discovery({
+ result = await flow.async_step_ssdp({
'name': 'HASS Bridge',
'host': '0.0.0.0',
- 'serial': '1234'
+ 'serial': '1234',
+ 'manufacturerURL': config_flow.HUE_MANUFACTURERURL
})
assert result['type'] == 'abort'
-async def test_bridge_discovery_already_configured(hass):
+async def test_bridge_ssdp_already_configured(hass):
"""Test if a discovered bridge has already been configured."""
MockConfigEntry(domain='hue', data={
'host': '0.0.0.0'
@@ -223,10 +239,12 @@ async def test_bridge_discovery_already_configured(hass):
flow = config_flow.HueFlowHandler()
flow.hass = hass
+ flow.context = {}
- result = await flow.async_step_discovery({
+ result = await flow.async_step_ssdp({
'host': '0.0.0.0',
- 'serial': '1234'
+ 'serial': '1234',
+ 'manufacturerURL': config_flow.HUE_MANUFACTURERURL
})
assert result['type'] == 'abort'
diff --git a/tests/components/mobile_app/__init__.py b/tests/components/mobile_app/__init__.py
index 98c7a20b059d0e..9b37214d079cdb 100644
--- a/tests/components/mobile_app/__init__.py
+++ b/tests/components/mobile_app/__init__.py
@@ -1,74 +1 @@
-"""Tests for mobile_app component."""
-# pylint: disable=redefined-outer-name,unused-import
-import pytest
-
-from tests.common import mock_device_registry
-
-from homeassistant.setup import async_setup_component
-
-from homeassistant.components.mobile_app.const import (DATA_BINARY_SENSOR,
- DATA_DELETED_IDS,
- DATA_SENSOR,
- DOMAIN,
- STORAGE_KEY,
- STORAGE_VERSION)
-
-from .const import REGISTER, REGISTER_CLEARTEXT
-
-
-@pytest.fixture
-def registry(hass):
- """Return a configured device registry."""
- return mock_device_registry(hass)
-
-
-@pytest.fixture
-async def create_registrations(authed_api_client):
- """Return two new registrations."""
- enc_reg = await authed_api_client.post(
- '/api/mobile_app/registrations', json=REGISTER
- )
-
- assert enc_reg.status == 201
- enc_reg_json = await enc_reg.json()
-
- clear_reg = await authed_api_client.post(
- '/api/mobile_app/registrations', json=REGISTER_CLEARTEXT
- )
-
- assert clear_reg.status == 201
- clear_reg_json = await clear_reg.json()
-
- return (enc_reg_json, clear_reg_json)
-
-
-@pytest.fixture
-async def webhook_client(hass, aiohttp_client, hass_storage, hass_admin_user):
- """mobile_app mock client."""
- hass_storage[STORAGE_KEY] = {
- 'version': STORAGE_VERSION,
- 'data': {
- DATA_BINARY_SENSOR: {},
- DATA_DELETED_IDS: [],
- DATA_SENSOR: {}
- }
- }
-
- await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
- await hass.async_block_till_done()
- return await aiohttp_client(hass.http.app)
-
-
-@pytest.fixture
-async def authed_api_client(hass, hass_client):
- """Provide an authenticated client for mobile_app to use."""
- await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
- await hass.async_block_till_done()
- return await hass_client()
-
-
-@pytest.fixture(autouse=True)
-async def setup_ws(hass):
- """Configure the websocket_api component."""
- assert await async_setup_component(hass, 'websocket_api', {})
- await hass.async_block_till_done()
+"""Tests for the mobile app integration."""
diff --git a/tests/components/mobile_app/conftest.py b/tests/components/mobile_app/conftest.py
new file mode 100644
index 00000000000000..b20d164e6e69da
--- /dev/null
+++ b/tests/components/mobile_app/conftest.py
@@ -0,0 +1,60 @@
+"""Tests for mobile_app component."""
+# pylint: disable=redefined-outer-name,unused-import
+import pytest
+
+from tests.common import mock_device_registry
+
+from homeassistant.setup import async_setup_component
+
+from homeassistant.components.mobile_app.const import DOMAIN
+
+from .const import REGISTER, REGISTER_CLEARTEXT
+
+
+@pytest.fixture
+def registry(hass):
+ """Return a configured device registry."""
+ return mock_device_registry(hass)
+
+
+@pytest.fixture
+async def create_registrations(authed_api_client):
+ """Return two new registrations."""
+ enc_reg = await authed_api_client.post(
+ '/api/mobile_app/registrations', json=REGISTER
+ )
+
+ assert enc_reg.status == 201
+ enc_reg_json = await enc_reg.json()
+
+ clear_reg = await authed_api_client.post(
+ '/api/mobile_app/registrations', json=REGISTER_CLEARTEXT
+ )
+
+ assert clear_reg.status == 201
+ clear_reg_json = await clear_reg.json()
+
+ return (enc_reg_json, clear_reg_json)
+
+
+@pytest.fixture
+async def webhook_client(hass, aiohttp_client):
+ """mobile_app mock client."""
+ await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
+ await hass.async_block_till_done()
+ return await aiohttp_client(hass.http.app)
+
+
+@pytest.fixture
+async def authed_api_client(hass, hass_client):
+ """Provide an authenticated client for mobile_app to use."""
+ await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
+ await hass.async_block_till_done()
+ return await hass_client()
+
+
+@pytest.fixture(autouse=True)
+async def setup_ws(hass):
+ """Configure the websocket_api component."""
+ assert await async_setup_component(hass, 'websocket_api', {})
+ await hass.async_block_till_done()
diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py
new file mode 100644
index 00000000000000..53f9ad6f6dd3b6
--- /dev/null
+++ b/tests/components/mobile_app/test_device_tracker.py
@@ -0,0 +1,116 @@
+"""Test mobile app device tracker."""
+
+
+async def test_sending_location(hass, create_registrations, webhook_client):
+ """Test sending a location via a webhook."""
+ resp = await webhook_client.post(
+ '/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
+ json={
+ 'type': 'update_location',
+ 'data': {
+ 'gps': [10, 20],
+ 'gps_accuracy': 30,
+ 'battery': 40,
+ 'altitude': 50,
+ 'course': 60,
+ 'speed': 70,
+ 'vertical_accuracy': 80,
+ 'location_name': 'bar',
+ }
+ }
+ )
+
+ assert resp.status == 200
+ await hass.async_block_till_done()
+ state = hass.states.get('device_tracker.test_1_2')
+ assert state is not None
+ assert state.name == 'Test 1'
+ assert state.state == 'bar'
+ assert state.attributes['source_type'] == 'gps'
+ assert state.attributes['latitude'] == 10
+ assert state.attributes['longitude'] == 20
+ assert state.attributes['gps_accuracy'] == 30
+ assert state.attributes['battery_level'] == 40
+ assert state.attributes['altitude'] == 50
+ assert state.attributes['course'] == 60
+ assert state.attributes['speed'] == 70
+ assert state.attributes['vertical_accuracy'] == 80
+
+ resp = await webhook_client.post(
+ '/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
+ json={
+ 'type': 'update_location',
+ 'data': {
+ 'gps': [1, 2],
+ 'gps_accuracy': 3,
+ 'battery': 4,
+ 'altitude': 5,
+ 'course': 6,
+ 'speed': 7,
+ 'vertical_accuracy': 8,
+ }
+ }
+ )
+
+ assert resp.status == 200
+ await hass.async_block_till_done()
+ state = hass.states.get('device_tracker.test_1_2')
+ assert state is not None
+ assert state.state == 'not_home'
+ assert state.attributes['source_type'] == 'gps'
+ assert state.attributes['latitude'] == 1
+ assert state.attributes['longitude'] == 2
+ assert state.attributes['gps_accuracy'] == 3
+ assert state.attributes['battery_level'] == 4
+ assert state.attributes['altitude'] == 5
+ assert state.attributes['course'] == 6
+ assert state.attributes['speed'] == 7
+ assert state.attributes['vertical_accuracy'] == 8
+
+
+async def test_restoring_location(hass, create_registrations, webhook_client):
+ """Test sending a location via a webhook."""
+ resp = await webhook_client.post(
+ '/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
+ json={
+ 'type': 'update_location',
+ 'data': {
+ 'gps': [10, 20],
+ 'gps_accuracy': 30,
+ 'battery': 40,
+ 'altitude': 50,
+ 'course': 60,
+ 'speed': 70,
+ 'vertical_accuracy': 80,
+ 'location_name': 'bar',
+ }
+ }
+ )
+
+ assert resp.status == 200
+ await hass.async_block_till_done()
+ state_1 = hass.states.get('device_tracker.test_1_2')
+ assert state_1 is not None
+
+ config_entry = hass.config_entries.async_entries('mobile_app')[1]
+
+ # mobile app doesn't support unloading, so we just reload device tracker
+ await hass.config_entries.async_forward_entry_unload(config_entry,
+ 'device_tracker')
+ await hass.config_entries.async_forward_entry_setup(config_entry,
+ 'device_tracker')
+
+ state_2 = hass.states.get('device_tracker.test_1_2')
+ assert state_2 is not None
+
+ assert state_1 is not state_2
+ assert state_2.name == 'Test 1'
+ assert state_2.attributes['source_type'] == 'gps'
+ assert state_2.attributes['latitude'] == 10
+ assert state_2.attributes['longitude'] == 20
+ assert state_2.attributes['gps_accuracy'] == 30
+ assert state_2.attributes['battery_level'] == 40
+ assert state_2.attributes['altitude'] == 50
+ assert state_2.attributes['course'] == 60
+ assert state_2.attributes['speed'] == 70
+ assert state_2.attributes['vertical_accuracy'] == 80
diff --git a/tests/components/mobile_app/test_entity.py b/tests/components/mobile_app/test_entity.py
index e98307468d1f31..750c346cbc31ea 100644
--- a/tests/components/mobile_app/test_entity.py
+++ b/tests/components/mobile_app/test_entity.py
@@ -2,9 +2,6 @@
# pylint: disable=redefined-outer-name,unused-import
import logging
-from . import (authed_api_client, create_registrations, # noqa: F401
- webhook_client) # noqa: F401
-
_LOGGER = logging.getLogger(__name__)
diff --git a/tests/components/mobile_app/test_http_api.py b/tests/components/mobile_app/test_http_api.py
index dc51b850a16e50..80f01315f705e9 100644
--- a/tests/components/mobile_app/test_http_api.py
+++ b/tests/components/mobile_app/test_http_api.py
@@ -7,10 +7,9 @@
from homeassistant.setup import async_setup_component
from .const import REGISTER, RENDER_TEMPLATE
-from . import authed_api_client # noqa: F401
-async def test_registration(hass, hass_client): # noqa: F811
+async def test_registration(hass, hass_client):
"""Test that registrations happen."""
try:
# pylint: disable=unused-import
diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py
index 43eac28ec18421..cd5b0a5bbed619 100644
--- a/tests/components/mobile_app/test_webhook.py
+++ b/tests/components/mobile_app/test_webhook.py
@@ -11,17 +11,14 @@
from tests.common import async_mock_service
-from . import (authed_api_client, create_registrations, # noqa: F401
- webhook_client) # noqa: F401
-
from .const import (CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT,
RENDER_TEMPLATE, UPDATE)
_LOGGER = logging.getLogger(__name__)
-async def test_webhook_handle_render_template(create_registrations, # noqa: F401, F811, E501
- webhook_client): # noqa: F811
+async def test_webhook_handle_render_template(create_registrations,
+ webhook_client):
"""Test that we render templates properly."""
resp = await webhook_client.post(
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
@@ -34,7 +31,7 @@ async def test_webhook_handle_render_template(create_registrations, # noqa: F40
assert json == {'one': 'Hello world'}
-async def test_webhook_handle_call_services(hass, create_registrations, # noqa: F401, F811, E501
+async def test_webhook_handle_call_services(hass, create_registrations,
webhook_client): # noqa: E501 F811
"""Test that we call services properly."""
calls = async_mock_service(hass, 'test', 'mobile_app')
@@ -49,8 +46,8 @@ async def test_webhook_handle_call_services(hass, create_registrations, # noqa:
assert len(calls) == 1
-async def test_webhook_handle_fire_event(hass, create_registrations, # noqa: F401, F811, E501
- webhook_client): # noqa: F811
+async def test_webhook_handle_fire_event(hass, create_registrations,
+ webhook_client):
"""Test that we can fire events."""
events = []
@@ -76,7 +73,7 @@ def store_event(event):
async def test_webhook_update_registration(webhook_client, hass_client): # noqa: E501 F811
"""Test that a we can update an existing registration via webhook."""
- authed_api_client = await hass_client() # noqa: F811
+ authed_api_client = await hass_client()
register_resp = await authed_api_client.post(
'/api/mobile_app/registrations', json=REGISTER_CLEARTEXT
)
@@ -102,8 +99,8 @@ async def test_webhook_update_registration(webhook_client, hass_client): # noqa
assert CONF_SECRET not in update_json
-async def test_webhook_handle_get_zones(hass, create_registrations, # noqa: F401, F811, E501
- webhook_client): # noqa: F811
+async def test_webhook_handle_get_zones(hass, create_registrations,
+ webhook_client):
"""Test that we can get zones properly."""
await async_setup_component(hass, ZONE_DOMAIN, {
ZONE_DOMAIN: {
@@ -126,8 +123,8 @@ async def test_webhook_handle_get_zones(hass, create_registrations, # noqa: F40
assert json[0]['entity_id'] == 'zone.home'
-async def test_webhook_handle_get_config(hass, create_registrations, # noqa: F401, F811, E501
- webhook_client): # noqa: F811
+async def test_webhook_handle_get_config(hass, create_registrations,
+ webhook_client):
"""Test that we can get config properly."""
resp = await webhook_client.post(
'/api/webhook/{}'.format(create_registrations[1]['webhook_id']),
@@ -160,8 +157,8 @@ async def test_webhook_handle_get_config(hass, create_registrations, # noqa: F4
assert expected_dict == json
-async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F401, F811, E501
- create_registrations, # noqa: F401, F811, E501
+async def test_webhook_returns_error_incorrect_json(webhook_client,
+ create_registrations,
caplog): # noqa: E501 F811
"""Test that an error is returned when JSON is invalid."""
resp = await webhook_client.post(
@@ -175,8 +172,8 @@ async def test_webhook_returns_error_incorrect_json(webhook_client, # noqa: F40
assert 'invalid JSON' in caplog.text
-async def test_webhook_handle_decryption(webhook_client, # noqa: F811
- create_registrations): # noqa: F401, F811, E501
+async def test_webhook_handle_decryption(webhook_client,
+ create_registrations):
"""Test that we can encrypt/decrypt properly."""
try:
# pylint: disable=unused-import
@@ -221,8 +218,8 @@ async def test_webhook_handle_decryption(webhook_client, # noqa: F811
assert json.loads(decrypted_data) == {'one': 'Hello world'}
-async def test_webhook_requires_encryption(webhook_client, # noqa: F811
- create_registrations): # noqa: F401, F811, E501
+async def test_webhook_requires_encryption(webhook_client,
+ create_registrations):
"""Test that encrypted registrations only accept encrypted data."""
resp = await webhook_client.post(
'/api/webhook/{}'.format(create_registrations[0]['webhook_id']),
diff --git a/tests/components/mobile_app/test_websocket_api.py b/tests/components/mobile_app/test_websocket_api.py
index ee656159d2e784..20676731393a77 100644
--- a/tests/components/mobile_app/test_websocket_api.py
+++ b/tests/components/mobile_app/test_websocket_api.py
@@ -5,7 +5,6 @@
from homeassistant.const import CONF_WEBHOOK_ID
from homeassistant.setup import async_setup_component
-from . import authed_api_client, setup_ws, webhook_client # noqa: F401
from .const import (CALL_SERVICE, REGISTER)
@@ -45,7 +44,7 @@ async def test_webocket_get_user_registrations(hass, aiohttp_client,
async def test_webocket_delete_registration(hass, hass_client,
- hass_ws_client, webhook_client): # noqa: E501 F811
+ hass_ws_client, webhook_client):
"""Test delete_registration websocket command."""
authed_api_client = await hass_client() # noqa: F811
register_resp = await authed_api_client.post(
diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py
index 8beceb7d6606cb..f8bef17554093e 100644
--- a/tests/components/mqtt/test_legacy_vacuum.py
+++ b/tests/components/mqtt/test_legacy_vacuum.py
@@ -612,7 +612,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock):
vacuum.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
- 'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
@@ -629,7 +628,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog):
vacuum.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
- 'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
@@ -647,7 +645,6 @@ async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog):
vacuum.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
- 'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
@@ -766,7 +763,6 @@ async def test_entity_device_info_update(hass, mqtt_mock):
config = {
'platform': 'mqtt',
'name': 'Test 1',
- 'state_topic': 'test-topic',
'command_topic': 'test-command-topic',
'device': {
'identifiers': ['helloworld'],
diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py
index ecd63a1dcdc942..588a808ecfb45d 100644
--- a/tests/components/mqtt/test_state_vacuum.py
+++ b/tests/components/mqtt/test_state_vacuum.py
@@ -331,8 +331,7 @@ async def test_discovery_removal_vacuum(hass, mqtt_mock):
data = (
'{ "name": "Beer",'
- ' "command_topic": "test_topic",'
- ' "component": "state" }'
+ ' "command_topic": "test_topic"}'
)
async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config',
@@ -357,13 +356,11 @@ async def test_discovery_broken(hass, mqtt_mock, caplog):
data1 = (
'{ "name": "Beer",'
- ' "command_topic": "test_topic#",'
- ' "component": "state" }'
+ ' "command_topic": "test_topic#"}'
)
data2 = (
'{ "name": "Milk",'
- ' "command_topic": "test_topic",'
- ' "component": "state" }'
+ ' "command_topic": "test_topic"}'
)
async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config',
@@ -391,13 +388,11 @@ async def test_discovery_update_vacuum(hass, mqtt_mock):
data1 = (
'{ "name": "Beer",'
- ' "command_topic": "test_topic",'
- '"component": "state" }'
+ ' "command_topic": "test_topic"}'
)
data2 = (
'{ "name": "Milk",'
- ' "command_topic": "test_topic",'
- ' "component": "state"}'
+ ' "command_topic": "test_topic"}'
)
async_fire_mqtt_message(hass, 'homeassistant/vacuum/bla/config',
@@ -425,7 +420,6 @@ async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock):
vacuum.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
- 'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
@@ -442,7 +436,6 @@ async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog):
vacuum.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
- 'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
@@ -460,7 +453,6 @@ async def test_update_with_json_attrs_bad_json(hass, mqtt_mock, caplog):
vacuum.DOMAIN: {
'platform': 'mqtt',
'name': 'test',
- 'state_topic': 'test-topic',
'json_attributes_topic': 'attr-topic'
}
})
@@ -579,7 +571,6 @@ async def test_entity_device_info_update(hass, mqtt_mock):
config = {
'platform': 'mqtt',
'name': 'Test 1',
- 'state_topic': 'test-topic',
'command_topic': 'test-command-topic',
'device': {
'identifiers': ['helloworld'],
diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py
index b81f434a2c131b..7d8d48de586c81 100644
--- a/tests/components/owntracks/test_device_tracker.py
+++ b/tests/components/owntracks/test_device_tracker.py
@@ -1491,3 +1491,47 @@ async def test_region_mapping(hass, setup_comp):
await send_message(hass, EVENT_TOPIC, message)
assert_location_state(hass, 'inner')
+
+
+async def test_restore_state(hass, hass_client):
+ """Test that we can restore state."""
+ entry = MockConfigEntry(domain='owntracks', data={
+ 'webhook_id': 'owntracks_test',
+ 'secret': 'abcd',
+ })
+ entry.add_to_hass(hass)
+
+ await hass.config_entries.async_setup(entry.entry_id)
+ await hass.async_block_till_done()
+
+ client = await hass_client()
+ resp = await client.post(
+ '/api/webhook/owntracks_test',
+ json=LOCATION_MESSAGE,
+ headers={
+ 'X-Limit-u': 'Paulus',
+ 'X-Limit-d': 'Pixel',
+ }
+ )
+ assert resp.status == 200
+ await hass.async_block_till_done()
+
+ state_1 = hass.states.get('device_tracker.paulus_pixel')
+ assert state_1 is not None
+
+ await hass.config_entries.async_reload(entry.entry_id)
+ await hass.async_block_till_done()
+
+ state_2 = hass.states.get('device_tracker.paulus_pixel')
+ assert state_2 is not None
+
+ assert state_1 is not state_2
+
+ assert state_1.state == state_2.state
+ assert state_1.name == state_2.name
+ assert state_1.attributes['latitude'] == state_2.attributes['latitude']
+ assert state_1.attributes['longitude'] == state_2.attributes['longitude']
+ assert state_1.attributes['battery_level'] == \
+ state_2.attributes['battery_level']
+ assert state_1.attributes['source_type'] == \
+ state_2.attributes['source_type']
diff --git a/tests/components/owntracks/test_init.py b/tests/components/owntracks/test_init.py
index fafe9678e78e07..b662bbcd6bdb79 100644
--- a/tests/components/owntracks/test_init.py
+++ b/tests/components/owntracks/test_init.py
@@ -4,7 +4,7 @@
import pytest
from homeassistant.setup import async_setup_component
-
+from homeassistant.components import owntracks
from tests.common import mock_component, MockConfigEntry
MINIMAL_LOCATION_MESSAGE = {
@@ -160,3 +160,24 @@ def test_returns_error_missing_device(mock_client):
json = yield from resp.json()
assert json == []
+
+
+def test_context_delivers_pending_msg():
+ """Test that context is able to hold pending messages while being init."""
+ context = owntracks.OwnTracksContext(
+ None, None, None, None, None, None, None, None
+ )
+ context.async_see(hello='world')
+ context.async_see(world='hello')
+ received = []
+
+ context.set_async_see(lambda **data: received.append(data))
+
+ assert len(received) == 2
+ assert received[0] == {'hello': 'world'}
+ assert received[1] == {'world': 'hello'}
+
+ received.clear()
+
+ context.set_async_see(lambda **data: received.append(data))
+ assert len(received) == 0
diff --git a/tests/components/remote/common.py b/tests/components/remote/common.py
index d03cf5d6d16139..30b158bca4b986 100644
--- a/tests/components/remote/common.py
+++ b/tests/components/remote/common.py
@@ -4,8 +4,9 @@
components. Instead call the service directly.
"""
from homeassistant.components.remote import (
- ATTR_ACTIVITY, ATTR_COMMAND, ATTR_DELAY_SECS, ATTR_DEVICE,
- ATTR_NUM_REPEATS, DOMAIN, SERVICE_SEND_COMMAND)
+ ATTR_ACTIVITY, ATTR_ALTERNATIVE, ATTR_COMMAND, ATTR_DELAY_SECS,
+ ATTR_DEVICE, ATTR_NUM_REPEATS, ATTR_TIMEOUT, DOMAIN,
+ SERVICE_LEARN_COMMAND, SERVICE_SEND_COMMAND)
from homeassistant.const import (
ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON)
from homeassistant.loader import bind_hass
@@ -53,3 +54,26 @@ def send_command(hass, command, entity_id=None, device=None,
data[ATTR_DELAY_SECS] = delay_secs
hass.services.call(DOMAIN, SERVICE_SEND_COMMAND, data)
+
+
+@bind_hass
+def learn_command(hass, entity_id=None, device=None, command=None,
+ alternative=None, timeout=None):
+ """Learn a command from a device."""
+ data = {}
+ if entity_id:
+ data[ATTR_ENTITY_ID] = entity_id
+
+ if device:
+ data[ATTR_DEVICE] = device
+
+ if command:
+ data[ATTR_COMMAND] = command
+
+ if alternative:
+ data[ATTR_ALTERNATIVE] = alternative
+
+ if timeout:
+ data[ATTR_TIMEOUT] = timeout
+
+ hass.services.call(DOMAIN, SERVICE_LEARN_COMMAND, data)
diff --git a/tests/components/remote/test_init.py b/tests/components/remote/test_init.py
index 2315dc1cf64514..2d1419c66aead3 100644
--- a/tests/components/remote/test_init.py
+++ b/tests/components/remote/test_init.py
@@ -13,6 +13,7 @@
TEST_PLATFORM = {remote.DOMAIN: {CONF_PLATFORM: 'test'}}
SERVICE_SEND_COMMAND = 'send_command'
+SERVICE_LEARN_COMMAND = 'learn_command'
class TestRemote(unittest.TestCase):
@@ -53,7 +54,7 @@ def test_turn_on(self):
self.hass.block_till_done()
- assert 1 == len(turn_on_calls)
+ assert len(turn_on_calls) == 1
call = turn_on_calls[-1]
assert remote.DOMAIN == call.domain
@@ -68,12 +69,12 @@ def test_turn_off(self):
self.hass.block_till_done()
- assert 1 == len(turn_off_calls)
+ assert len(turn_off_calls) == 1
call = turn_off_calls[-1]
- assert remote.DOMAIN == call.domain
- assert SERVICE_TURN_OFF == call.service
- assert 'entity_id_val' == call.data[ATTR_ENTITY_ID]
+ assert call.domain == remote.DOMAIN
+ assert call.service == SERVICE_TURN_OFF
+ assert call.data[ATTR_ENTITY_ID] == 'entity_id_val'
def test_send_command(self):
"""Test send_command."""
@@ -87,9 +88,28 @@ def test_send_command(self):
self.hass.block_till_done()
- assert 1 == len(send_command_calls)
+ assert len(send_command_calls) == 1
call = send_command_calls[-1]
- assert remote.DOMAIN == call.domain
- assert SERVICE_SEND_COMMAND == call.service
- assert 'entity_id_val' == call.data[ATTR_ENTITY_ID]
+ assert call.domain == remote.DOMAIN
+ assert call.service == SERVICE_SEND_COMMAND
+ assert call.data[ATTR_ENTITY_ID] == 'entity_id_val'
+
+ def test_learn_command(self):
+ """Test learn_command."""
+ learn_command_calls = mock_service(
+ self.hass, remote.DOMAIN, SERVICE_LEARN_COMMAND)
+
+ common.learn_command(
+ self.hass, entity_id='entity_id_val',
+ device='test_device', command=['test_command'],
+ alternative=True, timeout=20)
+
+ self.hass.block_till_done()
+
+ assert len(learn_command_calls) == 1
+ call = learn_command_calls[-1]
+
+ assert call.domain == remote.DOMAIN
+ assert call.service == SERVICE_LEARN_COMMAND
+ assert call.data[ATTR_ENTITY_ID] == 'entity_id_val'
diff --git a/tests/components/ssdp/__init__.py b/tests/components/ssdp/__init__.py
new file mode 100644
index 00000000000000..b6dcb9d49b5b36
--- /dev/null
+++ b/tests/components/ssdp/__init__.py
@@ -0,0 +1 @@
+"""Tests for the SSDP integration."""
diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py
new file mode 100644
index 00000000000000..4b1e27d2dc88ca
--- /dev/null
+++ b/tests/components/ssdp/test_init.py
@@ -0,0 +1,107 @@
+"""Test the SSDP integration."""
+import asyncio
+from unittest.mock import patch, Mock
+
+import aiohttp
+import pytest
+
+from homeassistant.generated import ssdp as gn_ssdp
+from homeassistant.components import ssdp
+
+from tests.common import mock_coro
+
+
+async def test_scan_match_st(hass):
+ """Test matching based on ST."""
+ scanner = ssdp.Scanner(hass)
+
+ with patch('netdisco.ssdp.scan', return_value=[
+ Mock(st="mock-st", location=None)
+ ]), patch.dict(
+ gn_ssdp.SSDP['st'], {'mock-st': ['mock-domain']}
+ ), patch.object(
+ hass.config_entries.flow, 'async_init',
+ return_value=mock_coro()
+ ) as mock_init:
+ await scanner.async_scan(None)
+
+ assert len(mock_init.mock_calls) == 1
+ assert mock_init.mock_calls[0][1][0] == 'mock-domain'
+ assert mock_init.mock_calls[0][2]['context'] == {'source': 'ssdp'}
+
+
+async def test_scan_match_manufacturer(hass, aioclient_mock):
+ """Test matching based on ST."""
+ aioclient_mock.get('http://1.1.1.1', text="""
+
+
+ Paulus
+
+
+ """)
+ scanner = ssdp.Scanner(hass)
+
+ with patch('netdisco.ssdp.scan', return_value=[
+ Mock(st="mock-st", location='http://1.1.1.1')
+ ]), patch.dict(
+ gn_ssdp.SSDP['manufacturer'], {'Paulus': ['mock-domain']}
+ ), patch.object(
+ hass.config_entries.flow, 'async_init',
+ return_value=mock_coro()
+ ) as mock_init:
+ await scanner.async_scan(None)
+
+ assert len(mock_init.mock_calls) == 1
+ assert mock_init.mock_calls[0][1][0] == 'mock-domain'
+ assert mock_init.mock_calls[0][2]['context'] == {'source': 'ssdp'}
+
+
+async def test_scan_match_device_type(hass, aioclient_mock):
+ """Test matching based on ST."""
+ aioclient_mock.get('http://1.1.1.1', text="""
+
+
+ Paulus
+
+
+ """)
+ scanner = ssdp.Scanner(hass)
+
+ with patch('netdisco.ssdp.scan', return_value=[
+ Mock(st="mock-st", location='http://1.1.1.1')
+ ]), patch.dict(
+ gn_ssdp.SSDP['device_type'], {'Paulus': ['mock-domain']}
+ ), patch.object(
+ hass.config_entries.flow, 'async_init',
+ return_value=mock_coro()
+ ) as mock_init:
+ await scanner.async_scan(None)
+
+ assert len(mock_init.mock_calls) == 1
+ assert mock_init.mock_calls[0][1][0] == 'mock-domain'
+ assert mock_init.mock_calls[0][2]['context'] == {'source': 'ssdp'}
+
+
+@pytest.mark.parametrize('exc', [asyncio.TimeoutError, aiohttp.ClientError])
+async def test_scan_description_fetch_fail(hass, aioclient_mock, exc):
+ """Test failing to fetch description."""
+ aioclient_mock.get('http://1.1.1.1', exc=exc)
+ scanner = ssdp.Scanner(hass)
+
+ with patch('netdisco.ssdp.scan', return_value=[
+ Mock(st="mock-st", location='http://1.1.1.1')
+ ]):
+ await scanner.async_scan(None)
+
+
+async def test_scan_description_parse_fail(hass, aioclient_mock):
+ """Test invalid XML."""
+ aioclient_mock.get('http://1.1.1.1', text="""
+INVALIDXML
+ """)
+ scanner = ssdp.Scanner(hass)
+
+ with patch('netdisco.ssdp.scan', return_value=[
+ Mock(st="mock-st", location='http://1.1.1.1')
+ ]):
+ await scanner.async_scan(None)
diff --git a/tests/components/tplink/test_common.py b/tests/components/tplink/test_common.py
new file mode 100644
index 00000000000000..6c963dc4617af9
--- /dev/null
+++ b/tests/components/tplink/test_common.py
@@ -0,0 +1,97 @@
+"""Common code tests."""
+from datetime import timedelta
+from unittest.mock import MagicMock
+
+from pyHS100 import SmartDeviceException
+
+from homeassistant.components.tplink.common import async_add_entities_retry
+from homeassistant.helpers.typing import HomeAssistantType
+
+
+async def test_async_add_entities_retry(
+ hass: HomeAssistantType
+):
+ """Test interval callback."""
+ async_add_entities_callback = MagicMock()
+
+ # The objects that will be passed to async_add_entities_callback.
+ objects = [
+ "Object 1",
+ "Object 2",
+ "Object 3",
+ "Object 4",
+ ]
+
+ # For each call to async_add_entities_callback, the following side effects
+ # will be triggered in order. This set of side effects accuratley simulates
+ # 3 attempts to add all entities while also handling several return types.
+ # To help understand what's going on, a comment exists describing what the
+ # object list looks like throughout the iterations.
+ callback_side_effects = [
+ # OB1, OB2, OB3, OB4
+ False,
+ False,
+ True, # Object 3
+ False,
+
+ # OB1, OB2, OB4
+ True, # Object 1
+ SmartDeviceException("My error"),
+ False,
+
+ # OB2, OB4
+ True, # Object 2
+ True, # Object 4
+ ]
+
+ callback = MagicMock(side_effect=callback_side_effects)
+
+ await async_add_entities_retry(
+ hass,
+ async_add_entities_callback,
+ objects,
+ callback,
+ interval=timedelta(milliseconds=100)
+ )
+ await hass.async_block_till_done()
+
+ assert callback.call_count == len(callback_side_effects)
+
+
+async def test_async_add_entities_retry_cancel(
+ hass: HomeAssistantType
+):
+ """Test interval callback."""
+ async_add_entities_callback = MagicMock()
+
+ callback_side_effects = [
+ False,
+ False,
+ True, # Object 1
+ False,
+ True, # Object 2
+ SmartDeviceException("My error"),
+ False,
+ True, # Object 3
+ True, # Object 4
+ ]
+
+ callback = MagicMock(side_effect=callback_side_effects)
+
+ objects = [
+ "Object 1",
+ "Object 2",
+ "Object 3",
+ "Object 4",
+ ]
+ cancel = await async_add_entities_retry(
+ hass,
+ async_add_entities_callback,
+ objects,
+ callback,
+ interval=timedelta(milliseconds=100)
+ )
+ cancel()
+ await hass.async_block_till_done()
+
+ assert callback.call_count == 4
diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py
index 1b234428c94097..2f8ad8e2960644 100644
--- a/tests/components/tplink/test_init.py
+++ b/tests/components/tplink/test_init.py
@@ -1,12 +1,20 @@
"""Tests for the TP-Link component."""
-from unittest.mock import patch
+from typing import Dict, Any
+from unittest.mock import MagicMock, patch
import pytest
+from pyHS100 import SmartPlug, SmartBulb, SmartDevice, SmartDeviceException
from homeassistant import config_entries, data_entry_flow
from homeassistant.components import tplink
+from homeassistant.components.tplink.common import (
+ CONF_DISCOVERY,
+ CONF_DIMMER,
+ CONF_LIGHT,
+ CONF_SWITCH,
+)
+from homeassistant.const import CONF_HOST
from homeassistant.setup import async_setup_component
-from pyHS100 import SmartPlug, SmartBulb
from tests.common import MockDependency, MockConfigEntry, mock_coro
MOCK_PYHS100 = MockDependency("pyHS100")
@@ -15,8 +23,8 @@
async def test_creating_entry_tries_discover(hass):
"""Test setting up does discovery."""
with MOCK_PYHS100, patch(
- "homeassistant.components.tplink.async_setup_entry",
- return_value=mock_coro(True),
+ "homeassistant.components.tplink.async_setup_entry",
+ return_value=mock_coro(True),
) as mock_setup, patch(
"pyHS100.Discover.discover", return_value={"host": 1234}
):
@@ -41,7 +49,7 @@ async def test_configuring_tplink_causes_discovery(hass):
"""Test that specifying empty config does discovery."""
with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover:
discover.return_value = {"host": 1234}
- await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}})
+ await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done()
assert len(discover.mock_calls) == 1
@@ -58,45 +66,111 @@ async def test_configuring_tplink_causes_discovery(hass):
async def test_configuring_device_types(hass, name, cls, platform, count):
"""Test that light or switch platform list is filled correctly."""
with patch("pyHS100.Discover.discover") as discover, patch(
- "pyHS100.SmartDevice._query_helper"
+ "pyHS100.SmartDevice._query_helper"
):
discovery_data = {
"123.123.123.{}".format(c): cls("123.123.123.123")
for c in range(count)
}
discover.return_value = discovery_data
- await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}})
+ await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done()
assert len(discover.mock_calls) == 1
assert len(hass.data[tplink.DOMAIN][platform]) == count
+class UnknownSmartDevice(SmartDevice):
+ """Dummy class for testing."""
+
+ @property
+ def has_emeter(self) -> bool:
+ """Do nothing."""
+ pass
+
+ def turn_off(self) -> None:
+ """Do nothing."""
+ pass
+
+ def turn_on(self) -> None:
+ """Do nothing."""
+ pass
+
+ @property
+ def is_on(self) -> bool:
+ """Do nothing."""
+ pass
+
+ @property
+ def state_information(self) -> Dict[str, Any]:
+ """Do nothing."""
+ pass
+
+
+async def test_configuring_devices_from_multiple_sources(hass):
+ """Test static and discover devices are not duplicated."""
+ with patch("pyHS100.Discover.discover") as discover, patch(
+ "pyHS100.SmartDevice._query_helper"
+ ):
+ discover_device_fail = SmartPlug("123.123.123.123")
+ discover_device_fail.get_sysinfo = MagicMock(
+ side_effect=SmartDeviceException()
+ )
+
+ discover.return_value = {
+ "123.123.123.1": SmartBulb("123.123.123.1"),
+ "123.123.123.2": SmartPlug("123.123.123.2"),
+ "123.123.123.3": SmartBulb("123.123.123.3"),
+ "123.123.123.4": SmartPlug("123.123.123.4"),
+ "123.123.123.123": discover_device_fail,
+ "123.123.123.124": UnknownSmartDevice("123.123.123.124")
+ }
+
+ await async_setup_component(hass, tplink.DOMAIN, {
+ tplink.DOMAIN: {
+ CONF_LIGHT: [
+ {CONF_HOST: "123.123.123.1"},
+ ],
+ CONF_SWITCH: [
+ {CONF_HOST: "123.123.123.2"},
+ ],
+ CONF_DIMMER: [
+ {CONF_HOST: "123.123.123.22"},
+ ],
+ }
+ })
+ await hass.async_block_till_done()
+
+ assert len(discover.mock_calls) == 1
+ assert len(hass.data[tplink.DOMAIN][CONF_LIGHT]) == 3
+ assert len(hass.data[tplink.DOMAIN][CONF_SWITCH]) == 2
+
+
async def test_is_dimmable(hass):
"""Test that is_dimmable switches are correctly added as lights."""
with patch("pyHS100.Discover.discover") as discover, patch(
- "homeassistant.components.tplink.light.async_setup_entry",
- return_value=mock_coro(True),
+ "homeassistant.components.tplink.light.async_setup_entry",
+ return_value=mock_coro(True),
) as setup, patch("pyHS100.SmartDevice._query_helper"), patch(
"pyHS100.SmartPlug.is_dimmable", True
):
dimmable_switch = SmartPlug("123.123.123.123")
discover.return_value = {"host": dimmable_switch}
- await async_setup_component(hass, tplink.DOMAIN, {"tplink": {}})
+ await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}})
await hass.async_block_till_done()
assert len(discover.mock_calls) == 1
assert len(setup.mock_calls) == 1
- assert len(hass.data[tplink.DOMAIN]["light"]) == 1
- assert len(hass.data[tplink.DOMAIN]["switch"]) == 0
+ assert len(hass.data[tplink.DOMAIN][CONF_LIGHT]) == 1
+ assert not hass.data[tplink.DOMAIN][CONF_SWITCH]
async def test_configuring_discovery_disabled(hass):
"""Test that discover does not get called when disabled."""
with MOCK_PYHS100, patch(
- "homeassistant.components.tplink.async_setup_entry",
- return_value=mock_coro(True),
+ "homeassistant.components.tplink.async_setup_entry",
+ return_value=mock_coro(True),
) as mock_setup, patch(
"pyHS100.Discover.discover", return_value=[]
) as discover:
@@ -107,22 +181,22 @@ async def test_configuring_discovery_disabled(hass):
)
await hass.async_block_till_done()
- assert len(discover.mock_calls) == 0
- assert len(mock_setup.mock_calls) == 1
+ assert discover.call_count == 0
+ assert mock_setup.call_count == 1
async def test_platforms_are_initialized(hass):
"""Test that platforms are initialized per configuration array."""
config = {
- "tplink": {
- "discovery": False,
- "light": [{"host": "123.123.123.123"}],
- "switch": [{"host": "321.321.321.321"}],
+ tplink.DOMAIN: {
+ CONF_DISCOVERY: False,
+ CONF_LIGHT: [{CONF_HOST: "123.123.123.123"}],
+ CONF_SWITCH: [{CONF_HOST: "321.321.321.321"}],
}
}
with patch("pyHS100.Discover.discover") as discover, patch(
- "pyHS100.SmartDevice._query_helper"
+ "pyHS100.SmartDevice._query_helper"
), patch(
"homeassistant.components.tplink.light.async_setup_entry",
return_value=mock_coro(True),
@@ -136,21 +210,21 @@ async def test_platforms_are_initialized(hass):
await async_setup_component(hass, tplink.DOMAIN, config)
await hass.async_block_till_done()
- assert len(discover.mock_calls) == 0
- assert len(light_setup.mock_calls) == 1
- assert len(switch_setup.mock_calls) == 1
+ assert discover.call_count == 0
+ assert light_setup.call_count == 1
+ assert switch_setup.call_count == 1
async def test_no_config_creates_no_entry(hass):
"""Test for when there is no tplink in config."""
with MOCK_PYHS100, patch(
- "homeassistant.components.tplink.async_setup_entry",
- return_value=mock_coro(True),
+ "homeassistant.components.tplink.async_setup_entry",
+ return_value=mock_coro(True),
) as mock_setup:
await async_setup_component(hass, tplink.DOMAIN, {})
await hass.async_block_till_done()
- assert len(mock_setup.mock_calls) == 0
+ assert mock_setup.call_count == 0
@pytest.mark.parametrize("platform", ["switch", "light"])
@@ -161,14 +235,14 @@ async def test_unload(hass, platform):
entry.add_to_hass(hass)
with patch("pyHS100.SmartDevice._query_helper"), patch(
- "homeassistant.components.tplink.{}"
- ".async_setup_entry".format(platform),
- return_value=mock_coro(True),
+ "homeassistant.components.tplink.{}"
+ ".async_setup_entry".format(platform),
+ return_value=mock_coro(True),
) as light_setup:
config = {
- "tplink": {
- platform: [{"host": "123.123.123.123"}],
- "discovery": False,
+ tplink.DOMAIN: {
+ platform: [{CONF_HOST: "123.123.123.123"}],
+ CONF_DISCOVERY: False,
}
}
assert await async_setup_component(hass, tplink.DOMAIN, config)
diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py
index 6756a01bbc7350..8fcc72dd4a585c 100644
--- a/tests/components/tradfri/test_config_flow.py
+++ b/tests/components/tradfri/test_config_flow.py
@@ -99,7 +99,7 @@ async def test_discovery_connection(hass, mock_auth, mock_entry_setup):
})
flow = await hass.config_entries.flow.async_init(
- 'tradfri', context={'source': 'discovery'}, data={
+ 'tradfri', context={'source': 'zeroconf'}, data={
'host': '123.123.123.123'
})
@@ -249,7 +249,7 @@ async def test_discovery_duplicate_aborted(hass):
).add_to_hass(hass)
flow = await hass.config_entries.flow.async_init(
- 'tradfri', context={'source': 'discovery'}, data={
+ 'tradfri', context={'source': 'zeroconf'}, data={
'host': 'some-host'
})
diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py
index 0fb0751c5b6248..5bc24c6c26980f 100644
--- a/tests/components/unifi/test_device_tracker.py
+++ b/tests/components/unifi/test_device_tracker.py
@@ -1,8 +1,6 @@
"""The tests for the Unifi WAP device tracker platform."""
from unittest import mock
from datetime import datetime, timedelta
-from pyunifi.controller import APIError
-
import pytest
import voluptuous as vol
@@ -13,13 +11,20 @@
from homeassistant.const import (CONF_HOST, CONF_USERNAME, CONF_PASSWORD,
CONF_PLATFORM, CONF_VERIFY_SSL,
CONF_MONITORED_CONDITIONS)
+
+from tests.common import mock_coro
+from asynctest import CoroutineMock
+from aiounifi.clients import Clients
+
DEFAULT_DETECTION_TIME = timedelta(seconds=300)
@pytest.fixture
def mock_ctrl():
"""Mock pyunifi."""
- with mock.patch('pyunifi.controller.Controller') as mock_control:
+ with mock.patch('aiounifi.Controller') as mock_control:
+ mock_control.return_value.login.return_value = mock_coro()
+ mock_control.return_value.initialize.return_value = mock_coro()
yield mock_control
@@ -33,7 +38,7 @@ def mock_scanner():
@mock.patch('os.access', return_value=True)
@mock.patch('os.path.isfile', mock.Mock(return_value=True))
-def test_config_valid_verify_ssl(hass, mock_scanner, mock_ctrl):
+async def test_config_valid_verify_ssl(hass, mock_scanner, mock_ctrl):
"""Test the setup with a string for ssl_verify.
Representing the absolute path to a CA certificate bundle.
@@ -46,12 +51,9 @@ def test_config_valid_verify_ssl(hass, mock_scanner, mock_ctrl):
CONF_VERIFY_SSL: "/tmp/unifi.crt"
})
}
- result = unifi.get_scanner(hass, config)
+ result = await unifi.async_get_scanner(hass, config)
assert mock_scanner.return_value == result
assert mock_ctrl.call_count == 1
- assert mock_ctrl.mock_calls[0] == \
- mock.call('localhost', 'foo', 'password', 8443,
- version='v4', site_id='default', ssl_verify="/tmp/unifi.crt")
assert mock_scanner.call_count == 1
assert mock_scanner.call_args == mock.call(mock_ctrl.return_value,
@@ -59,7 +61,7 @@ def test_config_valid_verify_ssl(hass, mock_scanner, mock_ctrl):
None, None)
-def test_config_minimal(hass, mock_scanner, mock_ctrl):
+async def test_config_minimal(hass, mock_scanner, mock_ctrl):
"""Test the setup with minimal configuration."""
config = {
DOMAIN: unifi.PLATFORM_SCHEMA({
@@ -68,12 +70,10 @@ def test_config_minimal(hass, mock_scanner, mock_ctrl):
CONF_PASSWORD: 'password',
})
}
- result = unifi.get_scanner(hass, config)
+
+ result = await unifi.async_get_scanner(hass, config)
assert mock_scanner.return_value == result
assert mock_ctrl.call_count == 1
- assert mock_ctrl.mock_calls[0] == \
- mock.call('localhost', 'foo', 'password', 8443,
- version='v4', site_id='default', ssl_verify=True)
assert mock_scanner.call_count == 1
assert mock_scanner.call_args == mock.call(mock_ctrl.return_value,
@@ -81,7 +81,7 @@ def test_config_minimal(hass, mock_scanner, mock_ctrl):
None, None)
-def test_config_full(hass, mock_scanner, mock_ctrl):
+async def test_config_full(hass, mock_scanner, mock_ctrl):
"""Test the setup with full configuration."""
config = {
DOMAIN: unifi.PLATFORM_SCHEMA({
@@ -96,12 +96,9 @@ def test_config_full(hass, mock_scanner, mock_ctrl):
'detection_time': 300,
})
}
- result = unifi.get_scanner(hass, config)
+ result = await unifi.async_get_scanner(hass, config)
assert mock_scanner.return_value == result
assert mock_ctrl.call_count == 1
- assert mock_ctrl.call_args == \
- mock.call('myhost', 'foo', 'password', 123,
- version='v4', site_id='abcdef01', ssl_verify=False)
assert mock_scanner.call_count == 1
assert mock_scanner.call_args == mock.call(
@@ -137,7 +134,7 @@ def test_config_error():
})
-def test_config_controller_failed(hass, mock_ctrl, mock_scanner):
+async def test_config_controller_failed(hass, mock_ctrl, mock_scanner):
"""Test for controller failure."""
config = {
'device_tracker': {
@@ -146,13 +143,12 @@ def test_config_controller_failed(hass, mock_ctrl, mock_scanner):
CONF_PASSWORD: 'password',
}
}
- mock_ctrl.side_effect = APIError(
- '/', 500, 'foo', {}, None)
- result = unifi.get_scanner(hass, config)
+ mock_ctrl.side_effect = unifi.CannotConnect
+ result = await unifi.async_get_scanner(hass, config)
assert result is False
-def test_scanner_update():
+async def test_scanner_update():
"""Test the scanner update."""
ctrl = mock.MagicMock()
fake_clients = [
@@ -161,21 +157,20 @@ def test_scanner_update():
{'mac': '234', 'essid': 'barnet',
'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
]
- ctrl.get_clients.return_value = fake_clients
- unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None, None)
- assert ctrl.get_clients.call_count == 1
- assert ctrl.get_clients.call_args == mock.call()
+ ctrl.clients = Clients([], CoroutineMock(return_value=fake_clients))
+ scnr = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None, None)
+ await scnr.async_update()
+ assert len(scnr._clients) == 2
def test_scanner_update_error():
"""Test the scanner update for error."""
ctrl = mock.MagicMock()
- ctrl.get_clients.side_effect = APIError(
- '/', 500, 'foo', {}, None)
+ ctrl.get_clients.side_effect = unifi.aiounifi.AiounifiException
unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None, None)
-def test_scan_devices():
+async def test_scan_devices():
"""Test the scanning for devices."""
ctrl = mock.MagicMock()
fake_clients = [
@@ -184,12 +179,13 @@ def test_scan_devices():
{'mac': '234', 'essid': 'barnet',
'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
]
- ctrl.get_clients.return_value = fake_clients
- scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None, None)
- assert set(scanner.scan_devices()) == set(['123', '234'])
+ ctrl.clients = Clients([], CoroutineMock(return_value=fake_clients))
+ scnr = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None, None)
+ await scnr.async_update()
+ assert set(await scnr.async_scan_devices()) == set(['123', '234'])
-def test_scan_devices_filtered():
+async def test_scan_devices_filtered():
"""Test the scanning for devices based on SSID."""
ctrl = mock.MagicMock()
fake_clients = [
@@ -204,13 +200,13 @@ def test_scan_devices_filtered():
]
ssid_filter = ['foonet', 'barnet']
- ctrl.get_clients.return_value = fake_clients
- scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, ssid_filter,
- None)
- assert set(scanner.scan_devices()) == set(['123', '234', '890'])
+ ctrl.clients = Clients([], CoroutineMock(return_value=fake_clients))
+ scnr = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, ssid_filter, None)
+ await scnr.async_update()
+ assert set(await scnr.async_scan_devices()) == set(['123', '234', '890'])
-def test_get_device_name():
+async def test_get_device_name():
"""Test the getting of device names."""
ctrl = mock.MagicMock()
fake_clients = [
@@ -226,15 +222,16 @@ def test_get_device_name():
'essid': 'barnet',
'last_seen': '1504786810'},
]
- ctrl.get_clients.return_value = fake_clients
- scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None, None)
- assert scanner.get_device_name('123') == 'foobar'
- assert scanner.get_device_name('234') == 'Nice Name'
- assert scanner.get_device_name('456') is None
- assert scanner.get_device_name('unknown') is None
+ ctrl.clients = Clients([], CoroutineMock(return_value=fake_clients))
+ scnr = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None, None)
+ await scnr.async_update()
+ assert scnr.get_device_name('123') == 'foobar'
+ assert scnr.get_device_name('234') == 'Nice Name'
+ assert scnr.get_device_name('456') is None
+ assert scnr.get_device_name('unknown') is None
-def test_monitored_conditions():
+async def test_monitored_conditions():
"""Test the filtering of attributes."""
ctrl = mock.MagicMock()
fake_clients = [
@@ -254,16 +251,17 @@ def test_monitored_conditions():
'essid': 'barnet',
'last_seen': dt_util.as_timestamp(dt_util.utcnow())},
]
- ctrl.get_clients.return_value = fake_clients
- scanner = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None,
- ['essid', 'signal', 'latest_assoc_time'])
- assert scanner.get_extra_attributes('123') == {
+ ctrl.clients = Clients([], CoroutineMock(return_value=fake_clients))
+ scnr = unifi.UnifiScanner(ctrl, DEFAULT_DETECTION_TIME, None,
+ ['essid', 'signal', 'latest_assoc_time'])
+ await scnr.async_update()
+ assert scnr.get_extra_attributes('123') == {
'essid': 'barnet',
'signal': -60,
'latest_assoc_time': datetime(2000, 1, 1, 0, 0, tzinfo=dt_util.UTC)
}
- assert scanner.get_extra_attributes('234') == {
+ assert scnr.get_extra_attributes('234') == {
'essid': 'barnet',
'signal': -42
}
- assert scanner.get_extra_attributes('456') == {'essid': 'barnet'}
+ assert scnr.get_extra_attributes('456') == {'essid': 'barnet'}
diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py
index d2d19204b40fa2..ec5ab5a577bf08 100644
--- a/tests/components/unifi/test_init.py
+++ b/tests/components/unifi/test_init.py
@@ -146,7 +146,8 @@ async def test_flow_works(hass, aioclient_mock):
flow.hass = hass
with patch('aiounifi.Controller') as mock_controller:
- def mock_constructor(host, username, password, port, site, websession):
+ def mock_constructor(
+ host, username, password, port, site, websession, sslcontext):
"""Fake the controller constructor."""
mock_controller.host = host
mock_controller.username = username
@@ -254,7 +255,8 @@ async def test_user_permissions_low(hass, aioclient_mock):
flow.hass = hass
with patch('aiounifi.Controller') as mock_controller:
- def mock_constructor(host, username, password, port, site, websession):
+ def mock_constructor(
+ host, username, password, port, site, websession, sslcontext):
"""Fake the controller constructor."""
mock_controller.host = host
mock_controller.username = username
diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py
index 0596d5e0ed54d1..27c1dc757492d9 100644
--- a/tests/components/zeroconf/test_init.py
+++ b/tests/components/zeroconf/test_init.py
@@ -1,20 +1,29 @@
"""Test Zeroconf component setup process."""
from unittest.mock import patch
-from aiozeroconf import ServiceInfo, ServiceStateChange
+import pytest
+from zeroconf import ServiceInfo, ServiceStateChange
+from homeassistant.generated import zeroconf as zc_gen
from homeassistant.setup import async_setup_component
from homeassistant.components import zeroconf
+@pytest.fixture
+def mock_zeroconf():
+ """Mock zeroconf."""
+ with patch('homeassistant.components.zeroconf.Zeroconf') as mock_zc:
+ yield mock_zc.return_value
+
+
def service_update_mock(zeroconf, service, handlers):
"""Call service update handler."""
handlers[0](
- None, service, '{}.{}'.format('name', service),
+ zeroconf, service, '{}.{}'.format('name', service),
ServiceStateChange.Added)
-async def get_service_info_mock(service_type, name):
+def get_service_info_mock(service_type, name):
"""Return service info for get_service_info."""
return ServiceInfo(
service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0,
@@ -22,19 +31,44 @@ async def get_service_info_mock(service_type, name):
properties={b'macaddress': b'ABCDEF012345'})
-async def test_setup(hass):
+def get_homekit_info_mock(service_type, name):
+ """Return homekit info for get_service_info."""
+ return ServiceInfo(
+ service_type, name, address=b'\n\x00\x00\x14', port=80, weight=0,
+ priority=0, server='name.local.',
+ properties={b'md': b'LIFX Bulb'})
+
+
+async def test_setup(hass, mock_zeroconf):
"""Test configured options for a device are loaded via config entry."""
- with patch.object(hass.config_entries, 'flow') as mock_config_flow, \
- patch.object(zeroconf, 'ServiceBrowser') as MockServiceBrowser, \
- patch.object(zeroconf.Zeroconf, 'get_service_info') as \
- mock_get_service_info:
+ with patch.object(
+ hass.config_entries, 'flow'
+ ) as mock_config_flow, patch.object(
+ zeroconf, 'ServiceBrowser', side_effect=service_update_mock
+ ) as mock_service_browser:
+ mock_zeroconf.get_service_info.side_effect = get_service_info_mock
+ assert await async_setup_component(
+ hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
- MockServiceBrowser.side_effect = service_update_mock
- mock_get_service_info.side_effect = get_service_info_mock
+ assert len(mock_service_browser.mock_calls) == len(zc_gen.ZEROCONF)
+ assert len(mock_config_flow.mock_calls) == len(zc_gen.ZEROCONF) * 2
+
+async def test_homekit(hass, mock_zeroconf):
+ """Test configured options for a device are loaded via config entry."""
+ with patch.dict(
+ zc_gen.ZEROCONF, {
+ zeroconf.HOMEKIT_TYPE: ["homekit_controller"]
+ }, clear=True
+ ), patch.object(
+ hass.config_entries, 'flow'
+ ) as mock_config_flow, patch.object(
+ zeroconf, 'ServiceBrowser', side_effect=service_update_mock
+ ) as mock_service_browser:
+ mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock
assert await async_setup_component(
hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}})
- await hass.async_block_till_done()
- assert len(MockServiceBrowser.mock_calls) == 2
+ assert len(mock_service_browser.mock_calls) == 1
assert len(mock_config_flow.mock_calls) == 2
+ assert mock_config_flow.mock_calls[0][1][0] == 'lifx'
diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py
index cd2eb53c3fe767..4cc7dec1edfaf1 100644
--- a/tests/components/zha/common.py
+++ b/tests/components/zha/common.py
@@ -82,6 +82,8 @@ def __init__(self, ieee, manufacturer, model):
self.initializing = False
self.manufacturer = manufacturer
self.model = model
+ from zigpy.zdo.types import NodeDescriptor
+ self.node_desc = NodeDescriptor()
def make_device(in_cluster_ids, out_cluster_ids, device_type, ieee,
diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py
index e9d6370575b7a8..02a0eba46a389d 100644
--- a/tests/components/zha/test_light.py
+++ b/tests/components/zha/test_light.py
@@ -57,9 +57,9 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch):
level_device_on_off_cluster = zigpy_device_level.endpoints.get(1).on_off
level_device_level_cluster = zigpy_device_level.endpoints.get(1).level
on_off_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock(
- return_value=(sentinel.data, Status.SUCCESS))))
+ return_value=[sentinel.data, Status.SUCCESS])))
level_mock = MagicMock(side_effect=asyncio.coroutine(MagicMock(
- return_value=(sentinel.data, Status.SUCCESS))))
+ return_value=[sentinel.data, Status.SUCCESS])))
monkeypatch.setattr(level_device_on_off_cluster, 'request', on_off_mock)
monkeypatch.setattr(level_device_level_cluster, 'request', level_mock)
level_entity_id = make_entity_id(DOMAIN, zigpy_device_level,
@@ -137,7 +137,7 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id):
from zigpy.zcl.foundation import Status
with patch(
'zigpy.zcl.Cluster.request',
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
+ return_value=mock_coro([0x00, Status.SUCCESS])):
# turn on via UI
await hass.services.async_call(DOMAIN, 'turn_on', {
'entity_id': entity_id
@@ -154,7 +154,7 @@ async def async_test_off_from_hass(hass, cluster, entity_id):
from zigpy.zcl.foundation import Status
with patch(
'zigpy.zcl.Cluster.request',
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
+ return_value=mock_coro([0x01, Status.SUCCESS])):
# turn off via UI
await hass.services.async_call(DOMAIN, 'turn_off', {
'entity_id': entity_id
diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py
new file mode 100644
index 00000000000000..5f682cb9b45936
--- /dev/null
+++ b/tests/components/zha/test_lock.py
@@ -0,0 +1,88 @@
+"""Test zha lock."""
+from unittest.mock import call, patch
+from homeassistant.const import (
+ STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE)
+from homeassistant.components.lock import DOMAIN
+from tests.common import mock_coro
+from .common import (
+ async_init_zigpy_device, make_attribute, make_entity_id,
+ async_enable_traffic)
+
+LOCK_DOOR = 0
+UNLOCK_DOOR = 1
+
+
+async def test_lock(hass, config_entry, zha_gateway):
+ """Test zha lock platform."""
+ from zigpy.zcl.clusters.closures import DoorLock
+ from zigpy.zcl.clusters.general import Basic
+
+ # create zigpy device
+ zigpy_device = await async_init_zigpy_device(
+ hass, [DoorLock.cluster_id, Basic.cluster_id], [], None, zha_gateway)
+
+ # load up lock domain
+ await hass.config_entries.async_forward_entry_setup(
+ config_entry, DOMAIN)
+ await hass.async_block_till_done()
+
+ cluster = zigpy_device.endpoints.get(1).door_lock
+ entity_id = make_entity_id(DOMAIN, zigpy_device, cluster)
+ zha_device = zha_gateway.get_device(zigpy_device.ieee)
+
+ # test that the lock was created and that it is unavailable
+ assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
+
+ # allow traffic to flow through the gateway and device
+ await async_enable_traffic(hass, zha_gateway, [zha_device])
+
+ # test that the state has changed from unavailable to unlocked
+ assert hass.states.get(entity_id).state == STATE_UNLOCKED
+
+ # set state to locked
+ attr = make_attribute(0, 1)
+ cluster.handle_message(False, 1, 0x0a, [[attr]])
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_LOCKED
+
+ # set state to unlocked
+ attr.value.value = 2
+ cluster.handle_message(False, 0, 0x0a, [[attr]])
+ await hass.async_block_till_done()
+ assert hass.states.get(entity_id).state == STATE_UNLOCKED
+
+ # lock from HA
+ await async_lock(hass, cluster, entity_id)
+
+ # unlock from HA
+ await async_lock(hass, cluster, entity_id)
+
+
+async def async_lock(hass, cluster, entity_id):
+ """Test lock functionality from hass."""
+ from zigpy.zcl.foundation import Status
+ with patch(
+ 'zigpy.zcl.Cluster.request',
+ return_value=mock_coro([Status.SUCCESS, ])):
+ # lock via UI
+ await hass.services.async_call(DOMAIN, 'lock', {
+ 'entity_id': entity_id
+ }, blocking=True)
+ assert cluster.request.call_count == 1
+ assert cluster.request.call_args == call(
+ False, LOCK_DOOR, (), expect_reply=True, manufacturer=None)
+
+
+async def async_unlock(hass, cluster, entity_id):
+ """Test lock functionality from hass."""
+ from zigpy.zcl.foundation import Status
+ with patch(
+ 'zigpy.zcl.Cluster.request',
+ return_value=mock_coro([Status.SUCCESS, ])):
+ # lock via UI
+ await hass.services.async_call(DOMAIN, 'unlock', {
+ 'entity_id': entity_id
+ }, blocking=True)
+ assert cluster.request.call_count == 1
+ assert cluster.request.call_args == call(
+ False, UNLOCK_DOOR, (), expect_reply=True, manufacturer=None)
diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py
index b0bbc103a9e3a0..2120bd6baf550c 100644
--- a/tests/components/zha/test_switch.py
+++ b/tests/components/zha/test_switch.py
@@ -54,7 +54,7 @@ async def test_switch(hass, config_entry, zha_gateway):
# turn on from HA
with patch(
'zigpy.zcl.Cluster.request',
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
+ return_value=mock_coro([0x00, Status.SUCCESS])):
# turn on via UI
await hass.services.async_call(DOMAIN, 'turn_on', {
'entity_id': entity_id
@@ -66,7 +66,7 @@ async def test_switch(hass, config_entry, zha_gateway):
# turn off from HA
with patch(
'zigpy.zcl.Cluster.request',
- return_value=mock_coro([Status.SUCCESS, Status.SUCCESS])):
+ return_value=mock_coro([0x01, Status.SUCCESS])):
# turn off via UI
await hass.services.async_call(DOMAIN, 'turn_off', {
'entity_id': entity_id
diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py
index 576be0ce03ca3d..11fe9ae5e66f9f 100644
--- a/tests/components/zone/test_init.py
+++ b/tests/components/zone/test_init.py
@@ -221,3 +221,24 @@ def test_in_zone_works_for_passive_zones(self):
assert zone.zone.in_zone(self.hass.states.get('zone.passive_zone'),
latitude, longitude)
+
+
+async def test_core_config_update(hass):
+ """Test updating core config will update home zone."""
+ assert await setup.async_setup_component(hass, 'zone', {})
+
+ home = hass.states.get('zone.home')
+
+ await hass.config.async_update(
+ location_name='Updated Name',
+ latitude=10,
+ longitude=20,
+ )
+ await hass.async_block_till_done()
+
+ home_updated = hass.states.get('zone.home')
+
+ assert home is not home_updated
+ assert home_updated.name == 'Updated Name'
+ assert home_updated.attributes['latitude'] == 10
+ assert home_updated.attributes['longitude'] == 20
diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py
index 5f8a642333a41f..eda62e1614ce18 100644
--- a/tests/helpers/test_config_entry_flow.py
+++ b/tests/helpers/test_config_entry_flow.py
@@ -75,24 +75,26 @@ async def test_user_has_confirmation(hass, discovery_flow_conf):
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
-async def test_discovery_single_instance(hass, discovery_flow_conf):
- """Test we ask for confirmation via discovery."""
+@pytest.mark.parametrize('source', ['discovery', 'ssdp', 'zeroconf'])
+async def test_discovery_single_instance(hass, discovery_flow_conf, source):
+ """Test we not allow duplicates."""
flow = config_entries.HANDLERS['test']()
flow.hass = hass
MockConfigEntry(domain='test').add_to_hass(hass)
- result = await flow.async_step_discovery({})
+ result = await getattr(flow, "async_step_{}".format(source))({})
assert result['type'] == data_entry_flow.RESULT_TYPE_ABORT
assert result['reason'] == 'single_instance_allowed'
-async def test_discovery_confirmation(hass, discovery_flow_conf):
+@pytest.mark.parametrize('source', ['discovery', 'ssdp', 'zeroconf'])
+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
- result = await flow.async_step_discovery({})
+ result = await getattr(flow, "async_step_{}".format(source))({})
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
assert result['step_id'] == 'confirm'
diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py
index e0bd509d33045f..4b65904b8b2651 100644
--- a/tests/helpers/test_config_validation.py
+++ b/tests/helpers/test_config_validation.py
@@ -943,34 +943,6 @@ def test_comp_entity_ids():
schema(invalid)
-def test_schema_with_slug_keys_allows_old_slugs(caplog):
- """Test schema with slug keys allowing old slugs."""
- schema = cv.schema_with_slug_keys(str)
-
- with patch.dict(cv.INVALID_SLUGS_FOUND, clear=True):
- for value in ('_world', 'wow__yeah'):
- caplog.clear()
- # Will raise if not allowing old slugs
- schema({value: 'yo'})
- assert "Found invalid slug {}".format(value) in caplog.text
-
- assert len(cv.INVALID_SLUGS_FOUND) == 2
-
-
-def test_entity_id_allow_old_validation(caplog):
- """Test schema allowing old entity_ids."""
- schema = vol.Schema(cv.entity_id)
-
- with patch.dict(cv.INVALID_ENTITY_IDS_FOUND, clear=True):
- for value in ('hello.__world', 'great.wow__yeah'):
- caplog.clear()
- # Will raise if not allowing old entity ID
- schema(value)
- assert "Found invalid entity_id {}".format(value) in caplog.text
-
- assert len(cv.INVALID_ENTITY_IDS_FOUND) == 2
-
-
def test_uuid4_hex(caplog):
"""Test uuid validation."""
schema = vol.Schema(cv.uuid4_hex)
diff --git a/tests/test_config.py b/tests/test_config.py
index 5579679937bcdb..8e983c673c5ae1 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -256,7 +256,8 @@ async def test_entity_customization(hass):
@mock.patch('homeassistant.config.shutil')
@mock.patch('homeassistant.config.os')
-def test_remove_lib_on_upgrade(mock_os, mock_shutil, hass):
+@mock.patch('homeassistant.config.is_docker_env', return_value=False)
+def test_remove_lib_on_upgrade(mock_docker, mock_os, mock_shutil, hass):
"""Test removal of library on upgrade from before 0.50."""
ha_version = '0.49.0'
mock_os.path.isdir = mock.Mock(return_value=True)
@@ -275,6 +276,28 @@ def test_remove_lib_on_upgrade(mock_os, mock_shutil, hass):
assert mock_shutil.rmtree.call_args == mock.call(hass_path)
+@mock.patch('homeassistant.config.shutil')
+@mock.patch('homeassistant.config.os')
+@mock.patch('homeassistant.config.is_docker_env', return_value=True)
+def test_remove_lib_on_upgrade_94(mock_docker, mock_os, mock_shutil, hass):
+ """Test removal of library on upgrade from before 0.94 and in Docker."""
+ ha_version = '0.94.0b5'
+ mock_os.path.isdir = mock.Mock(return_value=True)
+ mock_open = mock.mock_open()
+ with mock.patch('homeassistant.config.open', mock_open, create=True):
+ opened_file = mock_open.return_value
+ # pylint: disable=no-member
+ opened_file.readline.return_value = ha_version
+ hass.config.path = mock.Mock()
+ config_util.process_ha_config_upgrade(hass)
+ hass_path = hass.config.path.return_value
+
+ assert mock_os.path.isdir.call_count == 1
+ assert mock_os.path.isdir.call_args == mock.call(hass_path)
+ assert mock_shutil.rmtree.call_count == 1
+ assert mock_shutil.rmtree.call_args == mock.call(hass_path)
+
+
def test_process_config_upgrade(hass):
"""Test update of version on upgrade."""
ha_version = '0.92.0'
@@ -421,7 +444,7 @@ async def test_updating_configuration(hass, hass_storage):
hass_storage["core.config"] = dict(core_data)
await config_util.async_process_ha_core_config(
hass, {'whitelist_external_dirs': '/tmp'})
- await hass.config.update(latitude=50)
+ await hass.config.async_update(latitude=50)
new_core_data = copy.deepcopy(core_data)
new_core_data['data']['latitude'] = 50
diff --git a/tests/test_core.py b/tests/test_core.py
index 15ab2baf3a9a8d..00bd4265da7a0d 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -955,7 +955,7 @@ def callback(event):
assert hass.config.latitude != 12
- await hass.config.update(latitude=12)
+ await hass.config.async_update(latitude=12)
await hass.async_block_till_done()
assert hass.config.latitude == 12
@@ -963,10 +963,10 @@ def callback(event):
assert events[0].data == {'latitude': 12}
-def test_bad_timezone_raises_value_error(hass):
+async def test_bad_timezone_raises_value_error(hass):
"""Test bad timezone raises ValueError."""
with pytest.raises(ValueError):
- hass.config.set_time_zone('not_a_timezone')
+ await hass.config.async_update(time_zone='not_a_timezone')
@patch('homeassistant.core.monotonic')
diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py
index f6e33d264b6e13..379ab35cad2ff3 100644
--- a/tests/test_data_entry_flow.py
+++ b/tests/test_data_entry_flow.py
@@ -21,16 +21,13 @@ async def async_create_flow(handler_name, *, context, data):
raise data_entry_flow.UnknownHandler
flow = handler()
- flow.init_step = context.get('init_step', 'init') \
- if context is not None else 'init'
- flow.source = context.get('source') \
- if context is not None else 'user_input'
+ flow.init_step = context.get('init_step', 'init')
+ flow.source = context.get('source')
return flow
async def async_add_entry(flow, result):
if result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY:
- result['source'] = flow.context.get('source') \
- if flow.context is not None else 'user'
+ result['source'] = flow.context.get('source')
entries.append(result)
return result
@@ -173,7 +170,7 @@ async def async_step_init(self, user_input=None):
assert entry['handler'] == 'test'
assert entry['title'] == 'Test Title'
assert entry['data'] == 'Test Data'
- assert entry['source'] == 'user'
+ assert entry['source'] is None
async def test_discovery_init_flow(manager):
diff --git a/tests/test_requirements.py b/tests/test_requirements.py
index dcc107ea07e1b4..fc9dee20ed2909 100644
--- a/tests/test_requirements.py
+++ b/tests/test_requirements.py
@@ -1,24 +1,15 @@
"""Test requirements module."""
import os
+from pathlib import Path
from unittest.mock import patch, call
from homeassistant import setup
from homeassistant.requirements import (
- CONSTRAINT_FILE, PackageLoadable, async_process_requirements)
-
-import pkg_resources
+ CONSTRAINT_FILE, async_process_requirements, PROGRESS_FILE, _install)
from tests.common import (
get_test_home_assistant, MockModule, mock_coro, mock_integration)
-RESOURCE_DIR = os.path.abspath(
- os.path.join(os.path.dirname(__file__), '..', 'resources'))
-
-TEST_NEW_REQ = 'pyhelloworld3==1.0.0'
-
-TEST_ZIP_REQ = 'file://{}#{}' \
- .format(os.path.join(RESOURCE_DIR, 'pyhelloworld3.zip'), TEST_NEW_REQ)
-
class TestRequirements:
"""Test the requirements module."""
@@ -37,11 +28,11 @@ def teardown_method(self, method):
@patch('os.path.dirname')
@patch('homeassistant.util.package.is_virtual_env', return_value=True)
+ @patch('homeassistant.util.package.is_docker_env', return_value=False)
@patch('homeassistant.util.package.install_package', return_value=True)
def test_requirement_installed_in_venv(
- self, mock_install, mock_venv, mock_dirname):
+ self, mock_install, mock_denv, mock_venv, mock_dirname):
"""Test requirement installed in virtual environment."""
- mock_venv.return_value = True
mock_dirname.return_value = 'ha_package_path'
self.hass.config.skip_pip = False
mock_integration(
@@ -51,13 +42,16 @@ def test_requirement_installed_in_venv(
assert 'comp' in self.hass.config.components
assert mock_install.call_args == call(
'package==0.0.1',
- constraints=os.path.join('ha_package_path', CONSTRAINT_FILE))
+ constraints=os.path.join('ha_package_path', CONSTRAINT_FILE),
+ no_cache_dir=False,
+ )
@patch('os.path.dirname')
@patch('homeassistant.util.package.is_virtual_env', return_value=False)
+ @patch('homeassistant.util.package.is_docker_env', return_value=False)
@patch('homeassistant.util.package.install_package', return_value=True)
def test_requirement_installed_in_deps(
- self, mock_install, mock_venv, mock_dirname):
+ self, mock_install, mock_denv, mock_venv, mock_dirname):
"""Test requirement installed in deps directory."""
mock_dirname.return_value = 'ha_package_path'
self.hass.config.skip_pip = False
@@ -68,7 +62,9 @@ def test_requirement_installed_in_deps(
assert 'comp' in self.hass.config.components
assert mock_install.call_args == call(
'package==0.0.1', target=self.hass.config.path('deps'),
- constraints=os.path.join('ha_package_path', CONSTRAINT_FILE))
+ constraints=os.path.join('ha_package_path', CONSTRAINT_FILE),
+ no_cache_dir=False,
+ )
async def test_install_existing_package(hass):
@@ -80,8 +76,7 @@ async def test_install_existing_package(hass):
assert len(mock_inst.mock_calls) == 1
- with patch('homeassistant.requirements.PackageLoadable.loadable',
- return_value=mock_coro(True)), \
+ with patch('homeassistant.util.package.is_installed', return_value=True), \
patch(
'homeassistant.util.package.install_package') as mock_inst:
assert await async_process_requirements(
@@ -90,37 +85,81 @@ async def test_install_existing_package(hass):
assert len(mock_inst.mock_calls) == 0
-async def test_check_package_global(hass):
- """Test for an installed package."""
- installed_package = list(pkg_resources.working_set)[0].project_name
- assert await PackageLoadable(hass).loadable(installed_package)
-
-
-async def test_check_package_zip(hass):
- """Test for an installed zip package."""
- assert not await PackageLoadable(hass).loadable(TEST_ZIP_REQ)
-
-
-async def test_package_loadable_installed_twice(hass):
- """Test that a package is loadable when installed twice.
-
- If a package is installed twice, only the first version will be imported.
- Test that package_loadable will only compare with the first package.
- """
- v1 = pkg_resources.Distribution(project_name='hello', version='1.0.0')
- v2 = pkg_resources.Distribution(project_name='hello', version='2.0.0')
+async def test_install_with_wheels_index(hass):
+ """Test an install attempt with wheels index URL."""
+ hass.config.skip_pip = False
+ mock_integration(
+ hass, MockModule('comp', requirements=['hello==1.0.0']))
- with patch('pkg_resources.find_distributions', side_effect=[[v1]]):
- assert not await PackageLoadable(hass).loadable('hello==2.0.0')
-
- with patch('pkg_resources.find_distributions', side_effect=[[v1], [v2]]):
- assert not await PackageLoadable(hass).loadable('hello==2.0.0')
+ with patch(
+ 'homeassistant.util.package.is_installed', return_value=False
+ ), \
+ patch(
+ 'homeassistant.util.package.is_docker_env', return_value=True
+ ), \
+ patch(
+ 'homeassistant.util.package.install_package'
+ ) as mock_inst, \
+ patch.dict(
+ os.environ, {'WHEELS_LINKS': "https://wheels.hass.io/test"}
+ ), \
+ patch(
+ 'os.path.dirname'
+ ) as mock_dir:
+ mock_dir.return_value = 'ha_package_path'
+ assert await setup.async_setup_component(hass, 'comp', {})
+ assert 'comp' in hass.config.components
+ print(mock_inst.call_args)
+ assert mock_inst.call_args == call(
+ 'hello==1.0.0', find_links="https://wheels.hass.io/test",
+ constraints=os.path.join('ha_package_path', CONSTRAINT_FILE),
+ no_cache_dir=True,
+ )
+
+
+async def test_install_on_docker(hass):
+ """Test an install attempt on an docker system env."""
+ hass.config.skip_pip = False
+ mock_integration(
+ hass, MockModule('comp', requirements=['hello==1.0.0']))
+
+ with patch(
+ 'homeassistant.util.package.is_installed', return_value=False
+ ), \
+ patch(
+ 'homeassistant.util.package.is_docker_env', return_value=True
+ ), \
+ patch(
+ 'homeassistant.util.package.install_package'
+ ) as mock_inst, \
+ patch(
+ 'os.path.dirname'
+ ) as mock_dir:
+ mock_dir.return_value = 'ha_package_path'
+ assert await setup.async_setup_component(hass, 'comp', {})
+ assert 'comp' in hass.config.components
+ print(mock_inst.call_args)
+ assert mock_inst.call_args == call(
+ 'hello==1.0.0',
+ constraints=os.path.join('ha_package_path', CONSTRAINT_FILE),
+ no_cache_dir=True,
+ )
+
+
+async def test_progress_lock(hass):
+ """Test an install attempt on an existing package."""
+ progress_path = Path(hass.config.path(PROGRESS_FILE))
+ kwargs = {'hello': 'world'}
- with patch('pkg_resources.find_distributions', side_effect=[[v2], [v1]]):
- assert await PackageLoadable(hass).loadable('hello==2.0.0')
+ def assert_env(req, **passed_kwargs):
+ """Assert the env."""
+ assert progress_path.exists()
+ assert req == 'hello'
+ assert passed_kwargs == kwargs
+ return True
- with patch('pkg_resources.find_distributions', side_effect=[[v2]]):
- assert await PackageLoadable(hass).loadable('hello==2.0.0')
+ with patch('homeassistant.util.package.install_package',
+ side_effect=assert_env):
+ _install(hass, 'hello', kwargs)
- with patch('pkg_resources.find_distributions', side_effect=[[v2]]):
- assert await PackageLoadable(hass).loadable('Hello==2.0.0')
+ assert not progress_path.exists()
diff --git a/tests/test_setup.py b/tests/test_setup.py
index 1dae51966beb8e..410d97b288d461 100644
--- a/tests/test_setup.py
+++ b/tests/test_setup.py
@@ -108,37 +108,6 @@ def test_validate_platform_config(self, caplog):
'platform_conf.whatever',
MockPlatform(platform_schema=platform_schema))
- with assert_setup_component(1):
- assert setup.setup_component(self.hass, 'platform_conf', {
- 'platform_conf': {
- 'platform': 'whatever',
- 'hello': 'world',
- 'invalid': 'extra',
- }
- })
- assert caplog.text.count('Your configuration contains '
- 'extra keys') == 1
-
- self.hass.data.pop(setup.DATA_SETUP)
- self.hass.config.components.remove('platform_conf')
-
- with assert_setup_component(2):
- assert setup.setup_component(self.hass, 'platform_conf', {
- 'platform_conf': {
- 'platform': 'whatever',
- 'hello': 'world',
- },
- 'platform_conf 2': {
- 'platform': 'whatever',
- 'invalid': True
- }
- })
- assert caplog.text.count('Your configuration contains '
- 'extra keys') == 2
-
- self.hass.data.pop(setup.DATA_SETUP)
- self.hass.config.components.remove('platform_conf')
-
with assert_setup_component(0):
assert setup.setup_component(self.hass, 'platform_conf', {
'platform_conf': {
@@ -206,21 +175,6 @@ def test_validate_platform_config_2(self, caplog):
MockPlatform('whatever',
platform_schema=platform_schema))
- with assert_setup_component(1):
- assert setup.setup_component(self.hass, 'platform_conf', {
- # fail: no extra keys allowed in platform schema
- 'platform_conf': {
- 'platform': 'whatever',
- 'hello': 'world',
- 'invalid': 'extra',
- }
- })
- assert caplog.text.count('Your configuration contains '
- 'extra keys') == 1
-
- self.hass.data.pop(setup.DATA_SETUP)
- self.hass.config.components.remove('platform_conf')
-
with assert_setup_component(1):
assert setup.setup_component(self.hass, 'platform_conf', {
# pass
@@ -235,9 +189,6 @@ def test_validate_platform_config_2(self, caplog):
}
})
- self.hass.data.pop(setup.DATA_SETUP)
- self.hass.config.components.remove('platform_conf')
-
def test_validate_platform_config_3(self, caplog):
"""Test fallback to component PLATFORM_SCHEMA."""
component_schema = PLATFORM_SCHEMA_BASE.extend({
@@ -258,20 +209,6 @@ def test_validate_platform_config_3(self, caplog):
MockPlatform('whatever',
platform_schema=platform_schema))
- with assert_setup_component(1):
- assert setup.setup_component(self.hass, 'platform_conf', {
- 'platform_conf': {
- 'platform': 'whatever',
- 'hello': 'world',
- 'invalid': 'extra',
- }
- })
- assert caplog.text.count('Your configuration contains '
- 'extra keys') == 1
-
- self.hass.data.pop(setup.DATA_SETUP)
- self.hass.config.components.remove('platform_conf')
-
with assert_setup_component(1):
assert setup.setup_component(self.hass, 'platform_conf', {
# pass
@@ -286,9 +223,6 @@ def test_validate_platform_config_3(self, caplog):
}
})
- self.hass.data.pop(setup.DATA_SETUP)
- self.hass.config.components.remove('platform_conf')
-
def test_validate_platform_config_4(self):
"""Test entity_namespace in PLATFORM_SCHEMA."""
component_schema = PLATFORM_SCHEMA_BASE
diff --git a/tests/util/test_package.py b/tests/util/test_package.py
index 5422140c232b6e..3751c0569074b2 100644
--- a/tests/util/test_package.py
+++ b/tests/util/test_package.py
@@ -6,13 +6,20 @@
from subprocess import PIPE
from unittest.mock import MagicMock, call, patch
+import pkg_resources
import pytest
import homeassistant.util.package as package
+RESOURCE_DIR = os.path.abspath(
+ os.path.join(os.path.dirname(__file__), '..', 'resources'))
+
TEST_NEW_REQ = 'pyhelloworld3==1.0.0'
+TEST_ZIP_REQ = 'file://{}#{}' \
+ .format(os.path.join(RESOURCE_DIR, 'pyhelloworld3.zip'), TEST_NEW_REQ)
+
@pytest.fixture
def mock_sys():
@@ -160,6 +167,23 @@ def test_install_constraint(mock_sys, mock_popen, mock_env_copy, mock_venv):
assert mock_popen.return_value.communicate.call_count == 1
+def test_install_find_links(mock_sys, mock_popen, mock_env_copy, mock_venv):
+ """Test install with find-links on not installed package."""
+ env = mock_env_copy()
+ link = 'https://wheels-repository'
+ assert package.install_package(
+ TEST_NEW_REQ, False, find_links=link)
+ assert mock_popen.call_count == 1
+ assert (
+ mock_popen.call_args ==
+ call([
+ mock_sys.executable, '-m', 'pip', 'install', '--quiet',
+ TEST_NEW_REQ, '--find-links', link
+ ], stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env)
+ )
+ assert mock_popen.return_value.communicate.call_count == 1
+
+
@asyncio.coroutine
def test_async_get_user_site(mock_env_copy):
"""Test async get user site directory."""
@@ -176,3 +200,14 @@ def test_async_get_user_site(mock_env_copy):
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
env=env)
assert ret == os.path.join(deps_dir, 'lib_dir')
+
+
+def test_check_package_global():
+ """Test for an installed package."""
+ installed_package = list(pkg_resources.working_set)[0].project_name
+ assert package.is_installed(installed_package)
+
+
+def test_check_package_zip():
+ """Test for an installed zip package."""
+ assert not package.is_installed(TEST_ZIP_REQ)